Migrated to Themmo and added new screen for themes.

This commit is contained in:
Sad Ellie 2022-07-20 22:17:59 +03:00
parent 4ecb29d7b5
commit 183c5d47a6
18 changed files with 371 additions and 396 deletions

View File

@ -162,4 +162,7 @@ dependencies {
// Retrofit with Moshi Converter
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
// Themmo
implementation("com.github.sadellie:themmo:0.0.2")
}

View File

@ -22,17 +22,21 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.animation.core.tween
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.sadellie.unitto.data.NavRoutes.ABOUT_SCREEN
import com.sadellie.unitto.data.NavRoutes.LEFT_LIST_SCREEN
import com.sadellie.unitto.data.NavRoutes.MAIN_SCREEN
import com.sadellie.unitto.data.NavRoutes.RIGHT_LIST_SCREEN
import com.sadellie.unitto.data.NavRoutes.SETTINGS_SCREEN
import com.sadellie.unitto.data.preferences.AppTheme
import com.sadellie.unitto.data.NavRoutes.THEMES_SCREEN
import com.sadellie.unitto.screens.MainViewModel
import com.sadellie.unitto.screens.about.AboutScreen
import com.sadellie.unitto.screens.main.MainScreen
@ -40,34 +44,58 @@ import com.sadellie.unitto.screens.second.LeftSideScreen
import com.sadellie.unitto.screens.second.RightSideScreen
import com.sadellie.unitto.screens.second.SecondViewModel
import com.sadellie.unitto.screens.setttings.SettingsScreen
import com.sadellie.unitto.ui.theme.UnittoTheme
import com.sadellie.unitto.screens.theming.ThemesScreen
import com.sadellie.unitto.screens.theming.ThemesViewModel
import com.sadellie.unitto.ui.theme.AppTypography
import com.sadellie.unitto.ui.theme.DarkThemeColors
import com.sadellie.unitto.ui.theme.LightThemeColors
import dagger.hilt.android.AndroidEntryPoint
import io.github.sadellie.themmo.Themmo
import io.github.sadellie.themmo.ThemmoController
import io.github.sadellie.themmo.rememberThemmoController
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val mainViewModel: MainViewModel by viewModels()
private val secondViewModel: SecondViewModel by viewModels()
private val themesViewModel: ThemesViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
val currentAppTheme: Int = mainViewModel.currentTheme
themesViewModel.collectThemeOptions()
// We don't draw anything until we know what theme we need to use
if (currentAppTheme != AppTheme.NOT_SET) {
UnittoTheme(
currentAppTheme = currentAppTheme
) {
UnittoApp(
navController = navController,
mainViewModel = mainViewModel,
secondViewModel = secondViewModel
)
}
val themmoController = rememberThemmoController(
lightColorScheme = LightThemeColors,
darkColorScheme = DarkThemeColors,
// Anything below will not called if theming mode is still loading from DataStore
themingMode = themesViewModel.themingMode ?: return@setContent,
dynamicThemeEnabled = themesViewModel.enableDynamic,
amoledThemeEnabled = themesViewModel.enableAmoled
)
val navController = rememberNavController()
val sysUiController = rememberSystemUiController()
Themmo(
themmoController = themmoController,
typography = AppTypography,
animationSpec = tween(150)
) {
val backgroundColor = MaterialTheme.colorScheme.background
UnittoApp(
navController = navController,
mainViewModel = mainViewModel,
secondViewModel = secondViewModel,
themesViewModel = themesViewModel,
themmoController = it
)
SideEffect { sysUiController.setSystemBarsColor(backgroundColor) }
}
}
}
@ -81,7 +109,9 @@ class MainActivity : ComponentActivity() {
fun UnittoApp(
navController: NavHostController,
mainViewModel: MainViewModel,
secondViewModel: SecondViewModel
secondViewModel: SecondViewModel,
themesViewModel: ThemesViewModel,
themmoController: ThemmoController
) {
NavHost(
navController = navController,
@ -123,6 +153,14 @@ fun UnittoApp(
)
}
composable(THEMES_SCREEN) {
ThemesScreen(
navigateUpAction = { navController.navigateUp() },
themmoController = themmoController,
viewModel = themesViewModel
)
}
composable(ABOUT_SCREEN) {
AboutScreen(navigateUpAction = { navController.navigateUp() })
}

View File

@ -28,5 +28,6 @@ object NavRoutes {
const val RIGHT_LIST_SCREEN = "RightScreen"
const val SETTINGS_SCREEN = "SettingsScreen"
const val THEMES_SCREEN = "ThemesScreen"
const val ABOUT_SCREEN = "AboutScreen"
}

View File

@ -1,62 +0,0 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2022-2022 Elshan Agaev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.data.preferences
import android.os.Build
import com.sadellie.unitto.R
/**
* All possible state of theme in the app
*/
object AppTheme {
// Used on app launch when we don't know which theme to use
const val NOT_SET = 0
const val AUTO = 1
const val LIGHT = 2
const val DARK = 3
const val LIGHT_DYNAMIC = 4
const val DARK_DYNAMIC = 5
const val AMOLED = 6
}
/**
* Device specific map of available themes. Used in settings
*/
val APP_THEMES: Map<Int, Int> by lazy {
// Dynamic themes are only for Android 8.1 and later
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
mapOf(
AppTheme.AUTO to R.string.force_auto_mode,
AppTheme.LIGHT to R.string.force_light_mode,
AppTheme.DARK to R.string.force_dark_mode,
AppTheme.AMOLED to R.string.force_amoled_mode,
AppTheme.LIGHT_DYNAMIC to R.string.force_light_dynamic_mode,
AppTheme.DARK_DYNAMIC to R.string.force_dark_dynamic_mode,
)
} else {
mapOf(
AppTheme.AUTO to R.string.force_auto_mode,
AppTheme.LIGHT to R.string.force_light_mode,
AppTheme.DARK to R.string.force_dark_mode,
AppTheme.AMOLED to R.string.force_amoled_mode,
)
}
}

View File

@ -22,19 +22,24 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import com.sadellie.unitto.data.units.AbstractUnit
import com.sadellie.unitto.data.units.MyUnitIDS
import io.github.sadellie.themmo.ThemingMode
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import okio.IOException
import javax.inject.Inject
/**
* Represents user preferences that are user across the app
*
* @property currentAppTheme Current [AppTheme] to be used
* @property themingMode [ThemingMode] from Themmo
* @property enableDynamicTheme Use dynamic color scheme
* @property enableAmoledTheme Use amoled color scheme
* @property digitsPrecision Current [PRECISIONS]. Number of digits in fractional part
* @property separator Current [Separator] that used to separate thousands
* @property outputFormat Current [OutputFormat] that is applied to converted value (not input)
@ -43,7 +48,9 @@ import javax.inject.Inject
* @property enableAnalytics Whether or not user wants to share application usage data
*/
data class UserPreferences(
val currentAppTheme: Int,
val themingMode: ThemingMode,
val enableDynamicTheme: Boolean,
val enableAmoledTheme: Boolean,
val digitsPrecision: Int,
val separator: Int,
val outputFormat: Int,
@ -61,7 +68,9 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
* Keys for DataStore
*/
private object PrefsKeys {
val CURRENT_APP_THEME = intPreferencesKey("CURRENT_APP_THEME")
val THEMING_MODE = stringPreferencesKey("THEMING_MODE_PREF_KEY")
val ENABLE_DYNAMIC_THEME = booleanPreferencesKey("ENABLE_DYNAMIC_THEME_PREF_KEY")
val ENABLE_AMOLED_THEME = booleanPreferencesKey("ENABLE_AMOLED_THEME_PREF_KEY")
val DIGITS_PRECISION = intPreferencesKey("DIGITS_PRECISION_PREF_KEY")
val SEPARATOR = intPreferencesKey("SEPARATOR_PREF_KEY")
val OUTPUT_FORMAT = intPreferencesKey("OUTPUT_FORMAT_PREF_KEY")
@ -71,9 +80,21 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
}
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val currentAppTheme: Int =
preferences[PrefsKeys.CURRENT_APP_THEME] ?: AppTheme.AUTO
val themingMode: ThemingMode =
preferences[PrefsKeys.THEMING_MODE]?.let { ThemingMode.valueOf(it) }
?: ThemingMode.AUTO
val enableDynamicTheme: Boolean =
preferences[PrefsKeys.ENABLE_DYNAMIC_THEME] ?: false
val enableAmoledTheme: Boolean =
preferences[PrefsKeys.ENABLE_AMOLED_THEME] ?: false
val digitsPrecision: Int =
preferences[PrefsKeys.DIGITS_PRECISION] ?: 3
val separator: Int =
@ -88,7 +109,9 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
preferences[PrefsKeys.ENABLE_ANALYTICS] ?: true
UserPreferences(
currentAppTheme = currentAppTheme,
themingMode = themingMode,
enableDynamicTheme = enableDynamicTheme,
enableAmoledTheme = enableAmoledTheme,
digitsPrecision = digitsPrecision,
separator = separator,
outputFormat = outputFormat,
@ -98,17 +121,6 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
)
}
/**
* Update current theme preference in DataStore
*
* @param appTheme [AppTheme] to change to
*/
suspend fun updateCurrentAppTheme(appTheme: Int) {
dataStore.edit { preferences ->
preferences[PrefsKeys.CURRENT_APP_THEME] = appTheme
}
}
/**
* Update precision preference in DataStore
*
@ -166,4 +178,37 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
preferences[PrefsKeys.LATEST_RIGHT_SIDE] = rightSideUnit.unitId
}
}
/**
* Update [ThemingMode]. Saves value as a string.
*
* @param themingMode [ThemingMode] to save.
*/
suspend fun updateThemingMode(themingMode: ThemingMode) {
dataStore.edit { preferences ->
preferences[PrefsKeys.THEMING_MODE] = themingMode.name
}
}
/**
* Update preference on whether or not generate color scheme from device wallpaper.
*
* @param enabled True if user wants to enable this feature.
*/
suspend fun updateDynamicTheme(enabled: Boolean) {
dataStore.edit { preferences ->
preferences[PrefsKeys.ENABLE_DYNAMIC_THEME] = enabled
}
}
/**
* Update preference on whether or not use true black colors.
*
* @param enabled True if user wants to enable this feature.
*/
suspend fun updateAmoledTheme(enabled: Boolean) {
dataStore.edit { preferences ->
preferences[PrefsKeys.ENABLE_AMOLED_THEME] = enabled
}
}
}

