From 43330eb3c4c98753c8e4b52b3642cf7547e2949e Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Sun, 19 Jun 2022 22:49:41 +0200 Subject: [PATCH] Added option to load custom themes. Fixes https://github.com/JetpackDuba/Gitnuro/issues/3 --- src/main/kotlin/app/App.kt | 8 +- .../kotlin/app/preferences/AppPreferences.kt | 31 +++++++ src/main/kotlin/app/theme/ColorsScheme.kt | 26 ++++++ src/main/kotlin/app/theme/Theme.kt | 14 ++- src/main/kotlin/app/ui/RepositoryOpen.kt | 4 - src/main/kotlin/app/ui/SystemDialogs.kt | 87 ++++++++----------- src/main/kotlin/app/ui/WelcomePage.kt | 2 - src/main/kotlin/app/ui/dialogs/CloneDialog.kt | 2 +- .../kotlin/app/ui/dialogs/SettingsDialog.kt | 59 ++++++++++++- 9 files changed, 167 insertions(+), 66 deletions(-) diff --git a/src/main/kotlin/app/App.kt b/src/main/kotlin/app/App.kt index c24225f..ff4c6b8 100644 --- a/src/main/kotlin/app/App.kt +++ b/src/main/kotlin/app/App.kt @@ -53,11 +53,14 @@ class App { val windowPlacement = appPreferences.windowPlacement.toWindowPlacement appStateManager.loadRepositoriesTabs() + appPreferences.loadCustomTheme() loadTabs() application { var isOpen by remember { mutableStateOf(true) } val theme by appPreferences.themeState.collectAsState() + val customTheme by appPreferences.customThemeFlow.collectAsState() + val windowState = rememberWindowState( placement = windowPlacement, size = DpSize(1280.dp, 720.dp) @@ -77,7 +80,10 @@ class App { ) { var showSettingsDialog by remember { mutableStateOf(false) } - AppTheme(selectedTheme = theme) { + AppTheme( + selectedTheme = theme, + customTheme = customTheme, + ) { Box(modifier = Modifier.background(MaterialTheme.colors.background)) { AppTabs( onOpenSettings = { diff --git a/src/main/kotlin/app/preferences/AppPreferences.kt b/src/main/kotlin/app/preferences/AppPreferences.kt index 828d4bd..507122f 100644 --- a/src/main/kotlin/app/preferences/AppPreferences.kt +++ b/src/main/kotlin/app/preferences/AppPreferences.kt @@ -1,12 +1,18 @@ package app.preferences import app.extensions.defaultWindowPlacement +import app.theme.ColorsScheme import app.theme.Themes +import app.theme.darkBlueTheme import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.serialization.json.Json +import java.io.File 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" @@ -16,6 +22,7 @@ private const val PREF_THEME = "theme" 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 DEFAULT_COMMITS_LIMIT = 1000 private const val DEFAULT_COMMITS_LIMIT_ENABLED = true @@ -33,6 +40,9 @@ class AppPreferences @Inject constructor() { private val _commitsLimitFlow = MutableStateFlow(commitsLimit) val commitsLimitFlow: StateFlow = _commitsLimitFlow + private val _customThemeFlow = MutableStateFlow(null) + val customThemeFlow: StateFlow = _customThemeFlow + var latestTabsOpened: String get() = preferences.get(PREF_LATEST_REPOSITORIES_TABS_OPENED, "") set(value) { @@ -87,4 +97,25 @@ class AppPreferences @Inject constructor() { set(placement) { preferences.putInt(PREF_WINDOW_PLACEMENT, placement.value) } + + fun saveCustomTheme(filePath: String) { + try { + val file = File(filePath) + val content = file.readText() + + Json.decodeFromString(content) // Load to see if it's valid (it will crash if not) + + preferences.put(PREF_CUSTOM_THEME, content) + loadCustomTheme() + } catch (ex: Exception) { + ex.printStackTrace() + } + } + + fun loadCustomTheme() { + val themeJson = preferences.get(PREF_CUSTOM_THEME, null) + if (themeJson != null) { + _customThemeFlow.value = Json.decodeFromString(themeJson) + } + } } \ No newline at end of file diff --git a/src/main/kotlin/app/theme/ColorsScheme.kt b/src/main/kotlin/app/theme/ColorsScheme.kt index 7fbdfda..81084f2 100644 --- a/src/main/kotlin/app/theme/ColorsScheme.kt +++ b/src/main/kotlin/app/theme/ColorsScheme.kt @@ -1,8 +1,19 @@ +@file:UseSerializers(ColorAsStringSerializer::class) package app.theme import androidx.compose.material.Colors import androidx.compose.ui.graphics.Color +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@Serializable data class ColorsScheme( val primary: Color, val primaryVariant: Color, @@ -45,4 +56,19 @@ data class ColorsScheme( isLight = true, // property specific for some colors, we don't care about this as all our components are customized ) } +} + +object ColorAsStringSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Color", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Color) { + encoder.encodeString("") + } + + override fun deserialize(decoder: Decoder): Color { + val value = decoder.decodeString() + val longValue = value.toLong(radix = 16) + + return Color(longValue) + } } \ No newline at end of file diff --git a/src/main/kotlin/app/theme/Theme.kt b/src/main/kotlin/app/theme/Theme.kt index a1be801..0ed71e7 100644 --- a/src/main/kotlin/app/theme/Theme.kt +++ b/src/main/kotlin/app/theme/Theme.kt @@ -8,14 +8,20 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import app.DropDownOption -private var appTheme: ColorsScheme = darkBlueTheme +private val defaultAppTheme: ColorsScheme = darkBlueTheme +private var appTheme: ColorsScheme = defaultAppTheme @Composable -fun AppTheme(selectedTheme: Themes = Themes.DARK, content: @Composable() () -> Unit) { +fun AppTheme( + selectedTheme: Themes = Themes.DARK, + customTheme: ColorsScheme?, + content: @Composable() () -> Unit +) { val theme = when (selectedTheme) { Themes.LIGHT -> lightTheme Themes.DARK -> darkBlueTheme Themes.DARK_GRAY -> darkGrayTheme + Themes.CUSTOM -> customTheme ?: defaultAppTheme } appTheme = theme @@ -96,7 +102,8 @@ val Colors.dialogOverlay: Color enum class Themes(val displayName: String) : DropDownOption { LIGHT("Light"), DARK("Dark"), - DARK_GRAY("Dark gray"); + DARK_GRAY("Dark gray"), + CUSTOM("Custom"); override val optionName: String get() = displayName @@ -106,4 +113,5 @@ val themesList = listOf( Themes.LIGHT, Themes.DARK, Themes.DARK_GRAY, + Themes.CUSTOM, ) \ No newline at end of file diff --git a/src/main/kotlin/app/ui/RepositoryOpen.kt b/src/main/kotlin/app/ui/RepositoryOpen.kt index 23aa766..ddbc950 100644 --- a/src/main/kotlin/app/ui/RepositoryOpen.kt +++ b/src/main/kotlin/app/ui/RepositoryOpen.kt @@ -3,16 +3,13 @@ package app.ui import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.unit.dp @@ -25,7 +22,6 @@ import app.ui.dialogs.StashWithMessageDialog import app.ui.log.Log import app.viewmodels.BlameState import app.viewmodels.TabViewModel -import openRepositoryDialog import org.eclipse.jgit.lib.RepositoryState import org.eclipse.jgit.revwalk.RevCommit import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi diff --git a/src/main/kotlin/app/ui/SystemDialogs.kt b/src/main/kotlin/app/ui/SystemDialogs.kt index 28e35a1..cbd1b2b 100644 --- a/src/main/kotlin/app/ui/SystemDialogs.kt +++ b/src/main/kotlin/app/ui/SystemDialogs.kt @@ -1,77 +1,62 @@ -import app.extensions.runCommand +package app.ui + import app.viewmodels.TabViewModel +import java.io.File import javax.swing.JFileChooser import javax.swing.UIManager -fun openDirectoryDialog(): String? { - val os = System.getProperty("os.name") - var dirToOpen: String? = null - - if (os.lowercase() == "linux") { - val checkZenityInstalled = runCommand("which zenity 2>/dev/null") - val isZenityInstalled = !checkZenityInstalled.isNullOrEmpty() - - if (isZenityInstalled) { - val openDirectory = runCommand( - "zenity --file-selection --title=Open --directory" - )?.replace("\n", "") - - if (!openDirectory.isNullOrEmpty()) - dirToOpen = openDirectory - } else - dirToOpen = openJvmDialog("", true) - } else { - dirToOpen = openJvmDialog("", false) - } - - return dirToOpen +fun openDirectoryDialog(basePath: String? = null): String? { + return openPickerDialog( + pickerType = PickerType.DIRECTORIES, + basePath = basePath, + ) } + +fun openFileDialog(basePath: String? = null): String? { + return openPickerDialog( + pickerType = PickerType.FILES, + basePath = basePath, + ) +} + fun openRepositoryDialog(tabViewModel: TabViewModel) { - val os = System.getProperty("os.name") val appStateManager = tabViewModel.appStateManager val latestDirectoryOpened = appStateManager.latestOpenedRepositoryPath - var dirToOpen: String? = null - if (os.lowercase() == "linux") { - val checkZenityInstalled = runCommand("which zenity 2>/dev/null") - val isZenityInstalled = !checkZenityInstalled.isNullOrEmpty() - - if (isZenityInstalled) { - val openDirectory = runCommand( - "zenity --file-selection --title=Open --directory --filename=\"$latestDirectoryOpened\"" - )?.replace("\n", "") - - if (!openDirectory.isNullOrEmpty()) - dirToOpen = openDirectory - } else - dirToOpen = openJvmDialog(latestDirectoryOpened, true) - } else { - dirToOpen = openJvmDialog(latestDirectoryOpened, false) - } - - if(dirToOpen != null) + val dirToOpen = openDirectoryDialog(latestDirectoryOpened) + if (dirToOpen != null) tabViewModel.openRepository(dirToOpen) } -private fun openJvmDialog( - latestDirectoryOpened: String, - isLinux: Boolean, -) : String? { +private fun openPickerDialog( + pickerType: PickerType, + basePath: String?, +): String? { + + val os = System.getProperty("os.name") + val isLinux = os.lowercase() == "linux" + if (!isLinux) { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()) } - val fileChooser = if (latestDirectoryOpened.isEmpty()) + val fileChooser = if (basePath.isNullOrEmpty()) JFileChooser() else - JFileChooser(latestDirectoryOpened) + JFileChooser(basePath) - fileChooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY + fileChooser.fileSelectionMode = pickerType.value fileChooser.showOpenDialog(null) return if (fileChooser.selectedFile != null) fileChooser.selectedFile.absolutePath - else + else null +} + +enum class PickerType(val value: Int) { + FILES(JFileChooser.FILES_ONLY), + DIRECTORIES(JFileChooser.DIRECTORIES_ONLY), + FILES_AND_DIRECTORIES(JFileChooser.FILES_AND_DIRECTORIES); } \ No newline at end of file diff --git a/src/main/kotlin/app/ui/WelcomePage.kt b/src/main/kotlin/app/ui/WelcomePage.kt index 7d27f68..eba4f2e 100644 --- a/src/main/kotlin/app/ui/WelcomePage.kt +++ b/src/main/kotlin/app/ui/WelcomePage.kt @@ -31,8 +31,6 @@ import app.ui.dialogs.AppInfoDialog import app.ui.dialogs.CloneDialog import app.updates.Update import app.viewmodels.TabViewModel -import openDirectoryDialog -import openRepositoryDialog @OptIn(ExperimentalMaterialApi::class) diff --git a/src/main/kotlin/app/ui/dialogs/CloneDialog.kt b/src/main/kotlin/app/ui/dialogs/CloneDialog.kt index 7643284..7416842 100644 --- a/src/main/kotlin/app/ui/dialogs/CloneDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/CloneDialog.kt @@ -23,7 +23,7 @@ import app.theme.primaryTextColor import app.theme.textButtonColors import app.ui.components.PrimaryButton import app.viewmodels.CloneViewModel -import openDirectoryDialog +import app.ui.openDirectoryDialog import java.io.File @Composable diff --git a/src/main/kotlin/app/ui/dialogs/SettingsDialog.kt b/src/main/kotlin/app/ui/dialogs/SettingsDialog.kt index e41305a..ca144d6 100644 --- a/src/main/kotlin/app/ui/dialogs/SettingsDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/SettingsDialog.kt @@ -12,10 +12,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.preferences.AppPreferences import app.DropDownOption -import app.theme.outlinedTextFieldColors -import app.theme.primaryTextColor -import app.theme.textButtonColors -import app.theme.themesList +import app.theme.* +import app.ui.openFileDialog import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -56,6 +54,21 @@ fun SettingsDialog( } ) + 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) + } + } + ) + } + SettingToogle( title = "Limit log commits", subtitle = "Turning off this may affect the performance", @@ -162,6 +175,44 @@ fun SettingDropDown( } } +@Composable +fun SettingButton( + title: String, + subtitle: String, + buttonText: String, + onClick: () -> Unit, +) { + Row( + modifier = Modifier.padding(vertical = 8.dp, horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(verticalArrangement = Arrangement.Center) { + Text( + text = title, + color = MaterialTheme.colors.primaryTextColor, + fontSize = 16.sp, + ) + + Text( + text = subtitle, + color = MaterialTheme.colors.primaryTextColor, + modifier = Modifier.padding(top = 4.dp), + fontSize = 12.sp, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + OutlinedButton(onClick = onClick) { + Text( + text = buttonText, + color = MaterialTheme.colors.primaryTextColor, + fontSize = 14.sp, + ) + } + } +} + @Composable fun SettingToogle( title: String,