Added option to load custom themes.

Fixes https://github.com/JetpackDuba/Gitnuro/issues/3
This commit is contained in:
Abdelilah El Aissaoui 2022-06-19 22:49:41 +02:00
parent 15827d119a
commit 43330eb3c4
9 changed files with 167 additions and 66 deletions

View File

@ -53,11 +53,14 @@ class App {
val windowPlacement = appPreferences.windowPlacement.toWindowPlacement val windowPlacement = appPreferences.windowPlacement.toWindowPlacement
appStateManager.loadRepositoriesTabs() appStateManager.loadRepositoriesTabs()
appPreferences.loadCustomTheme()
loadTabs() loadTabs()
application { application {
var isOpen by remember { mutableStateOf(true) } var isOpen by remember { mutableStateOf(true) }
val theme by appPreferences.themeState.collectAsState() val theme by appPreferences.themeState.collectAsState()
val customTheme by appPreferences.customThemeFlow.collectAsState()
val windowState = rememberWindowState( val windowState = rememberWindowState(
placement = windowPlacement, placement = windowPlacement,
size = DpSize(1280.dp, 720.dp) size = DpSize(1280.dp, 720.dp)
@ -77,7 +80,10 @@ class App {
) { ) {
var showSettingsDialog by remember { mutableStateOf(false) } var showSettingsDialog by remember { mutableStateOf(false) }
AppTheme(selectedTheme = theme) { AppTheme(
selectedTheme = theme,
customTheme = customTheme,
) {
Box(modifier = Modifier.background(MaterialTheme.colors.background)) { Box(modifier = Modifier.background(MaterialTheme.colors.background)) {
AppTabs( AppTabs(
onOpenSettings = { onOpenSettings = {

View File

@ -1,12 +1,18 @@
package app.preferences package app.preferences
import app.extensions.defaultWindowPlacement import app.extensions.defaultWindowPlacement
import app.theme.ColorsScheme
import app.theme.Themes import app.theme.Themes
import app.theme.darkBlueTheme
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.serialization.json.Json
import java.io.File
import java.util.prefs.Preferences import java.util.prefs.Preferences
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
private const val PREFERENCES_NAME = "GitnuroConfig" 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 = "commitsLimit"
private const val PREF_COMMITS_LIMIT_ENABLED = "commitsLimitEnabled" private const val PREF_COMMITS_LIMIT_ENABLED = "commitsLimitEnabled"
private const val PREF_WINDOW_PLACEMENT = "windowsPlacement" 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 = 1000
private const val DEFAULT_COMMITS_LIMIT_ENABLED = true private const val DEFAULT_COMMITS_LIMIT_ENABLED = true
@ -33,6 +40,9 @@ class AppPreferences @Inject constructor() {
private val _commitsLimitFlow = MutableStateFlow(commitsLimit) private val _commitsLimitFlow = MutableStateFlow(commitsLimit)
val commitsLimitFlow: StateFlow<Int> = _commitsLimitFlow val commitsLimitFlow: StateFlow<Int> = _commitsLimitFlow
private val _customThemeFlow = MutableStateFlow<ColorsScheme?>(null)
val customThemeFlow: StateFlow<ColorsScheme?> = _customThemeFlow
var latestTabsOpened: String var latestTabsOpened: String
get() = preferences.get(PREF_LATEST_REPOSITORIES_TABS_OPENED, "") get() = preferences.get(PREF_LATEST_REPOSITORIES_TABS_OPENED, "")
set(value) { set(value) {
@ -87,4 +97,25 @@ class AppPreferences @Inject constructor() {
set(placement) { set(placement) {
preferences.putInt(PREF_WINDOW_PLACEMENT, placement.value) preferences.putInt(PREF_WINDOW_PLACEMENT, placement.value)
} }
fun saveCustomTheme(filePath: String) {
try {
val file = File(filePath)
val content = file.readText()
Json.decodeFromString<ColorsScheme>(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<ColorsScheme>(themeJson)
}
}
} }

View File

@ -1,8 +1,19 @@
@file:UseSerializers(ColorAsStringSerializer::class)
package app.theme package app.theme
import androidx.compose.material.Colors import androidx.compose.material.Colors
import androidx.compose.ui.graphics.Color 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( data class ColorsScheme(
val primary: Color, val primary: Color,
val primaryVariant: Color, val primaryVariant: Color,
@ -46,3 +57,18 @@ data class ColorsScheme(
) )
} }
} }
object ColorAsStringSerializer : KSerializer<Color> {
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)
}
}

View File

@ -8,14 +8,20 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import app.DropDownOption import app.DropDownOption
private var appTheme: ColorsScheme = darkBlueTheme private val defaultAppTheme: ColorsScheme = darkBlueTheme
private var appTheme: ColorsScheme = defaultAppTheme
@Composable @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) { val theme = when (selectedTheme) {
Themes.LIGHT -> lightTheme Themes.LIGHT -> lightTheme
Themes.DARK -> darkBlueTheme Themes.DARK -> darkBlueTheme
Themes.DARK_GRAY -> darkGrayTheme Themes.DARK_GRAY -> darkGrayTheme
Themes.CUSTOM -> customTheme ?: defaultAppTheme
} }
appTheme = theme appTheme = theme
@ -96,7 +102,8 @@ val Colors.dialogOverlay: Color
enum class Themes(val displayName: String) : DropDownOption { enum class Themes(val displayName: String) : DropDownOption {
LIGHT("Light"), LIGHT("Light"),
DARK("Dark"), DARK("Dark"),
DARK_GRAY("Dark gray"); DARK_GRAY("Dark gray"),
CUSTOM("Custom");
override val optionName: String override val optionName: String
get() = displayName get() = displayName
@ -106,4 +113,5 @@ val themesList = listOf(
Themes.LIGHT, Themes.LIGHT,
Themes.DARK, Themes.DARK,
Themes.DARK_GRAY, Themes.DARK_GRAY,
Themes.CUSTOM,
) )

View File

@ -3,16 +3,13 @@
package app.ui package app.ui
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color 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.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -25,7 +22,6 @@ import app.ui.dialogs.StashWithMessageDialog
import app.ui.log.Log import app.ui.log.Log
import app.viewmodels.BlameState import app.viewmodels.BlameState
import app.viewmodels.TabViewModel import app.viewmodels.TabViewModel
import openRepositoryDialog
import org.eclipse.jgit.lib.RepositoryState import org.eclipse.jgit.lib.RepositoryState
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi

View File

@ -1,73 +1,52 @@
import app.extensions.runCommand package app.ui
import app.viewmodels.TabViewModel import app.viewmodels.TabViewModel
import java.io.File
import javax.swing.JFileChooser import javax.swing.JFileChooser
import javax.swing.UIManager import javax.swing.UIManager
fun openDirectoryDialog(): String? { fun openDirectoryDialog(basePath: String? = null): String? {
val os = System.getProperty("os.name") return openPickerDialog(
var dirToOpen: String? = null pickerType = PickerType.DIRECTORIES,
basePath = basePath,
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 openFileDialog(basePath: String? = null): String? {
return openPickerDialog(
pickerType = PickerType.FILES,
basePath = basePath,
)
}
fun openRepositoryDialog(tabViewModel: TabViewModel) { fun openRepositoryDialog(tabViewModel: TabViewModel) {
val os = System.getProperty("os.name")
val appStateManager = tabViewModel.appStateManager val appStateManager = tabViewModel.appStateManager
val latestDirectoryOpened = appStateManager.latestOpenedRepositoryPath val latestDirectoryOpened = appStateManager.latestOpenedRepositoryPath
var dirToOpen: String? = null
if (os.lowercase() == "linux") { val dirToOpen = openDirectoryDialog(latestDirectoryOpened)
val checkZenityInstalled = runCommand("which zenity 2>/dev/null") if (dirToOpen != 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)
tabViewModel.openRepository(dirToOpen) tabViewModel.openRepository(dirToOpen)
} }
private fun openJvmDialog( private fun openPickerDialog(
latestDirectoryOpened: String, pickerType: PickerType,
isLinux: Boolean, basePath: String?,
) : String? { ): String? {
val os = System.getProperty("os.name")
val isLinux = os.lowercase() == "linux"
if (!isLinux) { if (!isLinux) {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()) UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
} }
val fileChooser = if (latestDirectoryOpened.isEmpty()) val fileChooser = if (basePath.isNullOrEmpty())
JFileChooser() JFileChooser()
else else
JFileChooser(latestDirectoryOpened) JFileChooser(basePath)
fileChooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY fileChooser.fileSelectionMode = pickerType.value
fileChooser.showOpenDialog(null) fileChooser.showOpenDialog(null)
return if (fileChooser.selectedFile != null) return if (fileChooser.selectedFile != null)
@ -75,3 +54,9 @@ private fun openJvmDialog(
else else
null null
} }
enum class PickerType(val value: Int) {
FILES(JFileChooser.FILES_ONLY),
DIRECTORIES(JFileChooser.DIRECTORIES_ONLY),
FILES_AND_DIRECTORIES(JFileChooser.FILES_AND_DIRECTORIES);
}

View File

@ -31,8 +31,6 @@ import app.ui.dialogs.AppInfoDialog
import app.ui.dialogs.CloneDialog import app.ui.dialogs.CloneDialog
import app.updates.Update import app.updates.Update
import app.viewmodels.TabViewModel import app.viewmodels.TabViewModel
import openDirectoryDialog
import openRepositoryDialog
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)

View File

@ -23,7 +23,7 @@ import app.theme.primaryTextColor
import app.theme.textButtonColors import app.theme.textButtonColors
import app.ui.components.PrimaryButton import app.ui.components.PrimaryButton
import app.viewmodels.CloneViewModel import app.viewmodels.CloneViewModel
import openDirectoryDialog import app.ui.openDirectoryDialog
import java.io.File import java.io.File
@Composable @Composable

View File

@ -12,10 +12,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import app.preferences.AppPreferences import app.preferences.AppPreferences
import app.DropDownOption import app.DropDownOption
import app.theme.outlinedTextFieldColors import app.theme.*
import app.theme.primaryTextColor import app.ui.openFileDialog
import app.theme.textButtonColors
import app.theme.themesList
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch 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( SettingToogle(
title = "Limit log commits", title = "Limit log commits",
subtitle = "Turning off this may affect the performance", subtitle = "Turning off this may affect the performance",
@ -162,6 +175,44 @@ fun <T : DropDownOption> 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 @Composable
fun SettingToogle( fun SettingToogle(
title: String, title: String,