View File

@ -30,7 +30,6 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.sadellie.unitto.data.KEY_0
import com.sadellie.unitto.data.KEY_DOT
import com.sadellie.unitto.data.KEY_MINUS
import com.sadellie.unitto.data.preferences.AppTheme
import com.sadellie.unitto.data.preferences.MAX_PRECISION
import com.sadellie.unitto.data.preferences.OutputFormat
import com.sadellie.unitto.data.preferences.Separator
@ -59,8 +58,6 @@ class MainViewModel @Inject constructor(
private val allUnitsRepository: AllUnitsRepository
) : ViewModel() {
var currentTheme: Int by mutableStateOf(AppTheme.NOT_SET)
private set
var precision: Int by mutableStateOf(3)
private set
var separator: Int by mutableStateOf(Separator.SPACES)
@ -70,15 +67,6 @@ class MainViewModel @Inject constructor(
var enableAnalytics: Boolean by mutableStateOf(false)
private set
/**
* See [UserPreferencesRepository.updateCurrentAppTheme]
*/
fun updateCurrentAppTheme(appTheme: Int) {
viewModelScope.launch {
userPrefsRepository.updateCurrentAppTheme(appTheme)
}
}
/**
* See [UserPreferencesRepository.updateDigitsPrecision]
*/
@ -407,13 +395,11 @@ class MainViewModel @Inject constructor(
*
* Any change in user preferences will update mutableStateOf so composables, that rely on this
* values recompose when actually needed. For example, when you change output format, composable
* in MainActivity will not be recomposed even though it needs currentTheme which is also in
* user preferences/
* in MainActivity will not be recomposed.
*/
private fun observePreferenceChanges() {
viewModelScope.launch {
userPrefsRepository.userPreferencesFlow.collect { userPref ->
currentTheme = userPref.currentAppTheme
precision = userPref.digitsPrecision
separator = userPref.separator.also { Formatter.setSeparator(it) }
outputFormat = userPref.outputFormat

View File

@ -18,7 +18,6 @@
package com.sadellie.unitto.screens.setttings
import android.os.Build
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -31,7 +30,6 @@ import com.sadellie.unitto.BuildConfig
import com.sadellie.unitto.R
import com.sadellie.unitto.data.NavRoutes.ABOUT_SCREEN
import com.sadellie.unitto.data.NavRoutes.THEMES_SCREEN
import com.sadellie.unitto.data.preferences.APP_THEMES
import com.sadellie.unitto.data.preferences.OUTPUT_FORMAT
import com.sadellie.unitto.data.preferences.PRECISIONS
import com.sadellie.unitto.data.preferences.SEPARATORS
@ -91,7 +89,7 @@ fun SettingsScreen(
SettingsListItem(
stringResource(R.string.theme_setting),
stringResource(R.string.theme_setting_support)
) { dialogState = DialogState.THEME }
) { navControllerAction(THEMES_SCREEN) }
}
// CURRENCY RATE NOTE
@ -192,19 +190,6 @@ fun SettingsScreen(
supportText = stringResource(id = R.string.output_format_setting_info)
)
}
DialogState.THEME -> {
AlertDialogWithList(
title = stringResource(id = R.string.theme_setting),
listItems = APP_THEMES,
selectedItemIndex = mainViewModel.currentTheme,
selectAction = { mainViewModel.updateCurrentAppTheme(it) },
dismissAction = { resetDialog() },
// Show note for users with devices that support custom Dynamic theming
supportText = if (Build.VERSION.SDK_INT in (Build.VERSION_CODES.O_MR1..Build.VERSION_CODES.R)) stringResource(
id = R.string.theme_setting_info
) else null
)
}
DialogState.CURRENCY_RATE -> {
AlertDialogWithList(
title = stringResource(id = R.string.currency_rates_note_title),
@ -222,5 +207,5 @@ fun SettingsScreen(
* All possible states for alert dialog that opens when user clicks on settings.
*/
private enum class DialogState {
NONE, PRECISION, SEPARATOR, OUTPUT_FORMAT, THEME, CURRENCY_RATE,
NONE, PRECISION, SEPARATOR, OUTPUT_FORMAT, CURRENCY_RATE,
}

View File

@ -25,12 +25,21 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
@ -121,3 +130,59 @@ fun SettingsListItem(
) = BasicSettingsListItem(label, supportText, { onSwitchChange(switchState) }) {
Switch(checked = switchState, onCheckedChange = { onSwitchChange(!it) })
}
/**
* Represents one item in list on Settings screen with drop-down menu.
*
* @param label Main text.
* @param supportText Text that is located below label.
* @param allOptions Options in drop-down menu.
* @param selected Selected option.
* @param onSelectedChange Action to perform when drop-down menu item is selected.
*/
@Composable
fun <T> SettingsListItem(
label: String,
supportText: String? = null,
allOptions: Collection<T>,
selected: T,
onSelectedChange: (T) -> Unit
) = BasicSettingsListItem(label, supportText, {}) {
var dropDownExpanded by rememberSaveable { mutableStateOf(false) }
var currentOption by rememberSaveable { mutableStateOf(selected) }
ExposedDropdownMenuBox(
modifier = Modifier,
expanded = dropDownExpanded,
onExpandedChange = { dropDownExpanded = it }
) {
OutlinedTextField(
modifier = Modifier.widthIn(1.dp),
value = currentOption.toString(),
onValueChange = {},
readOnly = true,
singleLine = true,
enabled = false,
colors = TextFieldDefaults.outlinedTextFieldColors(
disabledBorderColor = MaterialTheme.colorScheme.outline,
disabledTextColor = MaterialTheme.colorScheme.onSurface,
)
)
ExposedDropdownMenu(
modifier = Modifier.exposedDropdownSize(),
expanded = dropDownExpanded,
onDismissRequest = { dropDownExpanded = false }
) {
allOptions.forEach {
DropdownMenuItem(
text = { Text(it.toString()) },
onClick = {
currentOption = it
onSelectedChange(it)
dropDownExpanded = false
}
)
}
}
}
}

View File

@ -0,0 +1,89 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2022 Elshan Agaev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.screens.theming
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.sadellie.unitto.R
import com.sadellie.unitto.screens.common.UnittoLargeTopAppBar
import com.sadellie.unitto.screens.setttings.components.SettingsListItem
import io.github.sadellie.themmo.ThemingMode
import io.github.sadellie.themmo.ThemmoController
@Composable
fun ThemesScreen(
navigateUpAction: () -> Unit = {},
themmoController: ThemmoController,
viewModel: ThemesViewModel
) {
UnittoLargeTopAppBar(
title = stringResource(id = R.string.theme_setting),
navigateUpAction = navigateUpAction
) { paddingValues ->
LazyColumn(contentPadding = paddingValues) {
item {
SettingsListItem(
label = stringResource(R.string.theme_setting),
allOptions = ThemingMode.values().toList(),
selected = themmoController.currentThemingMode,
onSelectedChange = {
themmoController.setThemingMode(it)
viewModel.updateThemingMode(it)
}
)
}
item {
SettingsListItem(
label = stringResource(R.string.enable_dynamic_colors),
supportText = stringResource(R.string.enable_dynamic_colors_support),
switchState = themmoController.isDynamicThemeEnabled,
onSwitchChange = {
themmoController.enableDynamicTheme(!it)
viewModel.updateDynamicTheme(!it)
}
)
}
item {
AnimatedVisibility(
visible = (themmoController.currentThemingMode != ThemingMode.FORCE_LIGHT),
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut(),
) {
SettingsListItem(
label = stringResource(R.string.force_amoled_mode),
supportText = stringResource(R.string.force_amoled_mode_support),
switchState = themmoController.isAmoledThemeEnabled,
onSwitchChange = {
themmoController.enableAmoledTheme(!it)
viewModel.updateAmoledTheme(!it)
}
)
}
}
}
}
}

View File

@ -0,0 +1,80 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2022 Elshan Agaev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.screens.theming
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sadellie.unitto.data.preferences.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.sadellie.themmo.ThemingMode
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ThemesViewModel @Inject constructor(
private val userPrefsRepository: UserPreferencesRepository
) : ViewModel() {
/**
* @see [UserPreferencesRepository.updateThemingMode]
*/
fun updateThemingMode(themingMode: ThemingMode) {
viewModelScope.launch {
userPrefsRepository.updateThemingMode(themingMode)
}
}
/**
* @see [UserPreferencesRepository.updateDynamicTheme]
*/
fun updateDynamicTheme(enabled: Boolean) {
viewModelScope.launch {
userPrefsRepository.updateDynamicTheme(enabled)
}
}
/**
* @see [UserPreferencesRepository.updateAmoledTheme]
*/
fun updateAmoledTheme(enabled: Boolean) {
viewModelScope.launch {
userPrefsRepository.updateAmoledTheme(enabled)
}
}
var themingMode: ThemingMode? by mutableStateOf(null)
var enableDynamic by mutableStateOf(false)
var enableAmoled by mutableStateOf(false)
/**
* Collect saved theming options. Used on app launch.
*/
fun collectThemeOptions() {
viewModelScope.launch {
val userPref = userPrefsRepository.userPreferencesFlow.first()
themingMode = userPref.themingMode
enableDynamic = userPref.enableDynamicTheme
enableAmoled = userPref.enableAmoledTheme
}
}
}

View File

@ -73,5 +73,3 @@ val md_theme_dark_onSurfaceVariant = Color(0xFFc1c9be)
val md_theme_dark_outline = Color(0xFF8b9389)
val md_theme_dark_inverseOnSurface = Color(0xFF1a1c19)
val md_theme_dark_inverseSurface = Color(0xFFe1e3dd)
val md_theme_amoled_black = Color(0xFF000000)

View File

@ -1,180 +0,0 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2022-2022 Elshan Agaev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.ui.theme
import android.app.WallpaperManager
import android.content.Context
import android.os.Build
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
/**
* Shifts colors to make them brighter
*/
@Stable
private fun Color.shiftTo255(ratio: Float): Color {
return this
.copy(
alpha,
red + (1.0f - red) * ratio,
green + (1.0f - green) * ratio,
blue + (1.0f - blue) * ratio,
)
}
/**
* Shifts colors to make them darker
*/
@Stable
private fun Color.shiftTo0(ratio: Float): Color {
return this
.copy(
alpha,
red * (1.0f - ratio),
green * (1.0f - ratio),
blue * (1.0f - ratio),
)
}
/**
* Decides which colors fits the best for the color that is used as a background
*/
@Stable
private fun Color.getAppropriateTextColor(): Color {
return if (luminance() > 0.5) Color.Black else Color.White
}
/**
* Function to return dynamic light theme. Determines which API to use based on Android version
*/
fun dynamicLightTheme(context: Context): ColorScheme {
return when {
// Android 12+ devices use new api
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> dynamicLightColorScheme(context)
// Dynamic theming for devices with Android 8.1 up to Android 11
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> {
// Wallpaper colors can be null. We return default theme for such cases
val wallpaperColors = WallpaperManager.getInstance(context)
.getWallpaperColors(WallpaperManager.FLAG_SYSTEM)
?: return LightThemeColors
val primary = Color(
red = wallpaperColors.primaryColor.red(),
green = wallpaperColors.primaryColor.green(),
blue = wallpaperColors.primaryColor.blue()
)
// Secondary color can be null. For such cases we just generate it from primary
val secondary: Color = primary.shiftTo255(0.5f)
val background = primary.shiftTo255(0.9f)
val onBackground = background.getAppropriateTextColor()
return lightColorScheme(
// Settings screen group text, units screen units group text
primary = primary,
// Switch thumb color
onPrimary = primary.getAppropriateTextColor(),
onPrimaryContainer = primary.shiftTo0(0.7f),
// Selected unit, group, keyboard buttons
secondaryContainer = secondary,
onSecondaryContainer = secondary.getAppropriateTextColor(),
// Background color for all screens
background = background,
// Main screen input/output text color
onBackground = onBackground,
// Collapsable top bar background
surface = background,
// Main screen Unitto text, disabled buttons
// Settings screen title and back icon, dialog window title, not selected radio button color
// Third party licenses screen title and back icon
onSurface = onBackground,
surfaceVariant = primary.shiftTo255(0.5f),
// Main Screen settings icon, Not selected chips text, short unit names
// Settings items secondary text
onSurfaceVariant = primary.shiftTo0(0.80f),
// Chips outline and cards outline on Third party licenses screen
outline = primary.shiftTo0(0.8f),
)
}
// Just in case
else -> LightThemeColors
}
}
/**
* Function to return dynamic light theme. Determines which API to use based on Android version
*/
fun dynamicDarkTheme(context: Context): ColorScheme {
return when {
// Android 12+ devices use new api
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> dynamicDarkColorScheme(context)
// Dynamic theming for devices with Android 8.1 up to Android 11
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> {
// Wallpaper colors can be null. We return default theme for such cases
val wallpaperColors = WallpaperManager.getInstance(context)
.getWallpaperColors(WallpaperManager.FLAG_SYSTEM)
?: return DarkThemeColors
val primary = Color(
red = wallpaperColors.primaryColor.red(),
green = wallpaperColors.primaryColor.green(),
blue = wallpaperColors.primaryColor.blue()
)
// Secondary color can be null. For such cases we just generate it from primary
val secondary: Color = primary.shiftTo0(0.7f)
val background = primary.shiftTo0(0.9f)
val onBackground = background.getAppropriateTextColor()
return darkColorScheme(
// Settings screen group text, units screen units group text
primary = primary,
// Switch thumb color
onPrimary = primary.getAppropriateTextColor(),
onPrimaryContainer = primary.shiftTo255(0.7f),
// Selected unit, group, keyboard buttons
secondaryContainer = secondary,
onSecondaryContainer = secondary.getAppropriateTextColor(),
// Background color for all screens
background = background,
// Main screen input/output text color
onBackground = onBackground,
// Collapsable top bar background
surface = background,
// Main screen Unitto text, disabled buttons
// Settings screen title and back icon, dialog window title, not selected radio button color
// Third party licenses screen title and back icon
onSurface = onBackground,
surfaceVariant = primary.shiftTo0(0.5f),
// Main Screen settings icon, Not selected chips text, short unit names
// Settings items secondary text
onSurfaceVariant = primary.shiftTo255(0.80f),
// Chips outline and cards outline on Third party licenses screen
outline = primary.shiftTo255(0.8f),
)
}
// Just in case
else -> DarkThemeColors
}
}

View File

@ -80,10 +80,3 @@ val DarkThemeColors by lazy {
inverseSurface = md_theme_dark_inverseSurface,
)
}
val AmoledThemeColors by lazy {
DarkThemeColors.copy(
background = md_theme_amoled_black,
surface = md_theme_amoled_black,
)
}

View File

@ -1,70 +0,0 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2022-2022 Elshan Agaev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.platform.LocalContext
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.sadellie.unitto.data.preferences.AppTheme
@Composable
fun UnittoTheme(
currentAppTheme: Int,
content: @Composable () -> Unit
) {
// Dynamic color is only for Android 12 and higher
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1
val sysUiController = rememberSystemUiController()
val colors = when (currentAppTheme) {
AppTheme.DARK -> DarkThemeColors
AppTheme.LIGHT -> LightThemeColors
AppTheme.AMOLED -> AmoledThemeColors
AppTheme.LIGHT_DYNAMIC -> dynamicLightTheme(LocalContext.current)
AppTheme.DARK_DYNAMIC -> dynamicDarkTheme(LocalContext.current)
// When it is set to Auto
else -> {
when {
dynamicColor and !isSystemInDarkTheme() -> dynamicLightTheme(LocalContext.current)
dynamicColor and isSystemInDarkTheme() -> dynamicDarkTheme(LocalContext.current)
!dynamicColor and !isSystemInDarkTheme() -> LightThemeColors
!dynamicColor and isSystemInDarkTheme() -> DarkThemeColors
// This case is here just in case, not actually used
else -> LightThemeColors
}
}
}
MaterialTheme(
colorScheme = colors,
typography = AppTypography,
content = content
)
SideEffect {
sysUiController.setSystemBarsColor(
color = colors.background
)
}
}

View File

@ -675,7 +675,7 @@
<string name="settings_screen">Settings</string>
<!--Settings items-->
<string name="theme_setting">Theme</string>
<string name="theme_setting">Themes</string>
<string name="precision_setting">Precision</string>
<string name="separator_setting">Separator</string>
<string name="output_format_setting">Output format</string>
@ -717,8 +717,6 @@
<string name="force_light_mode">Light</string>
<string name="force_dark_mode">Dark</string>
<string name="force_amoled_mode">AMOLED Dark</string>
<string name="force_light_dynamic_mode">Dynamic light</string>
<string name="force_dark_dynamic_mode">Dynamic dark</string>
<!--MISC.-->
<string name="loading_label">Loading…</string>
@ -738,5 +736,8 @@
<string name="clear_input_description">Clear input</string>
<string name="favorite_button_description">Add or remove unit from favorites</string>
<string name="empty_search_result_description">Empty search result</string>
<string name="enable_dynamic_colors">Dynamic colors</string>
<string name="enable_dynamic_colors_support">Generate theme from your current wallpaper</string>
<string name="force_amoled_mode_support">Use black background for dark themes</string>
</resources>

View File

@ -492,7 +492,7 @@
<string name="units_screen_from">Перевести из</string>
<string name="units_screen_to">Перевести в</string>
<string name="settings_screen">Настройки</string>
<string name="theme_setting">Тема</string>
<string name="theme_setting">Темы</string>
<string name="precision_setting">Точность</string>
<string name="separator_setting">Разделитель</string>
<string name="output_format_setting">Формат вывода</string>
@ -523,8 +523,6 @@
<string name="force_light_mode">Светлая</string>
<string name="force_dark_mode">Темная</string>
<string name="force_amoled_mode">Темная AMOLED</string>
<string name="force_light_dynamic_mode">Динамическая светлая</string>
<string name="force_dark_dynamic_mode">Динамическая темная</string>
<string name="loading_label">Загрузка…</string>
<string name="error_label">Ошибка</string>
<string name="copied">Скопировано %1$s!</string>
@ -673,5 +671,8 @@
<string name="send_usage_statistics">Отправлять статистику использования</string>
<string name="send_usage_statistics_support">Все данные анонимны и зашифрованы</string>
<string name="app_version_name_setting">Название версии</string>
<string name="enable_dynamic_colors">Динамичные цвета</string>
<string name="enable_dynamic_colors_support">Использовать цвета обоев рабочего стола</string>
<string name="force_amoled_mode_support">Использовать черный фон в темных темах</string>
</resources>

View File

@ -936,7 +936,7 @@
<string name="settings_screen">Settings</string>
<!--Settings items-->
<string name="theme_setting">Theme</string>
<string name="theme_setting">Themes</string>
<string name="precision_setting">Precision</string>
<string name="separator_setting">Separator</string>
<string name="output_format_setting">Output format</string>
@ -991,8 +991,9 @@
<string name="force_light_mode">Light</string>
<string name="force_dark_mode">Dark</string>
<string name="force_amoled_mode">AMOLED Dark</string>
<string name="force_light_dynamic_mode">Dynamic light</string>
<string name="force_dark_dynamic_mode">Dynamic dark</string>
<string name="force_amoled_mode_support">Use black background for dark themes</string>
<string name="enable_dynamic_colors">Dynamic colors</string>
<string name="enable_dynamic_colors_support">Generate theme from your current wallpaper</string>
<!--MISC.-->
<string name="loading_label">Loading…</string>

View File

@ -3,6 +3,7 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven(url = "https://jitpack.io")
}
}
rootProject.name = "Unitto"