diff --git a/src/main/kotlin/app/App.kt b/src/main/kotlin/app/App.kt index ff4c6b8..45f807e 100644 --- a/src/main/kotlin/app/App.kt +++ b/src/main/kotlin/app/App.kt @@ -11,12 +11,13 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.PointerIconDefaults import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Window -import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState import app.di.DaggerAppComponent @@ -29,11 +30,13 @@ import app.theme.secondaryTextColor import app.ui.AppTab import app.ui.components.RepositoriesTabPanel import app.ui.components.TabInformation -import app.ui.dialogs.SettingsDialog +import app.ui.dialogs.settings.SettingsDialog +import app.viewmodels.SettingsViewModel import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject + class App { private val appComponent = DaggerAppComponent.create() @@ -43,6 +46,9 @@ class App { @Inject lateinit var appPreferences: AppPreferences + @Inject + lateinit var settingsViewModel: SettingsViewModel + init { appComponent.inject(this) } @@ -60,6 +66,7 @@ class App { var isOpen by remember { mutableStateOf(true) } val theme by appPreferences.themeState.collectAsState() val customTheme by appPreferences.customThemeFlow.collectAsState() + val scale by appPreferences.scaleUiFlow.collectAsState() val windowState = rememberWindowState( placement = windowPlacement, @@ -78,25 +85,32 @@ class App { state = windowState, icon = painterResource("logo.svg"), ) { - var showSettingsDialog by remember { mutableStateOf(false) } + val density = if(scale != -1f) { + arrayOf(LocalDensity provides Density(scale, 1f)) + } else + emptyArray() - AppTheme( - selectedTheme = theme, - customTheme = customTheme, - ) { - Box(modifier = Modifier.background(MaterialTheme.colors.background)) { - AppTabs( - onOpenSettings = { - showSettingsDialog = true - } - ) - } + CompositionLocalProvider(values = density) { + var showSettingsDialog by remember { mutableStateOf(false) } - if (showSettingsDialog) { - SettingsDialog( - appPreferences = appPreferences, - onDismiss = { showSettingsDialog = false } - ) + AppTheme( + selectedTheme = theme, + customTheme = customTheme, + ) { + Box(modifier = Modifier.background(MaterialTheme.colors.background)) { + AppTabs( + onOpenSettings = { + showSettingsDialog = true + } + ) + } + + if (showSettingsDialog) { + SettingsDialog( + settingsViewModel = settingsViewModel, + onDismiss = { showSettingsDialog = false } + ) + } } } } @@ -104,6 +118,7 @@ class App { appStateManager.cancelCoroutines() this.exitApplication() } + } } diff --git a/src/main/kotlin/app/preferences/AppPreferences.kt b/src/main/kotlin/app/preferences/AppPreferences.kt index 81a5fa9..5751f5b 100644 --- a/src/main/kotlin/app/preferences/AppPreferences.kt +++ b/src/main/kotlin/app/preferences/AppPreferences.kt @@ -2,8 +2,7 @@ package app.preferences import app.extensions.defaultWindowPlacement import app.theme.ColorsScheme -import app.theme.Themes -import app.theme.darkBlueTheme +import app.theme.Theme import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.json.Json @@ -12,7 +11,6 @@ import java.util.prefs.Preferences import javax.inject.Inject import javax.inject.Singleton import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString private const val PREFERENCES_NAME = "GitnuroConfig" @@ -23,19 +21,21 @@ private const val PREF_COMMITS_LIMIT = "commitsLimit" private const val PREF_COMMITS_LIMIT_ENABLED = "commitsLimitEnabled" private const val PREF_WINDOW_PLACEMENT = "windowsPlacement" private const val PREF_CUSTOM_THEME = "customTheme" +private const val PREF_UI_SCALE = "ui_scale" private const val PREF_GIT_FF_MERGE = "gitFFMerge" private const val DEFAULT_COMMITS_LIMIT = 1000 private const val DEFAULT_COMMITS_LIMIT_ENABLED = true +const val DEFAULT_UI_SCALE = -1f @Singleton class AppPreferences @Inject constructor() { private val preferences: Preferences = Preferences.userRoot().node(PREFERENCES_NAME) private val _themeState = MutableStateFlow(theme) - val themeState: StateFlow = _themeState + val themeState: StateFlow = _themeState private val _commitsLimitEnabledFlow = MutableStateFlow(commitsLimitEnabled) val commitsLimitEnabledFlow: StateFlow = _commitsLimitEnabledFlow @@ -49,6 +49,9 @@ class AppPreferences @Inject constructor() { private val _customThemeFlow = MutableStateFlow(null) val customThemeFlow: StateFlow = _customThemeFlow + private val _scaleUiFlow = MutableStateFlow(scaleUi) + val scaleUiFlow: StateFlow = _scaleUiFlow + var latestTabsOpened: String get() = preferences.get(PREF_LATEST_REPOSITORIES_TABS_OPENED, "") set(value) { @@ -61,14 +64,14 @@ class AppPreferences @Inject constructor() { preferences.put(PREF_LAST_OPENED_REPOSITORIES_PATH, value) } - var theme: Themes + var theme: Theme get() { - val lastTheme = preferences.get(PREF_THEME, Themes.DARK.toString()) + val lastTheme = preferences.get(PREF_THEME, Theme.DARK.toString()) return try { - Themes.valueOf(lastTheme) + Theme.valueOf(lastTheme) } catch (ex: Exception) { ex.printStackTrace() - Themes.DARK + Theme.DARK } } set(value) { @@ -85,6 +88,15 @@ class AppPreferences @Inject constructor() { _commitsLimitEnabledFlow.value = value } + var scaleUi: Float + get() { + return preferences.getFloat(PREF_UI_SCALE, DEFAULT_UI_SCALE) + } + set(value) { + preferences.putFloat(PREF_UI_SCALE, value) + _scaleUiFlow.value = value + } + /** * Property that decides if the merge should fast-forward when possible */ diff --git a/src/main/kotlin/app/theme/Theme.kt b/src/main/kotlin/app/theme/Theme.kt index 32cc7a6..797e4a4 100644 --- a/src/main/kotlin/app/theme/Theme.kt +++ b/src/main/kotlin/app/theme/Theme.kt @@ -13,15 +13,15 @@ private var appTheme: ColorsScheme = defaultAppTheme @Composable fun AppTheme( - selectedTheme: Themes = Themes.DARK, + selectedTheme: Theme = Theme.DARK, customTheme: ColorsScheme?, content: @Composable() () -> Unit ) { val theme = when (selectedTheme) { - Themes.LIGHT -> lightTheme - Themes.DARK -> darkBlueTheme - Themes.DARK_GRAY -> darkGrayTheme - Themes.CUSTOM -> customTheme ?: defaultAppTheme + Theme.LIGHT -> lightTheme + Theme.DARK -> darkBlueTheme + Theme.DARK_GRAY -> darkGrayTheme + Theme.CUSTOM -> customTheme ?: defaultAppTheme } appTheme = theme @@ -99,7 +99,7 @@ val Colors.dialogOverlay: Color get() = appTheme.dialogOverlay -enum class Themes(val displayName: String) : DropDownOption { +enum class Theme(val displayName: String) : DropDownOption { LIGHT("Light"), DARK("Dark"), DARK_GRAY("Dark gray"), @@ -109,9 +109,9 @@ enum class Themes(val displayName: String) : DropDownOption { get() = displayName } -val themesList = listOf( - Themes.LIGHT, - Themes.DARK, - Themes.DARK_GRAY, - Themes.CUSTOM, +val themeLists = listOf( + Theme.LIGHT, + Theme.DARK, + Theme.DARK_GRAY, + Theme.CUSTOM, ) \ No newline at end of file diff --git a/src/main/kotlin/app/ui/dialogs/SettingsDialog.kt b/src/main/kotlin/app/ui/dialogs/settings/SettingsDialog.kt similarity index 50% rename from src/main/kotlin/app/ui/dialogs/SettingsDialog.kt rename to src/main/kotlin/app/ui/dialogs/settings/SettingsDialog.kt index 176a119..cb915bb 100644 --- a/src/main/kotlin/app/ui/dialogs/SettingsDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/settings/SettingsDialog.kt @@ -1,5 +1,6 @@ -package app.ui.dialogs +package app.ui.dialogs.settings +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* @@ -10,35 +11,54 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import app.preferences.AppPreferences import app.DropDownOption import app.theme.* import app.ui.components.AdjustableOutlinedTextField +import app.ui.components.ScrollableColumn +import app.ui.dialogs.MaterialDialog import app.ui.openFileDialog import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalDensity +import app.extensions.handMouseClickable +import app.preferences.DEFAULT_UI_SCALE +import app.viewmodels.SettingsViewModel + +enum class SettingsCategory(val displayName: String) { + UI("UI"), + GIT("Git"), +} + @Composable fun SettingsDialog( - appPreferences: AppPreferences, + settingsViewModel: SettingsViewModel, onDismiss: () -> Unit, ) { - val currentTheme by appPreferences.themeState.collectAsState() - val commitsLimitEnabled by appPreferences.commitsLimitEnabledFlow.collectAsState() - val ffMerge by appPreferences.ffMergeFlow.collectAsState() - var commitsLimit by remember { mutableStateOf(appPreferences.commitsLimit) } + + LaunchedEffect(Unit) { + settingsViewModel.resetInfo() + } + + val categories = remember { + listOf( + SettingsCategory.UI, + SettingsCategory.GIT, + ) + } + + var selectedCategory by remember { mutableStateOf(SettingsCategory.UI) } MaterialDialog( onCloseRequested = { - savePendingSettings( - appPreferences = appPreferences, - commitsLimit = commitsLimit, - ) + settingsViewModel.savePendingChanges() onDismiss() } ) { - Column(modifier = Modifier.width(720.dp)) { + Column(modifier = Modifier.height(720.dp)) { Text( text = "Settings", color = MaterialTheme.colors.primaryTextColor, @@ -46,69 +66,37 @@ fun SettingsDialog( modifier = Modifier.padding(top = 8.dp, bottom = 16.dp, start = 8.dp) ) - SettingDropDown( - title = "Theme", - subtitle = "Select the UI theme between light and dark mode", - dropDownOptions = themesList, - currentOption = currentTheme, - onOptionSelected = { theme -> - appPreferences.theme = theme - } - ) - - if (currentTheme == Themes.CUSTOM) { - SettingButton( - title = "Custom theme", - subtitle = "Select a JSON file to load the custom theme", - buttonText = "Open file", - onClick = { - val filePath = openFileDialog() - - if (filePath != null) { - appPreferences.saveCustomTheme(filePath) - } + Row(modifier = Modifier.weight(1f)) { + ScrollableColumn( + modifier = Modifier + .width(200.dp) + .fillMaxHeight() + ) { + categories.forEach { category -> + Category( + category = category, + isSelected = category == selectedCategory, + onClick = { selectedCategory = category } + ) } - ) + } + + + Column(modifier = Modifier.width(720.dp)) { + when (selectedCategory) { + SettingsCategory.UI -> UiSettings(settingsViewModel) + SettingsCategory.GIT -> GitSettings(settingsViewModel) + } + } } - SettingToogle( - title = "Limit log commits", - subtitle = "Turning off this may affect the performance", - value = commitsLimitEnabled, - onValueChanged = { value -> - appPreferences.commitsLimitEnabled = value - } - ) - - SettingIntInput( - title = "Max commits", - subtitle = "Increasing this value may affect the performance", - value = commitsLimit, - enabled = commitsLimitEnabled, - onValueChanged = { value -> - commitsLimit = value - } - ) - - SettingToogle( - title = "Fast-forward merge", - subtitle = "Try to fast-forward merges when possible", - value = ffMerge, - onValueChanged = { value -> - appPreferences.ffMerge = value - } - ) - TextButton( modifier = Modifier .padding(end = 8.dp) .align(Alignment.End), colors = textButtonColors(), onClick = { - savePendingSettings( - appPreferences = appPreferences, - commitsLimit = commitsLimit, - ) + settingsViewModel.savePendingChanges() onDismiss() } @@ -119,18 +107,126 @@ fun SettingsDialog( ) } } + } } -fun savePendingSettings( - appPreferences: AppPreferences, - commitsLimit: Int, -) { - if (appPreferences.commitsLimit != commitsLimit) { - appPreferences.commitsLimit = commitsLimit - } +@Composable +fun GitSettings(settingsViewModel: SettingsViewModel) { + val commitsLimitEnabled by settingsViewModel.commitsLimitEnabledFlow.collectAsState() + val ffMerge by settingsViewModel.ffMergeFlow.collectAsState() + var commitsLimit by remember { mutableStateOf(settingsViewModel.commitsLimit) } + + SettingToggle( + title = "Limit log commits", + subtitle = "Turning off this may affect the performance", + value = commitsLimitEnabled, + onValueChanged = { value -> + settingsViewModel.commitsLimitEnabled = value + } + ) + + SettingIntInput( + title = "Max commits", + subtitle = "Increasing this value may affect the performance", + value = commitsLimit, + enabled = commitsLimitEnabled, + onValueChanged = { value -> + commitsLimit = value + settingsViewModel.commitsLimit = value + } + ) + + SettingToggle( + title = "Fast-forward merge", + subtitle = "Try to fast-forward merges when possible", + value = ffMerge, + onValueChanged = { value -> + settingsViewModel.ffMerge = value + } + ) } +@Composable +fun UiSettings(settingsViewModel: SettingsViewModel) { + val currentTheme by settingsViewModel.themeState.collectAsState() + + SettingDropDown( + title = "Theme", + subtitle = "Select the UI theme between light and dark mode", + dropDownOptions = themeLists, + currentOption = currentTheme, + onOptionSelected = { theme -> + settingsViewModel.theme = theme + } + ) + + if (currentTheme == Theme.CUSTOM) { + SettingButton( + title = "Custom theme", + subtitle = "Select a JSON file to load the custom theme", + buttonText = "Open file", + onClick = { + val filePath = openFileDialog() + + if (filePath != null) { + settingsViewModel.saveCustomTheme(filePath) + } + } + ) + } + + val density = LocalDensity.current.density + var scaleValue by remember { + val savedScaleUi = settingsViewModel.scaleUi + val scaleUi = if (savedScaleUi == DEFAULT_UI_SCALE) { + density + } else { + savedScaleUi + } * 100 + + mutableStateOf(scaleUi) + } + + SettingSlider( + title = "Scale", + subtitle = "Adapt the size the UI to your preferred scale", + value = scaleValue, + onValueChanged = { newValue -> + scaleValue = newValue + }, + onValueChangeFinished = { + settingsViewModel.scaleUi = scaleValue / 100 + }, + steps = 5, + minValue = 100f, + maxValue = 300f, + ) +} + +@Composable +fun Category( + category: SettingsCategory, + isSelected: Boolean, + onClick: () -> Unit, +) { + val backgroundColor = if (isSelected) + MaterialTheme.colors.backgroundSelected + else + MaterialTheme.colors.background + + Text( + text = category.displayName, + modifier = Modifier + .fillMaxWidth() + .background(color = backgroundColor) + .handMouseClickable(onClick) + .padding(8.dp), + style = MaterialTheme.typography.body1, + ) +} + + @Composable fun SettingDropDown( title: String, @@ -203,7 +299,7 @@ fun SettingButton( } @Composable -fun SettingToogle( +fun SettingToggle( title: String, subtitle: String, value: Boolean, @@ -221,6 +317,46 @@ fun SettingToogle( } } +@Composable +fun SettingSlider( + title: String, + subtitle: String, + value: Float, + minValue: Float, + maxValue: Float, + steps: Int, + onValueChanged: (Float) -> Unit, + onValueChangeFinished: () -> Unit, +) { + Row( + modifier = Modifier.padding(vertical = 8.dp, horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + FieldTitles(title, subtitle) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = "$minValue%", + style = MaterialTheme.typography.caption, + ) + + Slider( + value = value, + onValueChange = onValueChanged, + onValueChangeFinished = onValueChangeFinished, + steps = steps, + valueRange = minValue..maxValue, + modifier = Modifier.width(200.dp) + ) + + Text( + text = "$maxValue%", + style = MaterialTheme.typography.caption, + ) + } +} + @Composable fun SettingIntInput( title: String, @@ -303,4 +439,13 @@ private fun isValidInt(value: String): Boolean { } catch (ex: Exception) { false } +} + +private fun isValidFloat(value: String): Boolean { + return try { + value.toFloat() + true + } catch (ex: Exception) { + false + } } \ No newline at end of file diff --git a/src/main/kotlin/app/ui/log/Log.kt b/src/main/kotlin/app/ui/log/Log.kt index 0bf0486..2aafb94 100644 --- a/src/main/kotlin/app/ui/log/Log.kt +++ b/src/main/kotlin/app/ui/log/Log.kt @@ -192,10 +192,11 @@ fun Log( } ) + val density = LocalDensity.current.density DividerLog( modifier = Modifier.draggable( rememberDraggableState { - weightMod.value += it + weightMod.value += it * density }, Orientation.Horizontal ), graphWidth = graphWidth, diff --git a/src/main/kotlin/app/viewmodels/SettingsViewModel.kt b/src/main/kotlin/app/viewmodels/SettingsViewModel.kt new file mode 100644 index 0000000..604080c --- /dev/null +++ b/src/main/kotlin/app/viewmodels/SettingsViewModel.kt @@ -0,0 +1,60 @@ +package app.viewmodels + +import app.preferences.AppPreferences +import app.theme.Theme +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SettingsViewModel @Inject constructor( + val appPreferences: AppPreferences, +) { + // Temporary values to detect changed variables + var commitsLimit: Int = -1 + + val themeState = appPreferences.themeState + val customThemeFlow = appPreferences.customThemeFlow + val ffMergeFlow = appPreferences.ffMergeFlow + val commitsLimitEnabledFlow = appPreferences.commitsLimitEnabledFlow + + var scaleUi: Float + get() = appPreferences.scaleUi + set(value) { + appPreferences.scaleUi = value + } + + var commitsLimitEnabled: Boolean + get() = appPreferences.commitsLimitEnabled + set(value) { + appPreferences.commitsLimitEnabled = value + } + + var ffMerge: Boolean + get() = appPreferences.ffMerge + set(value) { + appPreferences.ffMerge = value + } + + var theme: Theme + get() = appPreferences.theme + set(value) { + appPreferences.theme = value + } + + fun saveCustomTheme(filePath: String) { + appPreferences.saveCustomTheme(filePath) + } + + + fun resetInfo() { + commitsLimit = appPreferences.commitsLimit + } + + fun savePendingChanges() { + val commitsLimit = this.commitsLimit + + if (appPreferences.commitsLimit != commitsLimit) { + appPreferences.commitsLimit = commitsLimit + } + } +} \ No newline at end of file