Added option to load custom themes.
Fixes https://github.com/JetpackDuba/Gitnuro/issues/3
This commit is contained in:
parent
15827d119a
commit
43330eb3c4
@ -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 = {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
)
|
)
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user