diff --git a/app/build.gradle b/app/build.gradle
index cb5e455ba..8442109a2 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -37,7 +37,14 @@ android {
versionCode 6
versionName "1.0.5"
- resourceConfigurations += ["en", "uk"]
+ // Only include supported languages to reduce APK size
+ // Must match languages available in LanguageFragment.kt
+ resourceConfigurations += [
+ "en", // English (default)
+ "bo", // Tibetan
+ "vi", // Vietnamese
+ "zh-rHK" // Chinese - Hong Kong
+ ]
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt
index 19c096338..5a810b73d 100644
--- a/app/src/main/java/org/openedx/app/AppActivity.kt
+++ b/app/src/main/java/org/openedx/app/AppActivity.kt
@@ -8,6 +8,7 @@ import android.os.Bundle
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.app.AppCompatDelegate
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
@@ -27,6 +28,7 @@ import org.openedx.auth.presentation.logistration.LogistrationFragment
import org.openedx.auth.presentation.signin.SignInFragment
import org.openedx.core.ApiConstants
import org.openedx.core.data.storage.CorePreferences
+import org.openedx.core.data.storage.ThemeMode
import org.openedx.core.presentation.global.InsetHolder
import org.openedx.core.presentation.global.WindowSizeHolder
import org.openedx.core.utils.Logger
@@ -102,6 +104,7 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ applyThemeMode()
installSplashScreen()
binding = ActivityAppBinding.inflate(layoutInflater)
lifecycle.addObserver(viewModel)
@@ -276,6 +279,30 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder {
viewModel.makeExternalRoute(supportFragmentManager, deepLink)
}
+ private fun applyThemeMode() {
+ val themeMode = corePreferencesManager.themeMode
+
+ val nightMode = when (themeMode) {
+ ThemeMode.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
+ ThemeMode.DARK -> AppCompatDelegate.MODE_NIGHT_YES
+ ThemeMode.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
+ }
+
+ AppCompatDelegate.setDefaultNightMode(nightMode)
+
+ // Also apply language
+ applyLanguage()
+ }
+
+ private fun applyLanguage() {
+ val languageCode = corePreferencesManager.appLanguage
+
+ if (languageCode.isNotEmpty()) {
+ val localeList = androidx.core.os.LocaleListCompat.forLanguageTags(languageCode)
+ AppCompatDelegate.setApplicationLocales(localeList)
+ }
+ }
+
companion object {
const val TOP_INSET = "topInset"
const val BOTTOM_INSET = "bottomInset"
diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt
index 0130d6b31..90f6625e8 100644
--- a/app/src/main/java/org/openedx/app/AppRouter.kt
+++ b/app/src/main/java/org/openedx/app/AppRouter.kt
@@ -53,7 +53,9 @@ import org.openedx.profile.presentation.delete.DeleteProfileFragment
import org.openedx.profile.presentation.edit.EditProfileFragment
import org.openedx.profile.presentation.manageaccount.ManageAccountFragment
import org.openedx.profile.presentation.profile.ProfileFragment
+import org.openedx.profile.presentation.settings.LanguageFragment
import org.openedx.profile.presentation.settings.SettingsFragment
+import org.openedx.profile.presentation.settings.ThemeFragment
import org.openedx.profile.presentation.video.VideoSettingsFragment
import org.openedx.whatsnew.WhatsNewRouter
import org.openedx.whatsnew.presentation.whatsnew.WhatsNewFragment
@@ -441,6 +443,14 @@ class AppRouter :
override fun navigateToCoursesToSync(fm: FragmentManager) {
replaceFragmentWithBackStack(fm, CoursesToSyncFragment())
}
+
+ override fun navigateToThemeSettings(fm: FragmentManager) {
+ replaceFragmentWithBackStack(fm, ThemeFragment.newInstance())
+ }
+
+ override fun navigateToLanguageSettings(fm: FragmentManager) {
+ replaceFragmentWithBackStack(fm, LanguageFragment.newInstance())
+ }
// endregion
fun getVisibleFragment(fm: FragmentManager): Fragment? {
diff --git a/app/src/main/java/org/openedx/app/MainFragment.kt b/app/src/main/java/org/openedx/app/MainFragment.kt
index 3ab735d27..e2909757b 100644
--- a/app/src/main/java/org/openedx/app/MainFragment.kt
+++ b/app/src/main/java/org/openedx/app/MainFragment.kt
@@ -42,6 +42,7 @@ class MainFragment : Fragment(R.layout.fragment_main) {
super.onViewCreated(view, savedInstanceState)
initViewPager()
+ fixBottomNavigationTextRendering()
binding.bottomNavView.setOnItemSelectedListener {
when (it.itemId) {
@@ -134,6 +135,49 @@ class MainFragment : Fragment(R.layout.fragment_main) {
}
}
+ /**
+ * Fix Tibetan text rendering in bottom navigation.
+ * Moves Tibetan labels down to prevent overlapping with icons.
+ */
+ private fun fixBottomNavigationTextRendering() {
+ binding.bottomNavView.post {
+ val bottomNavMenuView = binding.bottomNavView.getChildAt(0) as? android.view.ViewGroup
+ bottomNavMenuView?.let { menuView ->
+ for (i in 0 until menuView.childCount) {
+ val item = menuView.getChildAt(i) as? android.view.ViewGroup
+ item?.let { itemView ->
+ fixTextViewsRecursively(itemView)
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Recursively find TextViews and fix Tibetan text rendering.
+ */
+ private fun fixTextViewsRecursively(view: View) {
+ if (view is android.widget.TextView) {
+ // Check if text contains Tibetan characters (Unicode range U+0F00-U+0FFF)
+ val text = view.text?.toString() ?: ""
+ val hasTibetan = text.any { it.code in 0x0F00..0x0FFF }
+
+ if (hasTibetan) {
+ // Only apply fixes for Tibetan text
+ view.includeFontPadding = false
+
+ // Move text down 8dp to prevent overlap with icon
+ val density = resources.displayMetrics.density
+ view.translationY = 8 * density
+ }
+ } else if (view is android.view.ViewGroup) {
+ for (i in 0 until view.childCount) {
+ fixTextViewsRecursively(view.getChildAt(i))
+ }
+ }
+ }
+
+
companion object {
private const val ARG_COURSE_ID = "courseId"
private const val ARG_INFO_TYPE = "info_type"
diff --git a/app/src/main/java/org/openedx/app/OpenEdXApp.kt b/app/src/main/java/org/openedx/app/OpenEdXApp.kt
index 6524cde5d..74deca322 100644
--- a/app/src/main/java/org/openedx/app/OpenEdXApp.kt
+++ b/app/src/main/java/org/openedx/app/OpenEdXApp.kt
@@ -1,6 +1,7 @@
package org.openedx.app
import android.app.Application
+import androidx.appcompat.app.AppCompatDelegate
import com.braze.Braze
import com.braze.configuration.BrazeConfig
import com.braze.ui.BrazeDeeplinkHandler
@@ -14,6 +15,7 @@ import org.openedx.app.di.appModule
import org.openedx.app.di.networkingModule
import org.openedx.app.di.screenModule
import org.openedx.core.config.Config
+import org.openedx.core.data.storage.ThemeMode
import org.openedx.firebase.OEXFirebaseAnalytics
class OpenEdXApp : Application() {
@@ -23,6 +25,8 @@ class OpenEdXApp : Application() {
override fun onCreate() {
super.onCreate()
+ applyThemeMode()
+
startKoin {
androidContext(this@OpenEdXApp)
modules(
@@ -31,6 +35,7 @@ class OpenEdXApp : Application() {
screenModule
)
}
+
if (config.getFirebaseConfig().enabled) {
FirebaseApp.initializeApp(this)
}
@@ -69,4 +74,39 @@ class OpenEdXApp : Application() {
pluginManager.addPlugin(OEXFirebaseAnalytics(context = this))
}
}
+
+ private fun applyThemeMode() {
+ val prefs = getSharedPreferences(BuildConfig.APPLICATION_ID, MODE_PRIVATE)
+ val themeModeString = prefs.getString("theme_mode", ThemeMode.SYSTEM.name)
+ ?: ThemeMode.SYSTEM.name
+
+ val themeMode = try {
+ ThemeMode.valueOf(themeModeString)
+ } catch (_: Exception) {
+ ThemeMode.SYSTEM
+ }
+
+ val nightMode = when (themeMode) {
+ ThemeMode.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
+ ThemeMode.DARK -> AppCompatDelegate.MODE_NIGHT_YES
+ ThemeMode.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
+ }
+
+ AppCompatDelegate.setDefaultNightMode(nightMode)
+ applyLanguage()
+ }
+
+ private fun applyLanguage() {
+ val prefs = getSharedPreferences(BuildConfig.APPLICATION_ID, MODE_PRIVATE)
+ val languageCode = prefs.getString("app_language", "") ?: ""
+
+ if (languageCode.isNotEmpty()) {
+ // Use AppCompatDelegate for proper locale handling
+ val localeList = androidx.core.os.LocaleListCompat.forLanguageTags(languageCode)
+ AppCompatDelegate.setApplicationLocales(localeList)
+ } else {
+ // Empty string means use system language
+ AppCompatDelegate.setApplicationLocales(androidx.core.os.LocaleListCompat.getEmptyLocaleList())
+ }
+ }
}
diff --git a/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt b/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt
index 3c8ea881e..aac165860 100644
--- a/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt
+++ b/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt
@@ -219,6 +219,23 @@ class PreferencesManager(context: Context) :
override fun isCalendarSyncEventsDialogShown(courseName: String): Boolean =
getBoolean(courseName.replaceSpace("_"))
+ override var themeMode: org.openedx.core.data.storage.ThemeMode
+ set(value) {
+ saveString(THEME_MODE, value.name)
+ }
+ get() {
+ val v = getString(THEME_MODE, defValue = org.openedx.core.data.storage.ThemeMode.SYSTEM.name)
+ return try {
+ org.openedx.core.data.storage.ThemeMode.valueOf(v)
+ } catch (e: Exception) {
+ org.openedx.core.data.storage.ThemeMode.SYSTEM
+ }
+ }
+
+ override var appLanguage: String
+ get() = getString(APP_LANGUAGE, "")
+ set(value) = saveString(APP_LANGUAGE, value)
+
companion object {
private const val ACCESS_TOKEN = "access_token"
private const val REFRESH_TOKEN = "refresh_token"
@@ -239,5 +256,7 @@ class PreferencesManager(context: Context) :
private const val IS_RELATIVE_DATES_ENABLED = "IS_RELATIVE_DATES_ENABLED"
private const val HIDE_INACTIVE_COURSES = "HIDE_INACTIVE_COURSES"
private const val CALENDAR_USER = "CALENDAR_USER"
+ private const val THEME_MODE = "theme_mode"
+ private const val APP_LANGUAGE = "app_language"
}
}
diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt
index 22acad07c..e9742b1c0 100644
--- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt
+++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt
@@ -208,6 +208,7 @@ val screenModule = module {
get(),
get(),
get(),
+ get(),
)
}
viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) }
diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml
index 43ca09370..f3627a0cb 100644
--- a/app/src/main/res/xml/locales_config.xml
+++ b/app/src/main/res/xml/locales_config.xml
@@ -1,5 +1,9 @@
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
diff --git a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt
index 5435494ba..555922496 100644
--- a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt
+++ b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt
@@ -4,6 +4,12 @@ import org.openedx.core.data.model.User
import org.openedx.core.domain.model.AppConfig
import org.openedx.core.domain.model.VideoSettings
+enum class ThemeMode {
+ LIGHT,
+ DARK,
+ SYSTEM
+}
+
interface CorePreferences {
var accessToken: String
var refreshToken: String
@@ -14,6 +20,7 @@ interface CorePreferences {
var appConfig: AppConfig
var canResetAppDirectory: Boolean
var isRelativeDatesEnabled: Boolean
-
+ var themeMode: ThemeMode
+ var appLanguage: String
fun clearCorePreferences()
}
diff --git a/core/src/main/java/org/openedx/core/ui/WebContentScreen.kt b/core/src/main/java/org/openedx/core/ui/WebContentScreen.kt
index a65411042..296f9999e 100644
--- a/core/src/main/java/org/openedx/core/ui/WebContentScreen.kt
+++ b/core/src/main/java/org/openedx/core/ui/WebContentScreen.kt
@@ -6,7 +6,6 @@ import android.net.Uri
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
-import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -36,6 +35,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.zIndex
import org.openedx.core.ui.theme.appColors
+import org.openedx.core.ui.theme.getDarkThemeFromPreferences
import org.openedx.core.utils.EmailUtil
import org.openedx.foundation.extension.applyDarkModeIfEnabled
import org.openedx.foundation.extension.isEmailValid
@@ -132,7 +132,7 @@ private fun WebViewContent(
onWebPageLoaded: () -> Unit
) {
val context = LocalContext.current
- val isDarkTheme = isSystemInDarkTheme()
+ val isDarkTheme = getDarkThemeFromPreferences()
AndroidView(
factory = {
WebView(context).apply {
diff --git a/core/src/main/java/org/openedx/core/ui/theme/Theme.kt b/core/src/main/java/org/openedx/core/ui/theme/Theme.kt
index 2ad2a4eae..0a9200e23 100644
--- a/core/src/main/java/org/openedx/core/ui/theme/Theme.kt
+++ b/core/src/main/java/org/openedx/core/ui/theme/Theme.kt
@@ -9,6 +9,10 @@ import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.platform.LocalContext
+import org.koin.core.context.GlobalContext
+import android.content.Context
+import org.openedx.core.data.storage.ThemeMode
private val DarkColorPalette = AppColors(
material = darkColors(
@@ -195,7 +199,7 @@ val MaterialTheme.appColors: AppColors
@OptIn(ExperimentalFoundationApi::class)
@Composable
-fun OpenEdXTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
+fun OpenEdXTheme(darkTheme: Boolean = getDarkThemeFromPreferences(), content: @Composable () -> Unit) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
@@ -213,3 +217,20 @@ fun OpenEdXTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composabl
)
}
}
+
+@Composable
+fun getDarkThemeFromPreferences(): Boolean {
+ val context = LocalContext.current
+ val prefs = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
+ val value = prefs.getString("theme_mode", ThemeMode.SYSTEM.name) ?: ThemeMode.SYSTEM.name
+ val mode = try {
+ ThemeMode.valueOf(value)
+ } catch (e: Exception) {
+ ThemeMode.SYSTEM
+ }
+ return when (mode) {
+ ThemeMode.LIGHT -> false
+ ThemeMode.DARK -> true
+ ThemeMode.SYSTEM -> isSystemInDarkTheme()
+ }
+}
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
index a82cd7da5..4702b8198 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -191,4 +191,13 @@
Not Synced
Syncing to calendar…
Next
+
+
+ Theme
+ Use system theme
+ Light
+ Dark
+
+
+ Language
diff --git a/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt b/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt
index e9f67ad48..d4bc834cf 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt
@@ -23,4 +23,8 @@ interface ProfileRouter {
fun navigateToManageAccount(fm: FragmentManager)
fun navigateToCoursesToSync(fm: FragmentManager)
+
+ fun navigateToThemeSettings(fm: FragmentManager)
+
+ fun navigateToLanguageSettings(fm: FragmentManager)
}
diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/LanguageFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/LanguageFragment.kt
new file mode 100644
index 000000000..41cb3ba41
--- /dev/null
+++ b/profile/src/main/java/org/openedx/profile/presentation/settings/LanguageFragment.kt
@@ -0,0 +1,275 @@
+package org.openedx.profile.presentation.settings
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Card
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+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.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.fragment.app.Fragment
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.openedx.core.R
+import org.openedx.core.ui.Toolbar
+import org.openedx.core.ui.displayCutoutForLandscape
+import org.openedx.core.ui.settingsHeaderBackground
+import org.openedx.core.ui.statusBarsInset
+import org.openedx.core.ui.theme.OpenEdXTheme
+import org.openedx.core.ui.theme.appColors
+import org.openedx.core.ui.theme.appShapes
+import org.openedx.core.ui.theme.appTypography
+import org.openedx.foundation.extension.tagId
+import org.openedx.foundation.presentation.WindowSize
+import org.openedx.foundation.presentation.WindowType
+import org.openedx.foundation.presentation.rememberWindowSize
+import org.openedx.foundation.presentation.windowSizeValue
+import org.openedx.profile.presentation.ui.SettingsDivider
+
+class LanguageFragment : Fragment() {
+
+ private val viewModel by viewModel()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ) = ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ OpenEdXTheme {
+ val windowSize = rememberWindowSize()
+ val uiState by viewModel.uiState.collectAsState()
+
+ val currentLanguage = (uiState as? SettingsUIState.Data)?.appLanguage ?: ""
+
+ LanguageScreen(
+ windowSize = windowSize,
+ currentLanguage = currentLanguage,
+ onLanguageSelected = { languageCode ->
+ viewModel.setAppLanguage(languageCode)
+ // Navigate back immediately, then recreate will happen
+ requireActivity().supportFragmentManager.popBackStack()
+ },
+ onBackClick = {
+ requireActivity().supportFragmentManager.popBackStack()
+ }
+ )
+
+ // Recreate activity when language is changed to apply new language.
+ LaunchedEffect(Unit) {
+ viewModel.languageChanged.collect {
+ // Activity recreation after navigation back
+ requireActivity().recreate()
+ }
+ }
+ }
+ }
+ }
+
+ companion object {
+ fun newInstance(): LanguageFragment {
+ return LanguageFragment()
+ }
+ }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun LanguageScreen(
+ windowSize: WindowSize,
+ currentLanguage: String,
+ onLanguageSelected: (String) -> Unit,
+ onBackClick: () -> Unit
+) {
+ val topBarWidth by remember(key1 = windowSize) {
+ mutableStateOf(
+ windowSize.windowSizeValue(
+ expanded = Modifier.widthIn(Dp.Unspecified, 560.dp),
+ compact = Modifier.fillMaxWidth()
+ )
+ )
+ }
+
+ val contentWidth by remember(key1 = windowSize) {
+ mutableStateOf(
+ windowSize.windowSizeValue(
+ expanded = Modifier.widthIn(Dp.Unspecified, 420.dp),
+ compact = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp)
+ )
+ )
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .settingsHeaderBackground()
+ .statusBarsInset()
+ ) {
+ Toolbar(
+ modifier = topBarWidth
+ .align(Alignment.CenterHorizontally)
+ .displayCutoutForLandscape(),
+ label = stringResource(id = R.string.core_language),
+ canShowBackBtn = true,
+ labelTint = MaterialTheme.appColors.settingsTitleContent,
+ iconTint = MaterialTheme.appColors.settingsTitleContent,
+ onBackClick = onBackClick
+ )
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ shape = MaterialTheme.appShapes.screenBackgroundShape,
+ color = MaterialTheme.appColors.background
+ ) {
+ Box(
+ contentAlignment = Alignment.TopCenter
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .displayCutoutForLandscape(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxHeight()
+ .then(contentWidth)
+ .verticalScroll(rememberScrollState()),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(Modifier.height(30.dp))
+
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = MaterialTheme.appShapes.cardShape,
+ elevation = 0.dp,
+ backgroundColor = MaterialTheme.appColors.cardViewBackground
+ ) {
+ Column(Modifier.fillMaxWidth()) {
+ val languages = remember {
+ listOf(
+ LanguageOption("en", "English"),
+ LanguageOption("bo", "བོད་སྐད"),
+ LanguageOption("zh-HK", "繁體中文"),
+ LanguageOption("vi", "Tiếng Việt")
+ )
+ }
+
+ languages.forEachIndexed { index, language ->
+ LanguageOption(
+ name = language.displayName,
+ code = language.code,
+ selected = currentLanguage == language.code,
+ onClick = {
+ onLanguageSelected(language.code)
+ }
+ )
+
+ if (index < languages.size - 1) {
+ SettingsDivider()
+ }
+ }
+ }
+ }
+
+ Spacer(Modifier.height(24.dp))
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun LanguageOption(
+ name: String,
+ code: String,
+ selected: Boolean,
+ onClick: () -> Unit
+) {
+ Row(
+ Modifier
+ .testTag("btn_language_${name.tagId()}")
+ .fillMaxWidth()
+ .clickable {
+ onClick()
+ }
+ .padding(horizontal = 20.dp, vertical = 24.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ modifier = Modifier.testTag("txt_language_${name.tagId()}"),
+ text = name,
+ color = MaterialTheme.appColors.textPrimary,
+ style = MaterialTheme.appTypography.titleMedium
+ )
+ if (selected) {
+ Icon(
+ modifier = Modifier
+ .testTag("ic_language_selected_${name.tagId()}")
+ .size(20.dp),
+ painter = painterResource(id = R.drawable.core_ic_check),
+ tint = MaterialTheme.appColors.primary,
+ contentDescription = null
+ )
+ }
+ }
+}
+
+private data class LanguageOption(
+ val code: String,
+ val displayName: String
+)
+
+@Preview
+@Composable
+private fun LanguageScreenPreview() {
+ OpenEdXTheme {
+ LanguageScreen(
+ windowSize = WindowSize(WindowType.Compact, WindowType.Compact),
+ currentLanguage = "en",
+ onLanguageSelected = {},
+ onBackClick = {}
+ )
+ }
+}
diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsFragment.kt
index 217a35258..eaec377aa 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsFragment.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsFragment.kt
@@ -10,6 +10,7 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.openedx.core.data.storage.ThemeMode
import org.openedx.core.ui.theme.OpenEdXTheme
import org.openedx.foundation.presentation.rememberWindowSize
@@ -94,6 +95,22 @@ class SettingsFragment : Fragment() {
requireActivity().supportFragmentManager
)
}
+
+ SettingsScreenAction.ThemeSettingsClick -> {
+ viewModel.themeSettingsClicked(
+ requireActivity().supportFragmentManager
+ )
+ }
+
+ SettingsScreenAction.LanguageSettingsClick -> {
+ viewModel.languageSettingsClicked(
+ requireActivity().supportFragmentManager
+ )
+ }
+
+ is SettingsScreenAction.ThemeModeSelected -> {
+ viewModel.setThemeMode(action.mode)
+ }
}
}
)
@@ -103,6 +120,12 @@ class SettingsFragment : Fragment() {
viewModel.restartApp(requireActivity().supportFragmentManager)
}
}
+
+ LaunchedEffect(Unit) {
+ viewModel.themeChanged.collect {
+ requireActivity().recreate()
+ }
+ }
}
}
}
@@ -120,4 +143,7 @@ internal interface SettingsScreenAction {
object VideoSettingsClick : SettingsScreenAction
object ManageAccountClick : SettingsScreenAction
object CalendarSettingsClick : SettingsScreenAction
+ object ThemeSettingsClick : SettingsScreenAction
+ object LanguageSettingsClick : SettingsScreenAction
+ data class ThemeModeSelected(val mode: ThemeMode) : SettingsScreenAction
}
diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt
index 68c773745..4cc06b0e5 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt
@@ -28,6 +28,10 @@ import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.outlined.CalendarMonth
+import androidx.compose.material.icons.outlined.Language
+import androidx.compose.material.icons.outlined.Palette
+import androidx.compose.material.icons.outlined.SmartDisplay
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -96,8 +100,7 @@ internal fun SettingsScreen(
mutableStateOf(
windowSize.windowSizeValue(
expanded = Modifier.widthIn(Dp.Unspecified, 560.dp),
- compact = Modifier
- .fillMaxWidth()
+ compact = Modifier.fillMaxWidth()
)
)
}
@@ -181,6 +184,12 @@ internal fun SettingsScreen(
},
onCalendarSettingsClick = {
onAction(SettingsScreenAction.CalendarSettingsClick)
+ },
+ onThemeClick = {
+ onAction(SettingsScreenAction.ThemeSettingsClick)
+ },
+ onLanguageClick = {
+ onAction(SettingsScreenAction.LanguageSettingsClick)
}
)
@@ -212,7 +221,9 @@ internal fun SettingsScreen(
@Composable
private fun SettingsSection(
onVideoSettingsClick: () -> Unit,
- onCalendarSettingsClick: () -> Unit
+ onCalendarSettingsClick: () -> Unit,
+ onThemeClick: () -> Unit,
+ onLanguageClick: () -> Unit
) {
Column {
Text(
@@ -231,13 +242,27 @@ private fun SettingsSection(
Column(Modifier.fillMaxWidth()) {
SettingsItem(
text = stringResource(id = profileR.string.profile_video),
+ icon = Icons.Outlined.SmartDisplay,
onClick = onVideoSettingsClick
)
SettingsDivider()
SettingsItem(
text = stringResource(id = profileR.string.profile_dates_and_calendar),
+ icon = Icons.Outlined.CalendarMonth,
onClick = onCalendarSettingsClick
)
+ SettingsDivider()
+ SettingsItem(
+ text = stringResource(id = R.string.core_theme),
+ icon = Icons.Outlined.Palette,
+ onClick = onThemeClick
+ )
+ SettingsDivider()
+ SettingsItem(
+ text = stringResource(id = R.string.core_language),
+ icon = Icons.Outlined.Language,
+ onClick = onLanguageClick
+ )
}
}
}
diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsUIState.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsUIState.kt
index f2b736d45..981c1fc6d 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsUIState.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsUIState.kt
@@ -1,10 +1,13 @@
package org.openedx.profile.presentation.settings
import org.openedx.profile.domain.model.Configuration
+import org.openedx.core.data.storage.ThemeMode
sealed class SettingsUIState {
data class Data(
val configuration: Configuration,
+ val themeMode: ThemeMode = ThemeMode.SYSTEM,
+ val appLanguage: String = "",
) : SettingsUIState()
object Loading : SettingsUIState()
diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt
index 59548d1c9..b7d62eddc 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt
@@ -17,6 +17,8 @@ import org.openedx.core.AppUpdateState
import org.openedx.core.CalendarRouter
import org.openedx.core.R
import org.openedx.core.config.Config
+import org.openedx.core.data.storage.CorePreferences
+import org.openedx.core.data.storage.ThemeMode
import org.openedx.core.module.DownloadWorkerController
import org.openedx.core.presentation.global.AppData
import org.openedx.core.system.AppCookieManager
@@ -49,9 +51,16 @@ class SettingsViewModel(
private val calendarRouter: CalendarRouter,
private val appNotifier: AppNotifier,
private val profileNotifier: ProfileNotifier,
+ private val corePreferences: CorePreferences,
) : BaseViewModel() {
- private val _uiState: MutableStateFlow = MutableStateFlow(SettingsUIState.Data(configuration))
+ private val _uiState: MutableStateFlow = MutableStateFlow(
+ SettingsUIState.Data(
+ configuration,
+ themeMode = corePreferences.themeMode,
+ appLanguage = corePreferences.appLanguage
+ )
+ )
internal val uiState: StateFlow = _uiState.asStateFlow()
private val _successLogout = MutableSharedFlow()
@@ -66,6 +75,12 @@ class SettingsViewModel(
val appUpgradeEvent: StateFlow
get() = _appUpgradeEvent.asStateFlow()
+ private val _themeChanged = MutableSharedFlow()
+ val themeChanged = _themeChanged.asSharedFlow()
+
+ private val _languageChanged = MutableSharedFlow()
+ val languageChanged = _languageChanged.asSharedFlow()
+
val isLogistrationEnabled get() = config.isPreLoginExperienceEnabled()
private val configuration
@@ -203,6 +218,25 @@ class SettingsViewModel(
calendarRouter.navigateToCalendarSettings(fragmentManager)
}
+ fun themeSettingsClicked(fragmentManager: FragmentManager) {
+ profileRouter.navigateToThemeSettings(fragmentManager)
+ }
+
+ fun languageSettingsClicked(fragmentManager: FragmentManager) {
+ profileRouter.navigateToLanguageSettings(fragmentManager)
+ }
+
+ fun setAppLanguage(languageCode: String) {
+ corePreferences.appLanguage = languageCode
+ viewModelScope.launch {
+ val currentState = _uiState.value
+ if (currentState is SettingsUIState.Data) {
+ _uiState.emit(currentState.copy(appLanguage = languageCode))
+ }
+ _languageChanged.emit(Unit)
+ }
+ }
+
fun restartApp(fragmentManager: FragmentManager) {
profileRouter.restartApp(
fragmentManager,
@@ -210,6 +244,22 @@ class SettingsViewModel(
)
}
+ fun setThemeMode(mode: ThemeMode) {
+ viewModelScope.launch {
+ try {
+ corePreferences.themeMode = mode
+ _uiState.emit(
+ SettingsUIState.Data(
+ configuration = configuration,
+ themeMode = mode
+ )
+ )
+ _themeChanged.emit(Unit)
+ } catch (_: Exception) {
+ }
+ }
+ }
+
private fun logProfileEvent(
event: ProfileAnalyticsEvent,
params: Map = emptyMap(),
diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/ThemeFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/ThemeFragment.kt
new file mode 100644
index 000000000..26ec0a0e3
--- /dev/null
+++ b/profile/src/main/java/org/openedx/profile/presentation/settings/ThemeFragment.kt
@@ -0,0 +1,265 @@
+package org.openedx.profile.presentation.settings
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Card
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+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.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.fragment.app.Fragment
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.openedx.core.R
+import org.openedx.core.data.storage.ThemeMode
+import org.openedx.core.ui.Toolbar
+import org.openedx.core.ui.displayCutoutForLandscape
+import org.openedx.core.ui.settingsHeaderBackground
+import org.openedx.core.ui.statusBarsInset
+import org.openedx.core.ui.theme.OpenEdXTheme
+import org.openedx.core.ui.theme.appColors
+import org.openedx.core.ui.theme.appShapes
+import org.openedx.core.ui.theme.appTypography
+import org.openedx.foundation.extension.tagId
+import org.openedx.foundation.presentation.WindowSize
+import org.openedx.foundation.presentation.WindowType
+import org.openedx.foundation.presentation.rememberWindowSize
+import org.openedx.foundation.presentation.windowSizeValue
+import org.openedx.profile.presentation.ui.SettingsDivider
+
+class ThemeFragment : Fragment() {
+
+ private val viewModel by viewModel()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ) = ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ OpenEdXTheme {
+ val windowSize = rememberWindowSize()
+ val uiState by viewModel.uiState.collectAsState()
+
+ val currentTheme = (uiState as? SettingsUIState.Data)?.themeMode ?: ThemeMode.SYSTEM
+
+ ThemeScreen(
+ windowSize = windowSize,
+ currentTheme = currentTheme,
+ onThemeSelected = { mode ->
+ viewModel.setThemeMode(mode)
+ requireActivity().supportFragmentManager.popBackStack()
+ },
+ onBackClick = {
+ requireActivity().supportFragmentManager.popBackStack()
+ }
+ )
+
+ LaunchedEffect(Unit) {
+ viewModel.themeChanged.collect {
+ requireActivity().recreate()
+ }
+ }
+ }
+ }
+ }
+
+ companion object {
+ fun newInstance(): ThemeFragment {
+ return ThemeFragment()
+ }
+ }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun ThemeScreen(
+ windowSize: WindowSize,
+ currentTheme: ThemeMode,
+ onThemeSelected: (ThemeMode) -> Unit,
+ onBackClick: () -> Unit
+) {
+
+ val topBarWidth by remember(key1 = windowSize) {
+ mutableStateOf(
+ windowSize.windowSizeValue(
+ expanded = Modifier.widthIn(Dp.Unspecified, 560.dp),
+ compact = Modifier.fillMaxWidth()
+ )
+ )
+ }
+
+ val contentWidth by remember(key1 = windowSize) {
+ mutableStateOf(
+ windowSize.windowSizeValue(
+ expanded = Modifier.widthIn(Dp.Unspecified, 420.dp),
+ compact = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp)
+ )
+ )
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .settingsHeaderBackground()
+ .statusBarsInset()
+ ) {
+ Toolbar(
+ modifier = topBarWidth
+ .align(Alignment.CenterHorizontally)
+ .displayCutoutForLandscape(),
+ label = stringResource(id = R.string.core_theme),
+ canShowBackBtn = true,
+ labelTint = MaterialTheme.appColors.settingsTitleContent,
+ iconTint = MaterialTheme.appColors.settingsTitleContent,
+ onBackClick = onBackClick
+ )
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ shape = MaterialTheme.appShapes.screenBackgroundShape,
+ color = MaterialTheme.appColors.background
+ ) {
+ Box(
+ contentAlignment = Alignment.TopCenter
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .displayCutoutForLandscape(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxHeight()
+ .then(contentWidth)
+ .verticalScroll(rememberScrollState()),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(Modifier.height(30.dp))
+
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = MaterialTheme.appShapes.cardShape,
+ elevation = 0.dp,
+ backgroundColor = MaterialTheme.appColors.cardViewBackground
+ ) {
+ Column(Modifier.fillMaxWidth()) {
+ val themeOptions = listOf(
+ ThemeMode.SYSTEM to R.string.core_theme_system,
+ ThemeMode.LIGHT to R.string.core_theme_light,
+ ThemeMode.DARK to R.string.core_theme_dark
+ )
+
+ themeOptions.forEachIndexed { index, (mode, stringRes) ->
+ ThemeOption(
+ title = stringResource(id = stringRes),
+ selected = currentTheme == mode,
+ onClick = {
+ onThemeSelected(mode)
+ }
+ )
+
+ if (index < themeOptions.size - 1) {
+ SettingsDivider()
+ }
+ }
+ }
+ }
+
+ Spacer(Modifier.height(24.dp))
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun ThemeOption(
+ title: String,
+ selected: Boolean,
+ onClick: () -> Unit
+) {
+ Row(
+ Modifier
+ .testTag("btn_theme_${title.tagId()}")
+ .fillMaxWidth()
+ .clickable {
+ onClick()
+ }
+ .padding(horizontal = 20.dp, vertical = 24.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ modifier = Modifier.testTag("txt_theme_${title.tagId()}"),
+ text = title,
+ color = MaterialTheme.appColors.textPrimary,
+ style = MaterialTheme.appTypography.titleMedium
+ )
+ if (selected) {
+ Icon(
+ modifier = Modifier
+ .testTag("ic_theme_selected_${title.tagId()}")
+ .size(20.dp),
+ painter = painterResource(id = R.drawable.core_ic_check),
+ tint = MaterialTheme.appColors.primary,
+ contentDescription = null
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun ThemeScreenPreview() {
+ OpenEdXTheme {
+ ThemeScreen(
+ windowSize = WindowSize(WindowType.Compact, WindowType.Compact),
+ currentTheme = ThemeMode.SYSTEM,
+ onThemeSelected = {},
+ onBackClick = {}
+ )
+ }
+}
+
diff --git a/profile/src/main/java/org/openedx/profile/presentation/ui/SettingsUI.kt b/profile/src/main/java/org/openedx/profile/presentation/ui/SettingsUI.kt
index f4811135a..c15154c74 100644
--- a/profile/src/main/java/org/openedx/profile/presentation/ui/SettingsUI.kt
+++ b/profile/src/main/java/org/openedx/profile/presentation/ui/SettingsUI.kt
@@ -3,9 +3,11 @@ package org.openedx.profile.presentation.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
@@ -16,6 +18,7 @@ import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -26,10 +29,11 @@ import org.openedx.foundation.extension.tagId
@Composable
fun SettingsItem(
text: String,
+ icon: ImageVector? = null,
external: Boolean = false,
onClick: () -> Unit
) {
- val icon = if (external) {
+ val trailingIcon = if (external) {
Icons.AutoMirrored.Filled.OpenInNew
} else {
Icons.AutoMirrored.Filled.ArrowForwardIos
@@ -46,20 +50,33 @@ fun SettingsItem(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
- Text(
- modifier = Modifier
- .testTag("txt_${text.tagId()}")
- .weight(1f),
- text = text,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- style = MaterialTheme.appTypography.titleMedium,
- color = MaterialTheme.appColors.textPrimary
- )
+ Row(
+ modifier = Modifier.weight(1f),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (icon != null) {
+ Icon(
+ modifier = Modifier.size(20.dp),
+ imageVector = icon,
+ contentDescription = null,
+ tint = MaterialTheme.appColors.textPrimary
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ }
+ Text(
+ modifier = Modifier.testTag("txt_${text.tagId()}"),
+ text = text,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.appTypography.titleMedium,
+ color = MaterialTheme.appColors.textPrimary
+ )
+ }
Icon(
modifier = Modifier.size(16.dp),
- imageVector = icon,
- contentDescription = null
+ imageVector = trailingIcon,
+ contentDescription = null,
+ tint = MaterialTheme.appColors.textPrimary
)
}
}