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
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 = {

View File

@ -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<Int> = _commitsLimitFlow
private val _customThemeFlow = MutableStateFlow<ColorsScheme?>(null)
val customThemeFlow: StateFlow<ColorsScheme?> = _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<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
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<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 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,
)

View File

@ -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

View File

@ -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);
}

View File

@ -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)

View File

@ -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

View File

@ -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 <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
fun SettingToogle(
title: String,