Merge branch 'JetpackDuba:main' into main

This commit is contained in:
Infinity 2022-06-11 05:18:09 +02:00 committed by GitHub
commit 7f22c833dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1040 additions and 356 deletions

View File

@ -4,18 +4,11 @@ package app
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.*
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
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.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerIconDefaults import androidx.compose.ui.input.pointer.PointerIconDefaults
import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@ -26,7 +19,6 @@ import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowPlacement
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
import androidx.compose.ui.zIndex
import app.di.DaggerAppComponent import app.di.DaggerAppComponent
import app.theme.AppTheme import app.theme.AppTheme
import app.theme.primaryTextColor import app.theme.primaryTextColor
@ -76,7 +68,7 @@ class App {
) { ) {
var showSettingsDialog by remember { mutableStateOf(false) } var showSettingsDialog by remember { mutableStateOf(false) }
AppTheme(theme = theme) { AppTheme(selectedTheme = theme) {
Box(modifier = Modifier.background(MaterialTheme.colors.background)) { Box(modifier = Modifier.background(MaterialTheme.colors.background)) {
AppTabs( AppTabs(
onOpenSettings = { onOpenSettings = {
@ -127,13 +119,8 @@ class App {
) { ) {
val tabs by tabsFlow.collectAsState() val tabs by tabsFlow.collectAsState()
val tabsInformationList = tabs.sortedBy { it.key } val tabsInformationList = tabs.sortedBy { it.key }
println("Tabs count ${tabs.count()}")
val selectedTabKey = remember { mutableStateOf(0) } val selectedTabKey = remember { mutableStateOf(0) }
println("Selected tab key: ${selectedTabKey.value}")
Column( Column(
modifier = Modifier.background(MaterialTheme.colors.background) modifier = Modifier.background(MaterialTheme.colors.background)
) { ) {
@ -214,7 +201,7 @@ class App {
painter = painterResource("settings.svg"), painter = painterResource("settings.svg"),
contentDescription = null, contentDescription = null,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
tint = MaterialTheme.colors.primary, tint = MaterialTheme.colors.primaryVariant,
) )
} }
} }

View File

@ -12,6 +12,11 @@ private const val PREFERENCES_NAME = "GitnuroConfig"
private const val PREF_LATEST_REPOSITORIES_TABS_OPENED = "latestRepositoriesTabsOpened" private const val PREF_LATEST_REPOSITORIES_TABS_OPENED = "latestRepositoriesTabsOpened"
private const val PREF_LAST_OPENED_REPOSITORIES_PATH = "lastOpenedRepositoriesList" private const val PREF_LAST_OPENED_REPOSITORIES_PATH = "lastOpenedRepositoriesList"
private const val PREF_THEME = "theme" private const val PREF_THEME = "theme"
private const val PREF_COMMITS_LIMIT = "commitsLimit"
private const val PREF_COMMITS_LIMIT_ENABLED = "commitsLimitEnabled"
private const val DEFAULT_COMMITS_LIMIT = 1000
private const val DEFAULT_COMMITS_LIMIT_ENABLED = true
@Singleton @Singleton
class AppPreferences @Inject constructor() { class AppPreferences @Inject constructor() {
@ -20,6 +25,12 @@ class AppPreferences @Inject constructor() {
private val _themeState = MutableStateFlow(theme) private val _themeState = MutableStateFlow(theme)
val themeState: StateFlow<Themes> = _themeState val themeState: StateFlow<Themes> = _themeState
private val _commitsLimitEnabledFlow = MutableStateFlow(true)
val commitsLimitEnabledFlow: StateFlow<Boolean> = _commitsLimitEnabledFlow
private val _commitsLimitFlow = MutableStateFlow(commitsLimit)
val commitsLimitFlow: StateFlow<Int> = _commitsLimitFlow
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) {
@ -35,10 +46,33 @@ class AppPreferences @Inject constructor() {
var theme: Themes var theme: Themes
get() { get() {
val lastTheme = preferences.get(PREF_THEME, Themes.DARK.toString()) val lastTheme = preferences.get(PREF_THEME, Themes.DARK.toString())
return Themes.valueOf(lastTheme) return try {
Themes.valueOf(lastTheme)
} catch (ex: Exception) {
ex.printStackTrace()
Themes.DARK
}
} }
set(value) { set(value) {
preferences.put(PREF_THEME, value.toString()) preferences.put(PREF_THEME, value.toString())
_themeState.value = value _themeState.value = value
} }
var commitsLimitEnabled: Boolean
get() {
return preferences.getBoolean(PREF_COMMITS_LIMIT_ENABLED, DEFAULT_COMMITS_LIMIT_ENABLED)
}
set(value) {
preferences.putBoolean(PREF_COMMITS_LIMIT_ENABLED, value)
_commitsLimitEnabledFlow.value = value
}
var commitsLimit: Int
get() {
return preferences.getInt(PREF_COMMITS_LIMIT, DEFAULT_COMMITS_LIMIT)
}
set(value) {
preferences.putInt(PREF_COMMITS_LIMIT, value)
_commitsLimitFlow.value = value
}
} }

View File

@ -1,6 +1,7 @@
package app.di package app.di
import app.App import app.App
import app.AppPreferences
import app.AppStateManager import app.AppStateManager
import dagger.Component import dagger.Component
import javax.inject.Singleton import javax.inject.Singleton
@ -10,4 +11,6 @@ import javax.inject.Singleton
interface AppComponent { interface AppComponent {
fun inject(main: App) fun inject(main: App)
fun appStateManager(): AppStateManager fun appStateManager(): AppStateManager
fun appPreferences(): AppPreferences
} }

View File

@ -1,5 +1,6 @@
package app.di package app.di
import app.AppPreferences
import app.di.modules.NetworkModule import app.di.modules.NetworkModule
import app.ui.components.TabInformation import app.ui.components.TabInformation
import dagger.Component import dagger.Component

View File

@ -52,12 +52,19 @@ class FileChangesWatcher @Inject constructor() {
while (watchService.take().also { key = it } != null) { while (watchService.take().also { key = it } != null) {
val events = key.pollEvents() val events = key.pollEvents()
println("Polled events on dir ${keys[key]}")
val dir = keys[key] ?: return@withContext val dir = keys[key] ?: return@withContext
val hasGitDirectoryChanged = dir.startsWith("$pathStr$systemSeparator.git$systemSeparator") val hasGitDirectoryChanged = dir.startsWith("$pathStr$systemSeparator.git$systemSeparator")
if(events.count() == 1) {
val fileChanged = events.first().context().toString()
val fullPathOfFileChanged = "$pathStr$systemSeparator.git$systemSeparator$fileChanged"
// Ignore COMMIT_EDITMSG changes
if(isGitMessageFile(pathStr, fullPathOfFileChanged))
return@withContext
}
println("Has git dir changed: $hasGitDirectoryChanged") println("Has git dir changed: $hasGitDirectoryChanged")
_changesNotifier.emit(hasGitDirectoryChanged) _changesNotifier.emit(hasGitDirectoryChanged)
@ -86,4 +93,10 @@ class FileChangesWatcher @Inject constructor() {
key.reset() key.reset()
} }
} }
private fun isGitMessageFile(repoPath: String, fullPathOfFileChanged: String): Boolean {
val gitDir = "$repoPath$systemSeparator.git${systemSeparator}"
return fullPathOfFileChanged == "${gitDir}COMMIT_EDITMSG" ||
fullPathOfFileChanged == "${gitDir}MERGE_MSG"
}
} }

View File

@ -14,12 +14,12 @@ import javax.inject.Inject
class LogManager @Inject constructor() { class LogManager @Inject constructor() {
suspend fun loadLog(git: Git, currentBranch: Ref?, hasUncommitedChanges: Boolean) = withContext(Dispatchers.IO) { suspend fun loadLog(git: Git, currentBranch: Ref?, hasUncommitedChanges: Boolean, commitsLimit: Int) = withContext(Dispatchers.IO) {
val commitList = GraphCommitList() val commitList = GraphCommitList()
val repositoryState = git.repository.repositoryState val repositoryState = git.repository.repositoryState
println("Repository state ${repositoryState.description}") println("Repository state ${repositoryState.description}")
if (currentBranch != null || repositoryState.isRebasing) { // Current branch is null when there is no log (new repo) or rebasing if (currentBranch != null || repositoryState.isRebasing) { // Current branch is null when there is no log (new repo) or rebasing
val logList = git.log().setMaxCount(2).call().toList() val logList = git.log().setMaxCount(1).call().toList()
val walk = GraphWalk(git.repository) val walk = GraphWalk(git.repository)
@ -36,7 +36,7 @@ class LogManager @Inject constructor() {
commitList.addUncommitedChangesGraphCommit(logList.first()) commitList.addUncommitedChangesGraphCommit(logList.first())
commitList.source(walk) commitList.source(walk)
commitList.fillTo(1000) // TODO: Limited commits to show to 1000, add a setting to let the user adjust this commitList.fillTo(commitsLimit)
} }
ensureActive() ensureActive()

View File

@ -7,10 +7,14 @@ import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject import javax.inject.Inject
class StashManager @Inject constructor() { class StashManager @Inject constructor() {
suspend fun stash(git: Git) = withContext(Dispatchers.IO) { suspend fun stash(git: Git, message: String?) = withContext(Dispatchers.IO) {
git git
.stashCreate() .stashCreate()
.setIncludeUntracked(true) .setIncludeUntracked(true)
.apply {
if (message != null)
setWorkingDirectoryMessage(message)
}
.call() .call()
} }

View File

@ -39,15 +39,10 @@ class StatusManager @Inject constructor(
} }
suspend fun stage(git: Git, statusEntry: StatusEntry) = withContext(Dispatchers.IO) { suspend fun stage(git: Git, statusEntry: StatusEntry) = withContext(Dispatchers.IO) {
if (statusEntry.statusType == StatusType.REMOVED) { git.add()
git.rm() .addFilepattern(statusEntry.filePath)
.addFilepattern(statusEntry.filePath) .setUpdate(statusEntry.statusType == StatusType.REMOVED)
.call() .call()
} else {
git.add()
.addFilepattern(statusEntry.filePath)
.call()
}
} }
suspend fun stageHunk(git: Git, diffEntry: DiffEntry, hunk: Hunk) = withContext(Dispatchers.IO) { suspend fun stageHunk(git: Git, diffEntry: DiffEntry, hunk: Hunk) = withContext(Dispatchers.IO) {
@ -160,7 +155,7 @@ class StatusManager @Inject constructor(
} }
splitted = splitted.mapIndexed { index, line -> splitted = splitted.mapIndexed { index, line ->
val lineWithBreak = line +lineDelimiter.orEmpty() val lineWithBreak = line + lineDelimiter.orEmpty()
if (index == splitted.count() - 1 && !content.endsWith(lineWithBreak)) { if (index == splitted.count() - 1 && !content.endsWith(lineWithBreak)) {
line line
@ -238,6 +233,12 @@ class StatusManager @Inject constructor(
git git
.add() .add()
.addFilepattern(".") .addFilepattern(".")
.setUpdate(true) // Modified and deleted files
.call()
git
.add()
.addFilepattern(".")
.setUpdate(false) // For newly added files
.call() .call()
} }

View File

@ -2,38 +2,75 @@ package app.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val primaryLight = Color(0xFF9FD1FF) val lightTheme = ColorsScheme(
val primary = Color(0xFF0070D8) primary = Color(0xFF0070D8),
val primaryDark = Color(0xFF014F97) primaryVariant = Color(0xFF0070D8),
val onPrimary = Color(0xFFFFFFFFF) onPrimary = Color(0xFFFFFFFFF),
val secondaryLight = Color(0xFF9c27b0) secondary = Color(0xFF9c27b0),
val secondaryDark = Color(0xFFe9c754) primaryText = Color(0xFF212934),
val mainText = Color(0xFF212934) secondaryText = Color(0xFF595858),
val mainTextDark = Color(0xFFFFFFFF) error = Color(0xFFc93838),
val secondaryText = Color(0xFF595858) onError = Color(0xFFFFFFFF),
val secondaryTextDark = Color(0xFFCCCBCB) background = Color(0xFFFFFFFF),
val borderColorLight = Color(0xFF989898) backgroundSelected = Color(0xC0cee1f2),
val borderColorDark = Color(0xFF989898) surface = Color(0xFFe9ecf7),
val errorColor = Color(0xFFc93838) headerBackground = Color(0xFFF4F6FA),
val onErrorColor = Color(0xFFFFFFFF) borderColor = Color(0xFF989898),
graphHeaderBackground = Color(0xFFF4F6FA),
addFile = Color(0xFF32A852),
deletedFile = Color(0xFFc93838),
modifiedFile = Color(0xFF0070D8),
conflictingFile = Color(0xFFFFB638),
dialogOverlay = Color(0xAA000000),
normalScrollbar = Color(0xFFCCCCCC),
hoverScrollbar = Color(0xFF0070D8),
)
val backgroundColorLight = Color(0xFFFFFFFF)
val backgroundColorSelectedLight = Color(0xFFcee1f2)
val backgroundColorDark = Color(0xFF0E1621)
val backgroundColorSelectedDark = Color(0xFF2f3640)
val surfaceColorLight = Color(0xFFe9ecf7)
val surfaceColorDark = Color(0xFF182533)
val headerBackgroundLight = Color(0xFFF4F6FA)
val graphHeaderBackgroundDark = Color(0xFF303132)
val headerBackgroundDark = Color(0xFF0a2b4a)
val addFileLight = Color(0xFF32A852) val darkBlueTheme = ColorsScheme(
val deleteFileLight = errorColor primary = Color(0xFF014F97),
val modifyFileLight = primary primaryVariant = Color(0xFF9FD1FF),
val conflictFileLight = Color(0xFFFFB638) onPrimary = Color(0xFFFFFFFFF),
secondary = Color(0xFFe9c754),
primaryText = Color(0xFFFFFFFF),
secondaryText = Color(0xFFCCCBCB),
error = Color(0xFFc93838),
onError = Color(0xFFFFFFFF),
background = Color(0xFF0E1621),
backgroundSelected = Color(0xFF2f3640),
surface = Color(0xFF182533),
headerBackground = Color(0xFF0a335c),
borderColor = Color(0xFF989898),
graphHeaderBackground = Color(0xFF303132),
addFile = Color(0xFF32A852),
deletedFile = Color(0xFFc93838),
modifiedFile = Color(0xFF0070D8),
conflictingFile = Color(0xFFFFB638),
dialogOverlay = Color(0xAA000000),
normalScrollbar = Color(0xFFCCCCCC),
hoverScrollbar = Color(0xFF888888)
)
val dialogBackgroundColor = Color(0xAA000000) val darkGrayTheme = ColorsScheme(
val unhoverScrollbarColorLight = Color.LightGray primary = Color(0xFF014F97),
val unhoverScrollbarColorDark = Color.Gray primaryVariant = Color(0xFFCDEAFF),
val hoverScrollbarColorLight = primary onPrimary = Color(0xFFFFFFFFF),
val hoverScrollbarColorDark = Color.LightGray secondary = Color(0xFFe9c754),
primaryText = Color(0xFFFFFFFF),
secondaryText = Color(0xFFCCCBCB),
error = Color(0xFFc93838),
onError = Color(0xFFFFFFFF),
background = Color(0xFF16181F),
backgroundSelected = Color(0xFF32373e),
surface = Color(0xFF212731),
headerBackground = Color(0xFF21303d),
borderColor = Color(0xFF989898),
graphHeaderBackground = Color(0xFF303132),
addFile = Color(0xFF32A852),
deletedFile = Color(0xFFc93838),
modifiedFile = Color(0xFF0070D8),
conflictingFile = Color(0xFFFFB638),
dialogOverlay = Color(0xAA000000),
normalScrollbar = Color(0xFFCCCCCC),
hoverScrollbar = Color(0xFF888888)
)

View File

@ -0,0 +1,47 @@
package app.theme
import androidx.compose.material.Colors
import androidx.compose.ui.graphics.Color
data class ColorsScheme(
val primary: Color,
val primaryVariant: Color,
val onPrimary: Color,
val secondary: Color,
val primaryText: Color,
val secondaryText: Color,
val error: Color,
val onError: Color,
val background: Color,
val backgroundSelected: Color,
val surface: Color,
val headerBackground: Color,
val onHeader: Color = primaryText,
val borderColor: Color,
val graphHeaderBackground: Color,
val addFile: Color,
val deletedFile: Color,
val modifiedFile: Color,
val conflictingFile: Color,
val dialogOverlay: Color,
val normalScrollbar: Color,
val hoverScrollbar: Color,
) {
fun toComposeColors(): Colors {
return Colors(
primary = this.primary,
primaryVariant = this.primaryVariant,
secondary = this.secondary,
secondaryVariant = this.secondary,
background = this.background,
surface = this.surface,
error = this.error,
onPrimary = this.onPrimary,
onSecondary = this.onPrimary,
onBackground = this.primaryText,
onSurface = this.primaryText,
onError = this.onError,
isLight = true, // todo what is this used for? Hardcoded value for now
)
}
}

View File

@ -0,0 +1,29 @@
package app.theme
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.MaterialTheme
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
@Composable
fun textFieldColors() = TextFieldDefaults.textFieldColors(
cursorColor = MaterialTheme.colors.primaryVariant,
focusedIndicatorColor = MaterialTheme.colors.primaryVariant,
focusedLabelColor = MaterialTheme.colors.primaryVariant,
backgroundColor = MaterialTheme.colors.background,
textColor = MaterialTheme.colors.primaryTextColor,
)
@Composable
fun outlinedTextFieldColors() = TextFieldDefaults.outlinedTextFieldColors(
cursorColor = MaterialTheme.colors.primaryVariant,
focusedBorderColor = MaterialTheme.colors.primaryVariant,
focusedLabelColor = MaterialTheme.colors.primaryVariant,
backgroundColor = MaterialTheme.colors.background,
textColor = MaterialTheme.colors.primaryTextColor,
)
@Composable
fun textButtonColors() = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colors.primaryVariant
)

View File

@ -1,44 +1,27 @@
@file:Suppress("unused")
package app.theme package app.theme
import androidx.compose.material.Colors import androidx.compose.material.Colors
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import app.DropDownOption import app.DropDownOption
private val DarkColorPalette = darkColors( private var appTheme: ColorsScheme = darkGrayTheme
primary = primaryLight,
primaryVariant = primaryDark,
secondary = secondaryDark,
surface = surfaceColorDark,
background = backgroundColorDark,
error = errorColor,
onError = onErrorColor,
onPrimary = onPrimary,
)
private val LightColorPalette = lightColors(
primary = primary,
primaryVariant = primaryDark,
secondary = secondaryLight,
background = backgroundColorLight,
surface = surfaceColorLight,
error = errorColor,
onError = onErrorColor,
onPrimary = onPrimary,
)
@Composable @Composable
fun AppTheme(theme: Themes = Themes.LIGHT, content: @Composable() () -> Unit) { fun AppTheme(selectedTheme: Themes = Themes.DARK, content: @Composable() () -> Unit) {
val colors = when (theme) { val theme = when (selectedTheme) {
Themes.LIGHT -> LightColorPalette Themes.LIGHT -> lightTheme
Themes.DARK -> DarkColorPalette Themes.DARK -> darkBlueTheme
Themes.DARK_GRAY -> darkGrayTheme
} }
appTheme = theme
MaterialTheme( MaterialTheme(
colors = colors, colors = theme.toComposeColors(),
content = content, content = content,
typography = typography, typography = typography,
) )
@ -46,99 +29,71 @@ fun AppTheme(theme: Themes = Themes.LIGHT, content: @Composable() () -> Unit) {
@get:Composable @get:Composable
val Colors.backgroundSelected: Color val Colors.backgroundSelected: Color
get() = if (isLight) backgroundColorSelectedLight else backgroundColorSelectedDark get() = appTheme.backgroundSelected
@get:Composable @get:Composable
val Colors.primaryTextColor: Color val Colors.primaryTextColor: Color
get() = if (isLight) mainText else mainTextDark get() = appTheme.primaryText
@get:Composable
val Colors.halfPrimary: Color
get() = primary.copy(alpha = 0.8f)
@get:Composable
val Colors.inversePrimaryTextColor: Color
get() = if (isLight) mainTextDark else mainText
@get:Composable @get:Composable
val Colors.secondaryTextColor: Color val Colors.secondaryTextColor: Color
get() = if (isLight) secondaryText else secondaryTextDark get() = appTheme.secondaryText
@get:Composable @get:Composable
val Colors.borderColor: Color val Colors.borderColor: Color
get() = if (isLight) get() = appTheme.borderColor
borderColorLight
else
borderColorDark
@get:Composable @get:Composable
val Colors.headerBackground: Color val Colors.headerBackground: Color
get() { get() = appTheme.headerBackground
return if (isLight)
headerBackgroundLight
else
headerBackgroundDark
}
@get:Composable @get:Composable
val Colors.graphHeaderBackground: Color val Colors.graphHeaderBackground: Color
get() { get() = appTheme.graphHeaderBackground
return if (isLight)
headerBackgroundLight
else
graphHeaderBackgroundDark
}
@get:Composable @get:Composable
val Colors.addFile: Color val Colors.addFile: Color
get() = addFileLight get() = appTheme.addFile
@get:Composable @get:Composable
val Colors.deleteFile: Color val Colors.deleteFile: Color
get() = deleteFileLight get() = appTheme.deletedFile
@get:Composable @get:Composable
val Colors.modifyFile: Color val Colors.modifyFile: Color
get() = modifyFileLight get() = appTheme.modifiedFile
@get:Composable @get:Composable
val Colors.conflictFile: Color val Colors.conflictFile: Color
get() = conflictFileLight get() = appTheme.conflictingFile
@get:Composable @get:Composable
val Colors.headerText: Color val Colors.headerText: Color
get() = if (isLight) primary else mainTextDark get() = appTheme.onHeader
val Colors.tabColorActive: Color
get() = if (isLight) surfaceColorLight else surfaceColorDark
val Colors.tabColorInactive: Color
get() = if (isLight) backgroundColorLight else backgroundColorDark
val Colors.stageButton: Color val Colors.stageButton: Color
get() = if (isLight) primary else primaryDark get() = appTheme.primary
val Colors.unstageButton: Color val Colors.unstageButton: Color
get() = error get() = appTheme.error
val Colors.abortButton: Color val Colors.abortButton: Color
get() = error get() = appTheme.error
val Colors.confirmationButton: Color val Colors.scrollbarNormal: Color
get() = if (isLight) primary else primaryDark get() = appTheme.normalScrollbar
val Colors.scrollbarUnhover: Color
get() = if (isLight) unhoverScrollbarColorLight else unhoverScrollbarColorDark
val Colors.scrollbarHover: Color val Colors.scrollbarHover: Color
get() = if (isLight) hoverScrollbarColorLight else hoverScrollbarColorDark get() = appTheme.hoverScrollbar
val Colors.dialogOverlay: Color
get() = appTheme.dialogOverlay
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");
override val optionName: String override val optionName: String
get() = displayName get() = displayName
@ -147,4 +102,5 @@ enum class Themes(val displayName: String) : DropDownOption {
val themesList = listOf( val themesList = listOf(
Themes.LIGHT, Themes.LIGHT,
Themes.DARK, Themes.DARK,
Themes.DARK_GRAY,
) )

View File

@ -61,7 +61,8 @@ fun AppTab(
LinearProgressIndicator( LinearProgressIndicator(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.alpha(linearProgressAlpha) .alpha(linearProgressAlpha),
color = MaterialTheme.colors.primaryVariant
) )
CredentialsDialog(tabViewModel) CredentialsDialog(tabViewModel)

View File

@ -4,6 +4,7 @@ package app.ui
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
@ -11,10 +12,17 @@ import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.pointer.PointerIconDefaults import androidx.compose.ui.input.pointer.PointerIconDefaults
import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@ -24,6 +32,7 @@ import androidx.compose.ui.unit.sp
import app.extensions.handMouseClickable import app.extensions.handMouseClickable
import app.extensions.lineAt import app.extensions.lineAt
import app.extensions.toStringWithSpaces import app.extensions.toStringWithSpaces
import app.theme.headerBackground
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.ui.components.PrimaryButton import app.ui.components.PrimaryButton
import app.ui.components.ScrollableLazyColumn import app.ui.components.ScrollableLazyColumn
@ -37,7 +46,25 @@ fun Blame(
onSelectCommit: (RevCommit) -> Unit, onSelectCommit: (RevCommit) -> Unit,
onClose: () -> Unit, onClose: () -> Unit,
) { ) {
Column {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Column(
modifier = Modifier
.focusRequester(focusRequester)
.focusable()
.onPreviewKeyEvent {
if (it.key == Key.Escape) {
onClose()
true
} else
false
},
) {
Header(filePath, onClose = onClose) Header(filePath, onClose = onClose)
SelectionContainer { SelectionContainer {
ScrollableLazyColumn( ScrollableLazyColumn(
@ -130,7 +157,7 @@ fun MinimizedBlame(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(50.dp) .height(52.dp)
.background(MaterialTheme.colors.surface), .background(MaterialTheme.colors.surface),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@ -181,9 +208,9 @@ private fun Header(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(50.dp) .height(40.dp)
.padding(start = 8.dp, end = 8.dp, top = 8.dp) .background(MaterialTheme.colors.headerBackground)
.background(MaterialTheme.colors.surface), .padding(start = 8.dp, end = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(

View File

@ -114,7 +114,7 @@ private fun BranchLineEntry(
painter = painterResource("location.svg"), painter = painterResource("location.svg"),
contentDescription = null, contentDescription = null,
modifier = Modifier.padding(horizontal = 4.dp), modifier = Modifier.padding(horizontal = 4.dp),
tint = MaterialTheme.colors.primary, tint = MaterialTheme.colors.primaryVariant,
) )
} }
} }

View File

@ -42,7 +42,7 @@ fun CommitChanges(
when (val commitChangesStatus = commitChangesStatusState.value) { when (val commitChangesStatus = commitChangesStatusState.value) {
CommitChangesStatus.Loading -> { CommitChangesStatus.Loading -> {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant)
} }
is CommitChangesStatus.Loaded -> { is CommitChangesStatus.Loaded -> {
CommitChangesView( CommitChangesView(
@ -75,7 +75,7 @@ fun CommitChangesView(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp), .padding(end = 8.dp),
) { ) {
SelectionContainer { SelectionContainer {
Text( Text(
@ -100,7 +100,7 @@ fun CommitChangesView(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f, fill = true) .weight(1f, fill = true)
.padding(horizontal = 8.dp, vertical = 8.dp) .padding(end = 8.dp, top = 8.dp, bottom = 8.dp)
.background(MaterialTheme.colors.background) .background(MaterialTheme.colors.background)
) { ) {
Text( Text(

View File

@ -4,6 +4,7 @@ package app.ui
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@ -13,14 +14,17 @@ import androidx.compose.material.IconButton
import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.pointer.PointerIconDefaults import androidx.compose.ui.input.pointer.PointerIconDefaults
import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.res.loadImageBitmap import androidx.compose.ui.res.loadImageBitmap
@ -38,6 +42,7 @@ import app.git.diff.DiffResult
import app.git.diff.Hunk import app.git.diff.Hunk
import app.git.diff.Line import app.git.diff.Line
import app.git.diff.LineType import app.git.diff.LineType
import app.theme.headerBackground
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.theme.stageButton import app.theme.stageButton
import app.theme.unstageButton import app.theme.unstageButton
@ -58,11 +63,25 @@ fun Diff(
) { ) {
val diffResultState = diffViewModel.diffResult.collectAsState() val diffResultState = diffViewModel.diffResult.collectAsState()
val viewDiffResult = diffResultState.value ?: return val viewDiffResult = diffResultState.value ?: return
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Column( Column(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colors.background) .background(MaterialTheme.colors.background)
.fillMaxSize() .fillMaxSize()
.focusRequester(focusRequester)
.focusable()
.onPreviewKeyEvent {
if (it.key == Key.Escape) {
onCloseDiffView()
true
} else
false
}
) { ) {
when (viewDiffResult) { when (viewDiffResult) {
ViewDiffResult.DiffNotFound -> { onCloseDiffView() } ViewDiffResult.DiffNotFound -> { onCloseDiffView() }
@ -97,7 +116,7 @@ fun Diff(
} }
} }
ViewDiffResult.Loading, ViewDiffResult.None -> { ViewDiffResult.Loading, ViewDiffResult.None -> {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant)
} }
} }
@ -303,9 +322,9 @@ fun DiffHeader(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(50.dp) .height(40.dp)
.padding(start = 8.dp, end = 8.dp, top = 8.dp) .background(MaterialTheme.colors.headerBackground)
.background(MaterialTheme.colors.surface), .padding(start = 8.dp, end = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
val filePath = if (diffEntry.newPath != "/dev/null") val filePath = if (diffEntry.newPath != "/dev/null")
@ -322,7 +341,7 @@ fun DiffHeader(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
if(diffEntryType is DiffEntryType.UncommitedDiff) { if (diffEntryType is DiffEntryType.UncommitedDiff) {
val buttonText: String val buttonText: String
val color: Color val color: Color
@ -404,7 +423,10 @@ fun DiffLine(
} }
Text( Text(
text = line.text.replace("\t", " "), // this replace is a workaround until this issue gets fixed https://github.com/JetBrains/compose-jb/issues/615 text = line.text.replace(
"\t",
" "
), // TODO this replace is a workaround until this issue gets fixed https://github.com/JetBrains/compose-jb/issues/615
modifier = Modifier modifier = Modifier
.padding(start = 8.dp) .padding(start = 8.dp)
.fillMaxSize(), .fillMaxSize(),

View File

@ -4,20 +4,24 @@ package app.ui
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.pointer.PointerIconDefaults import androidx.compose.ui.input.pointer.PointerIconDefaults
import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@ -27,6 +31,7 @@ import app.extensions.handMouseClickable
import app.extensions.toSmartSystemString import app.extensions.toSmartSystemString
import app.extensions.toSystemDateTimeString import app.extensions.toSystemDateTimeString
import app.git.diff.DiffResult import app.git.diff.DiffResult
import app.theme.headerBackground
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.theme.secondaryTextColor import app.theme.secondaryTextColor
import app.ui.components.AvatarImage import app.ui.components.AvatarImage
@ -45,9 +50,24 @@ fun FileHistory(
) { ) {
val historyState by historyViewModel.historyState.collectAsState() val historyState by historyViewModel.historyState.collectAsState()
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.focusRequester(focusRequester)
.focusable()
.onKeyEvent {
if (it.key == Key.Escape) {
onClose()
true
} else
false
},
) { ) {
Header(filePath = historyState.filePath, onClose = onClose) Header(filePath = historyState.filePath, onClose = onClose)
@ -67,9 +87,9 @@ private fun Header(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(50.dp) .height(40.dp)
.padding(start = 8.dp, end = 8.dp, top = 8.dp) .background(MaterialTheme.colors.headerBackground)
.background(MaterialTheme.colors.surface), .padding(start = 8.dp, end = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(

View File

@ -32,6 +32,7 @@ fun Menu(
menuViewModel: MenuViewModel, menuViewModel: MenuViewModel,
onRepositoryOpen: () -> Unit, onRepositoryOpen: () -> Unit,
onCreateBranch: () -> Unit, onCreateBranch: () -> Unit,
onStashWithMessage: () -> Unit,
) { ) {
var showAdditionalOptionsDropDownMenu by remember { mutableStateOf(false) } var showAdditionalOptionsDropDownMenu by remember { mutableStateOf(false) }
@ -53,6 +54,7 @@ fun Menu(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
ExtendedMenuButton( ExtendedMenuButton(
modifier = Modifier.padding(end = 8.dp),
title = "Pull", title = "Pull",
icon = painterResource("download.svg"), icon = painterResource("download.svg"),
onClick = { menuViewModel.pull() }, onClick = { menuViewModel.pull() },
@ -80,7 +82,7 @@ fun Menu(
) )
) )
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(24.dp))
MenuButton( MenuButton(
title = "Branch", title = "Branch",
@ -90,12 +92,16 @@ fun Menu(
}, },
) )
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(24.dp))
MenuButton( ExtendedMenuButton(
modifier = Modifier.padding(end = 8.dp),
title = "Stash", title = "Stash",
icon = painterResource("stash.svg"), icon = painterResource("stash.svg"),
onClick = { menuViewModel.stash() }, onClick = { menuViewModel.stash() },
extendedListItems = stashContextMenuItems(
onStashWithMessage = onStashWithMessage
)
) )
MenuButton( MenuButton(
@ -146,16 +152,15 @@ fun MenuButton(
onClick: () -> Unit onClick: () -> Unit
) { ) {
val iconColor = if (enabled) { val iconColor = if (enabled) {
MaterialTheme.colors.primary MaterialTheme.colors.primaryVariant
} else { } else {
MaterialTheme.colors.secondaryVariant MaterialTheme.colors.secondaryVariant //todo this color isn't specified anywhere
} }
Box( Box(
modifier = modifier modifier = modifier
.padding(horizontal = 2.dp)
.handMouseClickable { if (enabled) onClick() } .handMouseClickable { if (enabled) onClick() }
.border(ButtonDefaults.outlinedBorder, RoundedCornerShape(3.dp)) .border(ButtonDefaults.outlinedBorder, RoundedCornerShape(4.dp))
.padding(vertical = 8.dp, horizontal = 16.dp), .padding(vertical = 8.dp, horizontal = 16.dp),
) { ) {
Row( Row(
@ -189,46 +194,42 @@ fun ExtendedMenuButton(
extendedListItems: List<DropDownContentData>, extendedListItems: List<DropDownContentData>,
) { ) {
val iconColor = if (enabled) { val iconColor = if (enabled) {
MaterialTheme.colors.primary MaterialTheme.colors.primaryVariant
} else { } else {
MaterialTheme.colors.secondaryVariant MaterialTheme.colors.secondaryVariant
} }
var showDropDownMenu by remember { mutableStateOf(false) } var showDropDownMenu by remember { mutableStateOf(false) }
Row(modifier = Modifier.height(IntrinsicSize.Min)) { Row(modifier = modifier.height(IntrinsicSize.Min)) {
Box( Row(
modifier = modifier modifier = Modifier
.handMouseClickable { if (enabled) onClick() } .handMouseClickable { if (enabled) onClick() }
.border(ButtonDefaults.outlinedBorder, RoundedCornerShape(topStart = 3.dp, bottomStart = 3.dp)) .border(ButtonDefaults.outlinedBorder, RoundedCornerShape(topStart = 4.dp, bottomStart = 4.dp))
.padding(vertical = 8.dp, horizontal = 16.dp), .padding(vertical = 8.dp, horizontal = 16.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) { ) {
Row( Image(
horizontalArrangement = Arrangement.Center, painter = icon,
verticalAlignment = Alignment.CenterVertically, contentDescription = title,
) { modifier = Modifier
Image( .padding(horizontal = 4.dp)
painter = icon, .size(24.dp),
contentDescription = title, colorFilter = ColorFilter.tint(iconColor),
modifier = Modifier )
.padding(horizontal = 4.dp) Text(
.size(24.dp), text = title,
colorFilter = ColorFilter.tint(iconColor), fontSize = 12.sp,
) color = MaterialTheme.colors.primaryTextColor
Text( )
text = title,
fontSize = 12.sp,
color = MaterialTheme.colors.primaryTextColor
)
}
} }
Box( Box(
modifier = modifier modifier = Modifier
.padding(end = 8.dp)
.width(20.dp) .width(20.dp)
.fillMaxHeight() .fillMaxHeight()
.border(ButtonDefaults.outlinedBorder, RoundedCornerShape(topEnd = 3.dp, bottomEnd = 3.dp)) .border(ButtonDefaults.outlinedBorder, RoundedCornerShape(topEnd = 4.dp, bottomEnd = 4.dp))
.handMouseClickable { .handMouseClickable {
showDropDownMenu = true showDropDownMenu = true
}, },
@ -263,7 +264,7 @@ fun IconMenuButton(
onClick: () -> Unit onClick: () -> Unit
) { ) {
val iconColor = if (enabled) { val iconColor = if (enabled) {
MaterialTheme.colors.primary MaterialTheme.colors.primaryVariant
} else { } else {
MaterialTheme.colors.secondaryVariant MaterialTheme.colors.secondaryVariant
} }

View File

@ -11,7 +11,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import app.theme.outlinedTextFieldColors
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.theme.textButtonColors
import app.ui.components.PrimaryButton import app.ui.components.PrimaryButton
import app.ui.components.ScrollableLazyColumn import app.ui.components.ScrollableLazyColumn
import app.viewmodels.RebaseInteractiveState import app.viewmodels.RebaseInteractiveState
@ -88,7 +90,8 @@ fun RebaseStateLoaded(
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.padding(end = 8.dp),
onClick = { onClick = {
onCancel() onCancel()
} },
colors = textButtonColors(),
) { ) {
Text("Cancel") Text("Cancel")
} }
@ -139,7 +142,7 @@ fun RebaseCommit(
newMessage = it newMessage = it
onMessageChanged(it) onMessageChanged(it)
}, },
colors = TextFieldDefaults.textFieldColors(backgroundColor = MaterialTheme.colors.background), colors = outlinedTextFieldColors(),
textStyle = TextStyle.Default.copy(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor), textStyle = TextStyle.Default.copy(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
) )

View File

@ -2,6 +2,7 @@
package app.ui package app.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -9,12 +10,17 @@ 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.Modifier 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 import androidx.compose.ui.unit.dp
import app.git.DiffEntryType import app.git.DiffEntryType
import app.theme.borderColor import app.theme.borderColor
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.ui.dialogs.NewBranchDialog import app.ui.dialogs.NewBranchDialog
import app.ui.dialogs.RebaseInteractive import app.ui.dialogs.RebaseInteractive
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
@ -23,7 +29,9 @@ 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
import org.jetbrains.compose.splitpane.HorizontalSplitPane import org.jetbrains.compose.splitpane.HorizontalSplitPane
import org.jetbrains.compose.splitpane.SplitterScope
import org.jetbrains.compose.splitpane.rememberSplitPaneState import org.jetbrains.compose.splitpane.rememberSplitPaneState
import java.awt.Cursor
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class) @OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
@ -36,6 +44,7 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) {
val showHistory by tabViewModel.showHistory.collectAsState() val showHistory by tabViewModel.showHistory.collectAsState()
var showNewBranchDialog by remember { mutableStateOf(false) } var showNewBranchDialog by remember { mutableStateOf(false) }
var showStashWithMessageDialog by remember { mutableStateOf(false) }
if (showNewBranchDialog) { if (showNewBranchDialog) {
NewBranchDialog( NewBranchDialog(
@ -47,6 +56,16 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) {
showNewBranchDialog = false showNewBranchDialog = false
} }
) )
} else if (showStashWithMessageDialog) {
StashWithMessageDialog(
onReject = {
showStashWithMessageDialog = false
},
onAccept = { stashMessage ->
tabViewModel.menuViewModel.stashWithMessage(stashMessage)
showStashWithMessageDialog = false
}
)
} }
Column { Column {
@ -65,7 +84,8 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) {
onRepositoryOpen = { onRepositoryOpen = {
openRepositoryDialog(tabViewModel = tabViewModel) openRepositoryDialog(tabViewModel = tabViewModel)
}, },
onCreateBranch = { showNewBranchDialog = true } onCreateBranch = { showNewBranchDialog = true },
onStashWithMessage = { showStashWithMessageDialog = true },
) )
RepoContent(tabViewModel, diffSelected, selectedItem, repositoryState, blameState, showHistory) RepoContent(tabViewModel, diffSelected, selectedItem, repositoryState, blameState, showHistory)
@ -83,10 +103,10 @@ fun RepoContent(
blameState: BlameState, blameState: BlameState,
showHistory: Boolean, showHistory: Boolean,
) { ) {
if(showHistory) { if (showHistory) {
val historyViewModel = tabViewModel.historyViewModel val historyViewModel = tabViewModel.historyViewModel
if(historyViewModel != null) { if (historyViewModel != null) {
FileHistory( FileHistory(
historyViewModel = historyViewModel, historyViewModel = historyViewModel,
onClose = { onClose = {
@ -117,11 +137,9 @@ fun MainContentView(
) { ) {
Row { Row {
HorizontalSplitPane { HorizontalSplitPane {
first(minSize = 200.dp) { first(minSize = 250.dp) {
Column( Column(
modifier = Modifier modifier = Modifier
.widthIn(min = 300.dp)
.weight(0.15f)
.fillMaxHeight() .fillMaxHeight()
) { ) {
Branches( Branches(
@ -139,6 +157,10 @@ fun MainContentView(
} }
} }
splitter {
this.repositorySplitter()
}
second { second {
HorizontalSplitPane( HorizontalSplitPane(
splitPaneState = rememberSplitPaneState(0.9f) splitPaneState = rememberSplitPaneState(0.9f)
@ -147,11 +169,6 @@ fun MainContentView(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.border(
width = 2.dp,
color = MaterialTheme.colors.borderColor,
shape = RoundedCornerShape(4.dp)
)
) { ) {
if (blameState is BlameState.Loaded && !blameState.isMinimized) { if (blameState is BlameState.Loaded && !blameState.isMinimized) {
Blame( Blame(
@ -191,6 +208,10 @@ fun MainContentView(
} }
} }
splitter {
this.repositorySplitter()
}
second(minSize = 300.dp) { second(minSize = 300.dp) {
Box( Box(
modifier = Modifier modifier = Modifier
@ -246,6 +267,27 @@ fun MainContentView(
} }
} }
fun SplitterScope.repositorySplitter() {
visiblePart {
Box(
Modifier
.width(8.dp)
.fillMaxHeight()
.background(Color.Transparent)
)
}
handle {
Box(
Modifier
.markAsHandle()
.pointerHoverIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR)))
.background(Color.Transparent)
.width(8.dp)
.fillMaxHeight()
)
}
}
sealed class SelectedItem { sealed class SelectedItem {
object None : SelectedItem() object None : SelectedItem()
object UncommitedChanges : SelectedItem() object UncommitedChanges : SelectedItem()

View File

@ -40,6 +40,7 @@ import app.ui.components.SecondaryButton
import app.ui.context_menu.* import app.ui.context_menu.*
import app.viewmodels.StageStatus import app.viewmodels.StageStatus
import app.viewmodels.StatusViewModel import app.viewmodels.StatusViewModel
import kotlinx.coroutines.flow.collect
import org.eclipse.jgit.lib.RepositoryState import org.eclipse.jgit.lib.RepositoryState
@Composable @Composable
@ -53,7 +54,7 @@ fun UncommitedChanges(
onHistoryFile: (String) -> Unit, onHistoryFile: (String) -> Unit,
) { ) {
val stageStatusState = statusViewModel.stageStatus.collectAsState() val stageStatusState = statusViewModel.stageStatus.collectAsState()
var commitMessage by remember { mutableStateOf(statusViewModel.savedCommitMessage) } var commitMessage by remember { mutableStateOf(statusViewModel.savedCommitMessage.message) }
val stageStatus = stageStatusState.value val stageStatus = stageStatusState.value
val staged: List<StatusEntry> val staged: List<StatusEntry>
@ -70,31 +71,36 @@ fun UncommitedChanges(
val doCommit = { amend: Boolean -> val doCommit = { amend: Boolean ->
statusViewModel.commit(commitMessage, amend) statusViewModel.commit(commitMessage, amend)
onStagedDiffEntrySelected(null) onStagedDiffEntrySelected(null)
statusViewModel.savedCommitMessage = ""
commitMessage = "" commitMessage = ""
} }
val canCommit = commitMessage.isNotEmpty() && staged.isNotEmpty() val canCommit = commitMessage.isNotEmpty() && staged.isNotEmpty()
val canAmend = (commitMessage.isNotEmpty() || staged.isNotEmpty()) && statusViewModel.hasPreviousCommits val canAmend = (commitMessage.isNotEmpty() || staged.isNotEmpty()) && statusViewModel.hasPreviousCommits
LaunchedEffect(Unit) {
statusViewModel.commitMessageChangesFlow.collect { newCommitMessage ->
commitMessage = newCommitMessage
}
}
Column { Column {
AnimatedVisibility( AnimatedVisibility(
visible = stageStatus is StageStatus.Loading, visible = stageStatus is StageStatus.Loading,
enter = fadeIn(), enter = fadeIn(),
exit = fadeOut(), exit = fadeOut(),
) { ) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant)
} }
EntriesList( EntriesList(
modifier = Modifier modifier = Modifier
.padding(start = 8.dp, end = 8.dp, bottom = 4.dp) .padding(end = 8.dp, bottom = 4.dp)
.weight(5f) .weight(5f)
.fillMaxWidth(), .fillMaxWidth(),
title = "Staged", title = "Staged",
allActionTitle = "Unstage all", allActionTitle = "Unstage all",
actionTitle = "Unstage", actionTitle = "Unstage",
selectedEntryType = if(selectedEntryType is DiffEntryType.StagedDiff) selectedEntryType else null, selectedEntryType = if (selectedEntryType is DiffEntryType.StagedDiff) selectedEntryType else null,
actionColor = MaterialTheme.colors.unstageButton, actionColor = MaterialTheme.colors.unstageButton,
statusEntries = staged, statusEntries = staged,
onDiffEntrySelected = onStagedDiffEntrySelected, onDiffEntrySelected = onStagedDiffEntrySelected,
@ -117,12 +123,12 @@ fun UncommitedChanges(
EntriesList( EntriesList(
modifier = Modifier modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 4.dp) .padding(end = 8.dp, top = 8.dp)
.weight(5f) .weight(5f)
.fillMaxWidth(), .fillMaxWidth(),
title = "Unstaged", title = "Unstaged",
actionTitle = "Stage", actionTitle = "Stage",
selectedEntryType = if(selectedEntryType is DiffEntryType.UnstagedDiff) selectedEntryType else null, selectedEntryType = if (selectedEntryType is DiffEntryType.UnstagedDiff) selectedEntryType else null,
actionColor = MaterialTheme.colors.stageButton, actionColor = MaterialTheme.colors.stageButton,
statusEntries = unstaged, statusEntries = unstaged,
onDiffEntrySelected = onUnstagedDiffEntrySelected, onDiffEntrySelected = onUnstagedDiffEntrySelected,
@ -149,7 +155,7 @@ fun UncommitedChanges(
Column( Column(
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(top = 8.dp, bottom = 8.dp, end = 8.dp)
.run { .run {
// When rebasing, we don't need a fixed size as we don't show the message TextField // When rebasing, we don't need a fixed size as we don't show the message TextField
if (!repositoryState.isRebasing) { if (!repositoryState.isRebasing) {
@ -175,23 +181,30 @@ fun UncommitedChanges(
value = commitMessage, value = commitMessage,
onValueChange = { onValueChange = {
commitMessage = it commitMessage = it
statusViewModel.savedCommitMessage = it
statusViewModel.updateCommitMessage(it)
}, },
label = { Text("Write your commit message here", fontSize = 14.sp) }, label = { Text("Write your commit message here", fontSize = 14.sp) },
colors = TextFieldDefaults.textFieldColors(backgroundColor = MaterialTheme.colors.background), colors = textFieldColors(),
textStyle = TextStyle.Default.copy(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor), textStyle = TextStyle.Default.copy(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
) )
when { when {
repositoryState.isMerging -> MergeButtons( repositoryState.isMerging -> MergeButtons(
haveConflictsBeenSolved = unstaged.isEmpty(), haveConflictsBeenSolved = unstaged.isEmpty(),
onAbort = { statusViewModel.abortMerge() }, onAbort = {
statusViewModel.abortMerge()
statusViewModel.updateCommitMessage("")
},
onMerge = { doCommit(false) } onMerge = { doCommit(false) }
) )
repositoryState.isRebasing -> RebasingButtons( repositoryState.isRebasing -> RebasingButtons(
canContinue = staged.isNotEmpty() || unstaged.isNotEmpty(), canContinue = staged.isNotEmpty() || unstaged.isNotEmpty(),
haveConflictsBeenSolved = unstaged.isEmpty(), haveConflictsBeenSolved = unstaged.isEmpty(),
onAbort = { statusViewModel.abortRebase() }, onAbort = {
statusViewModel.abortRebase()
statusViewModel.updateCommitMessage("")
},
onContinue = { statusViewModel.continueRebase() }, onContinue = { statusViewModel.continueRebase() },
onSkip = { statusViewModel.skipRebase() }, onSkip = { statusViewModel.skipRebase() },
) )
@ -237,7 +250,7 @@ fun UncommitedChangesButtons(
modifier = Modifier modifier = Modifier
.height(40.dp) .height(40.dp)
.clip(MaterialTheme.shapes.small.copy(topStart = CornerSize(0.dp), bottomStart = CornerSize(0.dp))) .clip(MaterialTheme.shapes.small.copy(topStart = CornerSize(0.dp), bottomStart = CornerSize(0.dp)))
.background(MaterialTheme.colors.confirmationButton) .background(MaterialTheme.colors.primary)
.handMouseClickable { showDropDownMenu = true } .handMouseClickable { showDropDownMenu = true }
) { ) {
Icon( Icon(
@ -367,7 +380,7 @@ fun ConfirmationButton(
enabled = enabled, enabled = enabled,
shape = shape, shape = shape,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.confirmationButton, backgroundColor = MaterialTheme.colors.primary,
contentColor = Color.White contentColor = Color.White
) )
) { ) {
@ -502,7 +515,7 @@ private fun FileEntry(
tint = statusEntry.iconColor, tint = statusEntry.iconColor,
) )
if(statusEntry.parentDirectoryPath.isNotEmpty()) { if (statusEntry.parentDirectoryPath.isNotEmpty()) {
Text( Text(
text = statusEntry.parentDirectoryPath, text = statusEntry.parentDirectoryPath,
modifier = Modifier.weight(1f, fill = false), modifier = Modifier.weight(1f, fill = false),

View File

@ -26,6 +26,7 @@ import app.extensions.dirPath
import app.extensions.openUrlInBrowser import app.extensions.openUrlInBrowser
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.theme.secondaryTextColor import app.theme.secondaryTextColor
import app.theme.textButtonColors
import app.ui.dialogs.AppInfoDialog import app.ui.dialogs.AppInfoDialog
import app.ui.dialogs.CloneDialog import app.ui.dialogs.CloneDialog
import app.updates.Update import app.updates.Update
@ -225,13 +226,13 @@ fun RecentRepositories(appStateManager: AppStateManager, tabViewModel: TabViewMo
TextButton( TextButton(
onClick = { onClick = {
tabViewModel.openRepository(repo) tabViewModel.openRepository(repo)
} },
colors = textButtonColors(),
) { ) {
Text( Text(
text = repoDirName, text = repoDirName,
fontSize = 14.sp, fontSize = 14.sp,
maxLines = 1, maxLines = 1,
color = MaterialTheme.colors.primary,
) )
} }
@ -270,12 +271,13 @@ fun ButtonTile(
.size(24.dp), .size(24.dp),
painter = painter, painter = painter,
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colors.primary), colorFilter = ColorFilter.tint(MaterialTheme.colors.primaryVariant),
) )
Text( Text(
text = title, text = title,
maxLines = 1, maxLines = 1,
color = MaterialTheme.colors.primaryVariant,
) )
} }
} }
@ -286,12 +288,13 @@ fun IconTextButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String, title: String,
painter: Painter, painter: Painter,
iconColor: Color = MaterialTheme.colors.primary, iconColor: Color = MaterialTheme.colors.primaryVariant,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
TextButton( TextButton(
onClick = onClick, onClick = onClick,
modifier = modifier.size(width = 280.dp, height = 40.dp) modifier = modifier.size(width = 280.dp, height = 40.dp),
colors = textButtonColors(),
) { ) {
Row( Row(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),

View File

@ -21,7 +21,7 @@ fun PrimaryButton(
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = enabled,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primaryVariant, backgroundColor = MaterialTheme.colors.primary,
contentColor = textColor contentColor = textColor
), ),
) { ) {

View File

@ -28,8 +28,6 @@ import app.di.AppComponent
import app.di.DaggerTabComponent import app.di.DaggerTabComponent
import app.extensions.handMouseClickable import app.extensions.handMouseClickable
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.theme.tabColorActive
import app.theme.tabColorInactive
import app.viewmodels.TabViewModel import app.viewmodels.TabViewModel
import javax.inject.Inject import javax.inject.Inject
import kotlin.io.path.Path import kotlin.io.path.Path
@ -44,9 +42,7 @@ fun RepositoriesTabPanel(
onTabClosed: (Int) -> Unit, onTabClosed: (Int) -> Unit,
newTabContent: (key: Int) -> TabInformation, newTabContent: (key: Int) -> TabInformation,
) { ) {
var tabsIdentifier by remember { var tabsIdentifier by remember { mutableStateOf(tabs.count()) }
mutableStateOf(tabs.count())
}
TabPanel( TabPanel(
modifier = modifier, modifier = modifier,
@ -123,7 +119,7 @@ fun TabPanel(
Icon( Icon(
imageVector = Icons.Default.Add, imageVector = Icons.Default.Add,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colors.primary tint = MaterialTheme.colors.primaryVariant,
) )
} }
} }
@ -138,9 +134,9 @@ fun Tab(title: MutableState<String>, selected: Boolean, onClick: () -> Unit, onC
0.dp 0.dp
Box { Box {
val backgroundColor = if (selected) val backgroundColor = if (selected)
MaterialTheme.colors.tabColorActive MaterialTheme.colors.surface
else else
MaterialTheme.colors.tabColorInactive MaterialTheme.colors.background
Row( Row(
modifier = Modifier modifier = Modifier

View File

@ -16,7 +16,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.theme.scrollbarHover import app.theme.scrollbarHover
import app.theme.scrollbarUnhover import app.theme.scrollbarNormal
@Composable @Composable
fun ScrollableLazyColumn( fun ScrollableLazyColumn(
@ -35,9 +35,9 @@ fun ScrollableLazyColumn(
modifier = Modifier modifier = Modifier
.align(Alignment.CenterEnd) .align(Alignment.CenterEnd)
.fillMaxHeight() .fillMaxHeight()
.padding(end = 4.dp), .padding(end = 2.dp),
style = LocalScrollbarStyle.current.copy( style = LocalScrollbarStyle.current.copy(
unhoverColor = MaterialTheme.colors.scrollbarUnhover, unhoverColor = MaterialTheme.colors.scrollbarNormal,
hoverColor = MaterialTheme.colors.scrollbarHover, hoverColor = MaterialTheme.colors.scrollbarHover,
), ),
adapter = rememberScrollbarAdapter( adapter = rememberScrollbarAdapter(

View File

@ -47,7 +47,7 @@ fun SideMenuSubentry(
modifier = Modifier modifier = Modifier
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
.size(16.dp), .size(16.dp),
tint = MaterialTheme.colors.primary, tint = MaterialTheme.colors.primaryVariant,
) )
Text( Text(

View File

@ -26,7 +26,7 @@ fun TextLink(
val textColor = if (isHovered == colorsInverted) { val textColor = if (isHovered == colorsInverted) {
MaterialTheme.colors.primaryTextColor MaterialTheme.colors.primaryTextColor
} else { } else {
MaterialTheme.colors.primary MaterialTheme.colors.primaryVariant
} }
Text( Text(

View File

@ -0,0 +1,15 @@
package app.ui.context_menu
import androidx.compose.foundation.ExperimentalFoundationApi
@OptIn(ExperimentalFoundationApi::class)
fun stashContextMenuItems(
onStashWithMessage: () -> Unit,
): List<DropDownContentData> {
return mutableListOf(
DropDownContentData(
label = "Stash with message",
onClick = onStashWithMessage,
),
)
}

View File

@ -14,6 +14,7 @@ import app.AppConstants
import app.AppConstants.openSourceProjects import app.AppConstants.openSourceProjects
import app.Project import app.Project
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.theme.textButtonColors
import app.ui.components.ScrollableLazyColumn import app.ui.components.ScrollableLazyColumn
import app.ui.components.TextLink import app.ui.components.TextLink
@ -21,7 +22,7 @@ import app.ui.components.TextLink
fun AppInfoDialog( fun AppInfoDialog(
onClose: () -> Unit, onClose: () -> Unit,
) { ) {
MaterialDialog { MaterialDialog(onCloseRequested = onClose) {
Column( Column(
modifier = Modifier modifier = Modifier
.width(600.dp) .width(600.dp)
@ -64,7 +65,8 @@ fun AppInfoDialog(
modifier = Modifier modifier = Modifier
.padding(top = 16.dp, end = 8.dp) .padding(top = 16.dp, end = 8.dp)
.align(Alignment.End), .align(Alignment.End),
onClick = onClose onClick = onClose,
colors = textButtonColors(),
) { ) {
Text("Close") Text("Close")
} }

View File

@ -18,7 +18,9 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import app.git.CloneStatus import app.git.CloneStatus
import app.theme.outlinedTextFieldColors
import app.theme.primaryTextColor import app.theme.primaryTextColor
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 openDirectoryDialog
@ -33,7 +35,7 @@ fun CloneDialog(
val cloneStatus = cloneViewModel.cloneStatus.collectAsState() val cloneStatus = cloneViewModel.cloneStatus.collectAsState()
val cloneStatusValue = cloneStatus.value val cloneStatusValue = cloneStatus.value
MaterialDialog { MaterialDialog(onCloseRequested = onClose) {
Box( Box(
modifier = Modifier modifier = Modifier
.width(400.dp) .width(400.dp)
@ -106,6 +108,7 @@ private fun CloneInput(
textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor), textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
maxLines = 1, maxLines = 1,
value = url, value = url,
colors = outlinedTextFieldColors(),
onValueChange = { onValueChange = {
cloneViewModel.resetStateIfError() cloneViewModel.resetStateIfError()
url = it url = it
@ -131,6 +134,7 @@ private fun CloneInput(
maxLines = 1, maxLines = 1,
label = { Text("Directory") }, label = { Text("Directory") },
value = directory, value = directory,
colors = outlinedTextFieldColors(),
onValueChange = { onValueChange = {
cloneViewModel.resetStateIfError() cloneViewModel.resetStateIfError()
directory = it directory = it
@ -190,6 +194,7 @@ private fun CloneInput(
previous = cloneButtonFocusRequester previous = cloneButtonFocusRequester
next = urlFocusRequester next = urlFocusRequester
}, },
colors = textButtonColors(),
onClick = { onClick = {
onClose() onClose()
} }
@ -248,6 +253,7 @@ private fun Cloning(cloneViewModel: CloneViewModel, cloneStatusValue: CloneStatu
end = 8.dp end = 8.dp
) )
.align(Alignment.End), .align(Alignment.End),
colors = textButtonColors(),
onClick = { onClick = {
cloneViewModel.cancelClone() cloneViewModel.cancelClone()
} }

View File

@ -1,11 +1,9 @@
package app.ui.dialogs package app.ui.dialogs
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.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Clear
@ -19,9 +17,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.extensions.handMouseClickable import app.extensions.handMouseClickable
import app.theme.borderColor import app.theme.*
import app.theme.primaryTextColor
import app.theme.secondaryTextColor
import app.ui.components.PrimaryButton import app.ui.components.PrimaryButton
import app.viewmodels.RemotesViewModel import app.viewmodels.RemotesViewModel
import org.eclipse.jgit.transport.RemoteConfig import org.eclipse.jgit.transport.RemoteConfig
@ -70,6 +66,7 @@ fun EditRemotesDialog(
MaterialDialog( MaterialDialog(
paddingVertical = 8.dp, paddingVertical = 8.dp,
paddingHorizontal = 16.dp, paddingHorizontal = 16.dp,
onCloseRequested = onDismiss
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@ -104,11 +101,6 @@ fun EditRemotesDialog(
Row( Row(
modifier = Modifier modifier = Modifier
.padding(bottom = 8.dp) .padding(bottom = 8.dp)
.border(
width = 1.dp,
shape = RoundedCornerShape(5.dp),
color = MaterialTheme.colors.borderColor,
)
.background(MaterialTheme.colors.surface) .background(MaterialTheme.colors.surface)
) { ) {
Column( Column(
@ -230,6 +222,7 @@ fun EditRemotesDialog(
}, },
textStyle = TextStyle.Default.copy(color = MaterialTheme.colors.primaryTextColor), textStyle = TextStyle.Default.copy(color = MaterialTheme.colors.primaryTextColor),
maxLines = 1, maxLines = 1,
colors = outlinedTextFieldColors(),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
@ -251,6 +244,7 @@ fun EditRemotesDialog(
}, },
textStyle = TextStyle.Default.copy(color = MaterialTheme.colors.primaryTextColor), textStyle = TextStyle.Default.copy(color = MaterialTheme.colors.primaryTextColor),
maxLines = 1, maxLines = 1,
colors = outlinedTextFieldColors(),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
@ -271,6 +265,7 @@ fun EditRemotesDialog(
}, },
textStyle = TextStyle.Default.copy(color = MaterialTheme.colors.primaryTextColor), textStyle = TextStyle.Default.copy(color = MaterialTheme.colors.primaryTextColor),
maxLines = 1, maxLines = 1,
colors = outlinedTextFieldColors(),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
@ -289,6 +284,7 @@ fun EditRemotesDialog(
TextButton( TextButton(
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.padding(end = 8.dp),
enabled = remoteChanged, enabled = remoteChanged,
colors = textButtonColors(),
onClick = { onClick = {
remotesEditorData = remotesEditorData.copy( remotesEditorData = remotesEditorData.copy(
selectedRemote = selectedRemote.copy( selectedRemote = selectedRemote.copy(

View File

@ -1,25 +1,37 @@
@file:OptIn(ExperimentalComposeUiApi::class)
package app.ui.dialogs package app.ui.dialogs
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider import androidx.compose.ui.window.PopupPositionProvider
import app.theme.dialogBackgroundColor import app.theme.dialogOverlay
@Composable @Composable
fun MaterialDialog( fun MaterialDialog(
alignment: Alignment = Alignment.Center, alignment: Alignment = Alignment.Center,
paddingHorizontal: Dp = 16.dp, paddingHorizontal: Dp = 16.dp,
paddingVertical: Dp = 16.dp, paddingVertical: Dp = 16.dp,
onCloseRequested: () -> Unit = {},
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
Popup( Popup(
@ -33,10 +45,26 @@ fun MaterialDialog(
): IntOffset = IntOffset.Zero ): IntOffset = IntOffset.Zero
} }
) { ) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(dialogBackgroundColor), .background(MaterialTheme.colors.dialogOverlay)
.focusRequester(focusRequester)
.focusable()
.onPreviewKeyEvent {
if (it.key == Key.Escape) {
onCloseRequested()
true
} else
false
},
contentAlignment = alignment, contentAlignment = alignment,
) { ) {
Box( Box(

View File

@ -15,6 +15,7 @@ import androidx.compose.ui.input.pointer.isPrimaryPressed
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.theme.textButtonColors
import app.ui.components.PrimaryButton import app.ui.components.PrimaryButton
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@ -28,7 +29,7 @@ fun MergeDialog(
) { ) {
var fastForwardCheck by remember { mutableStateOf(fastForward) } var fastForwardCheck by remember { mutableStateOf(fastForward) }
MaterialDialog { MaterialDialog(onCloseRequested = onReject) {
Column( Column(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colors.background), .background(MaterialTheme.colors.background),
@ -89,6 +90,7 @@ fun MergeDialog(
) { ) {
TextButton( TextButton(
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.padding(end = 8.dp),
colors = textButtonColors(),
onClick = { onClick = {
onReject() onReject()
} }

View File

@ -15,7 +15,9 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import app.theme.outlinedTextFieldColors
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.theme.textButtonColors
import app.ui.components.PrimaryButton import app.ui.components.PrimaryButton
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@ -28,7 +30,7 @@ fun NewBranchDialog(
val branchFieldFocusRequester = remember { FocusRequester() } val branchFieldFocusRequester = remember { FocusRequester() }
val buttonFieldFocusRequester = remember { FocusRequester() } val buttonFieldFocusRequester = remember { FocusRequester() }
MaterialDialog { MaterialDialog(onCloseRequested = onReject) {
Column( Column(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colors.background), .background(MaterialTheme.colors.background),
@ -42,7 +44,7 @@ fun NewBranchDialog(
} }
.width(300.dp) .width(300.dp)
.onPreviewKeyEvent { .onPreviewKeyEvent {
if (it.key == Key.Enter) { if (it.key == Key.Enter && branchField.isNotBlank()) {
onAccept(branchField) onAccept(branchField)
true true
} else { } else {
@ -53,6 +55,7 @@ fun NewBranchDialog(
singleLine = true, singleLine = true,
label = { Text("New branch name", fontSize = 14.sp) }, label = { Text("New branch name", fontSize = 14.sp) },
textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor), textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
colors = outlinedTextFieldColors(),
onValueChange = { onValueChange = {
branchField = it branchField = it
}, },
@ -64,6 +67,7 @@ fun NewBranchDialog(
) { ) {
TextButton( TextButton(
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.padding(end = 8.dp),
colors = textButtonColors(),
onClick = { onClick = {
onReject() onReject()
} }
@ -75,7 +79,7 @@ fun NewBranchDialog(
this.previous = branchFieldFocusRequester this.previous = branchFieldFocusRequester
this.next = branchFieldFocusRequester this.next = branchFieldFocusRequester
}, },
enabled = branchField.isNotEmpty(), enabled = branchField.isNotBlank(),
onClick = { onClick = {
onAccept(branchField) onAccept(branchField)
}, },

View File

@ -15,7 +15,9 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import app.theme.outlinedTextFieldColors
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.theme.textButtonColors
import app.ui.components.PrimaryButton import app.ui.components.PrimaryButton
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@ -28,7 +30,7 @@ fun NewTagDialog(
val tagFieldFocusRequester = remember { FocusRequester() } val tagFieldFocusRequester = remember { FocusRequester() }
val buttonFieldFocusRequester = remember { FocusRequester() } val buttonFieldFocusRequester = remember { FocusRequester() }
MaterialDialog { MaterialDialog(onCloseRequested = onReject) {
Column( Column(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colors.background), .background(MaterialTheme.colors.background),
@ -42,7 +44,7 @@ fun NewTagDialog(
} }
.width(300.dp) .width(300.dp)
.onPreviewKeyEvent { .onPreviewKeyEvent {
if (it.key == Key.Enter) { if (it.key == Key.Enter && tagField.isBlank()) {
onAccept(tagField) onAccept(tagField)
true true
} else { } else {
@ -53,6 +55,7 @@ fun NewTagDialog(
singleLine = true, singleLine = true,
label = { Text("New tag name", fontSize = 14.sp) }, label = { Text("New tag name", fontSize = 14.sp) },
textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor), textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
colors = outlinedTextFieldColors(),
onValueChange = { onValueChange = {
tagField = it tagField = it
}, },
@ -64,6 +67,7 @@ fun NewTagDialog(
) { ) {
TextButton( TextButton(
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.padding(end = 8.dp),
colors = textButtonColors(),
onClick = { onClick = {
onReject() onReject()
} }
@ -75,7 +79,7 @@ fun NewTagDialog(
this.previous = tagFieldFocusRequester this.previous = tagFieldFocusRequester
this.next = tagFieldFocusRequester this.next = tagFieldFocusRequester
}, },
enabled = tagField.isNotEmpty(), enabled = tagField.isBlank(),
onClick = { onClick = {
onAccept(tagField) onAccept(tagField)
}, },

View File

@ -16,6 +16,7 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import app.theme.outlinedTextFieldColors
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.ui.components.PrimaryButton import app.ui.components.PrimaryButton
@ -29,7 +30,7 @@ fun PasswordDialog(
val passwordFieldFocusRequester = remember { FocusRequester() } val passwordFieldFocusRequester = remember { FocusRequester() }
val buttonFieldFocusRequester = remember { FocusRequester() } val buttonFieldFocusRequester = remember { FocusRequester() }
MaterialDialog { MaterialDialog(onCloseRequested = onReject) {
Column( Column(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colors.background), .background(MaterialTheme.colors.background),
@ -61,6 +62,7 @@ fun PasswordDialog(
singleLine = true, singleLine = true,
label = { Text("Password", fontSize = 14.sp) }, label = { Text("Password", fontSize = 14.sp) },
textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor), textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
colors = outlinedTextFieldColors(),
onValueChange = { onValueChange = {
passwordField = it passwordField = it
}, },

View File

@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.theme.textButtonColors
import app.ui.components.PrimaryButton import app.ui.components.PrimaryButton
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@ -25,7 +26,7 @@ fun RebaseDialog(
onReject: () -> Unit, onReject: () -> Unit,
onAccept: () -> Unit onAccept: () -> Unit
) { ) {
MaterialDialog { MaterialDialog(onCloseRequested = onReject) {
Column( Column(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colors.background), .background(MaterialTheme.colors.background),
@ -67,6 +68,7 @@ fun RebaseDialog(
) { ) {
TextButton( TextButton(
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.padding(end = 8.dp),
colors = textButtonColors(),
onClick = { onClick = {
onReject() onReject()
} }

View File

@ -17,6 +17,7 @@ import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.git.ResetType import app.git.ResetType
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.theme.textButtonColors
import app.ui.components.PrimaryButton import app.ui.components.PrimaryButton
@Composable @Composable
@ -26,7 +27,7 @@ fun ResetBranchDialog(
) { ) {
var resetType by remember { mutableStateOf(ResetType.MIXED) } var resetType by remember { mutableStateOf(ResetType.MIXED) }
MaterialDialog { MaterialDialog(onCloseRequested = onReject) {
Column( Column(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colors.background), .background(MaterialTheme.colors.background),
@ -60,6 +61,7 @@ fun ResetBranchDialog(
) { ) {
TextButton( TextButton(
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.padding(end = 8.dp),
colors = textButtonColors(),
onClick = { onClick = {
onReject() onReject()
} }

View File

@ -1,16 +1,23 @@
package app.ui.dialogs package app.ui.dialogs
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.* import androidx.compose.material.*
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.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.AppPreferences import app.AppPreferences
import app.DropDownOption import app.DropDownOption
import app.theme.Themes import app.theme.outlinedTextFieldColors
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.theme.textButtonColors
import app.theme.themesList import app.theme.themesList
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable @Composable
fun SettingsDialog( fun SettingsDialog(
@ -18,16 +25,30 @@ fun SettingsDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
val currentTheme by appPreferences.themeState.collectAsState() val currentTheme by appPreferences.themeState.collectAsState()
val commitsLimitEnabled by appPreferences.commitsLimitEnabledFlow.collectAsState()
var commitsLimit by remember { mutableStateOf(appPreferences.commitsLimit) }
MaterialDialog { MaterialDialog(
Column(modifier = Modifier.width(500.dp)) { onCloseRequested = {
savePendingSettings(
appPreferences = appPreferences,
commitsLimit = commitsLimit,
)
onDismiss()
}
) {
Column(modifier = Modifier.width(720.dp)) {
Text( Text(
text = "Settings", text = "Settings",
color = MaterialTheme.colors.primaryTextColor, color = MaterialTheme.colors.primaryTextColor,
fontSize = 20.sp,
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp, start = 8.dp)
) )
SettingDropDown( SettingDropDown(
title = "Theme", title = "Theme",
subtitle = "Select the UI theme between light and dark mode",
dropDownOptions = themesList, dropDownOptions = themesList,
currentOption = currentTheme, currentOption = currentTheme,
onOptionSelected = { theme -> onOptionSelected = { theme ->
@ -35,11 +56,38 @@ fun SettingsDialog(
} }
) )
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
}
)
TextButton( TextButton(
modifier = Modifier modifier = Modifier
.padding(end = 8.dp) .padding(end = 8.dp)
.align(Alignment.End), .align(Alignment.End),
onClick = onDismiss colors = textButtonColors(),
onClick = {
savePendingSettings(
appPreferences = appPreferences,
commitsLimit = commitsLimit,
)
onDismiss()
}
) { ) {
Text("Close") Text("Close")
} }
@ -47,9 +95,19 @@ fun SettingsDialog(
} }
} }
fun savePendingSettings(
appPreferences: AppPreferences,
commitsLimit: Int,
) {
if (appPreferences.commitsLimit != commitsLimit) {
appPreferences.commitsLimit = commitsLimit
}
}
@Composable @Composable
fun <T: DropDownOption> SettingDropDown( fun <T : DropDownOption> SettingDropDown(
title: String, title: String,
subtitle: String,
dropDownOptions: List<T>, dropDownOptions: List<T>,
onOptionSelected: (T) -> Unit, onOptionSelected: (T) -> Unit,
currentOption: T, currentOption: T,
@ -59,16 +117,28 @@ fun <T: DropDownOption> SettingDropDown(
modifier = Modifier.padding(vertical = 8.dp, horizontal = 8.dp), modifier = Modifier.padding(vertical = 8.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Column(verticalArrangement = Arrangement.Center) {
text = title, Text(
color = MaterialTheme.colors.primaryTextColor, 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)) Spacer(modifier = Modifier.weight(1f))
Box { Box {
OutlinedButton(onClick = { showThemeDropdown = true }) { OutlinedButton(onClick = { showThemeDropdown = true }) {
Text( Text(
currentOption.optionName, text = currentOption.optionName,
color = MaterialTheme.colors.primaryTextColor, color = MaterialTheme.colors.primaryTextColor,
fontSize = 14.sp,
) )
} }
@ -93,46 +163,111 @@ fun <T: DropDownOption> SettingDropDown(
} }
@Composable @Composable
fun <T: DropDownOption> SettingTextInput( fun SettingToogle(
title: String, title: String,
dropDownOptions: List<T>, subtitle: String,
onOptionSelected: (T) -> Unit, value: Boolean,
currentOption: T, onValueChanged: (Boolean) -> Unit,
) { ) {
var showThemeDropdown by remember { mutableStateOf(false) }
Row( Row(
modifier = Modifier.padding(vertical = 8.dp, horizontal = 8.dp), modifier = Modifier.padding(vertical = 8.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Column(
text = title, verticalArrangement = Arrangement.Center
color = MaterialTheme.colors.primaryTextColor, ) {
) Text(
Spacer(modifier = Modifier.width(300.dp)) text = title,
Box { color = MaterialTheme.colors.primaryTextColor,
OutlinedButton(onClick = { showThemeDropdown = true }) { fontSize = 16.sp,
Text( )
currentOption.optionName,
color = MaterialTheme.colors.primaryTextColor,
)
}
DropdownMenu( Text(
expanded = showThemeDropdown, text = subtitle,
onDismissRequest = { showThemeDropdown = false }, color = MaterialTheme.colors.primaryTextColor,
) { modifier = Modifier.padding(top = 4.dp),
for (dropDownOption in dropDownOptions) { fontSize = 12.sp,
DropdownMenuItem( )
onClick = { }
showThemeDropdown = false
onOptionSelected(dropDownOption) Spacer(modifier = Modifier.weight(1f))
}
) { Switch(value, onCheckedChange = onValueChanged)
Text(dropDownOption.optionName) }
}
@Composable
fun SettingIntInput(
title: String,
subtitle: String,
value: Int,
enabled: Boolean = true,
onValueChanged: (Int) -> 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))
var text by remember {
mutableStateOf(value.toString())
}
var isError by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
OutlinedTextField(
value = text,
modifier = Modifier.width(136.dp),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
isError = isError,
enabled = enabled,
onValueChange = {
val textFiltered = it.filter { c -> c.isDigit() }
if (textFiltered.isEmpty() || isValidInt(textFiltered)) {
isError = false
val newValue = textFiltered.toIntOrNull() ?: 0
text = newValue.toString()
onValueChanged(newValue)
} else {
scope.launch {
isError = true
delay(500) // Show an error
isError = false
} }
} }
} },
colors = outlinedTextFieldColors(),
maxLines = 1,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.End),
)
}
}
} private fun isValidInt(value: String): Boolean {
return try {
value.toInt()
true
} catch (ex: Exception) {
false
} }
} }

View File

@ -0,0 +1,94 @@
package app.ui.dialogs
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusOrder
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.theme.outlinedTextFieldColors
import app.theme.primaryTextColor
import app.theme.textButtonColors
import app.ui.components.PrimaryButton
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun StashWithMessageDialog(
onReject: () -> Unit,
onAccept: (stashMessage: String) -> Unit
) {
var textField by remember { mutableStateOf("") }
val textFieldFocusRequester = remember { FocusRequester() }
val buttonFieldFocusRequester = remember { FocusRequester() }
MaterialDialog(onCloseRequested = onReject) {
Column(
modifier = Modifier
.background(MaterialTheme.colors.background),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
OutlinedTextField(
modifier = Modifier
.focusOrder(textFieldFocusRequester) {
this.next = buttonFieldFocusRequester
}
.width(300.dp)
.onPreviewKeyEvent {
if (it.key == Key.Enter && textField.isNotBlank()) {
onAccept(textField)
true
} else {
false
}
},
value = textField,
label = { Text("New stash message", fontSize = 14.sp) },
textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
colors = outlinedTextFieldColors(),
onValueChange = {
textField = it
},
)
Row(
modifier = Modifier
.padding(top = 16.dp)
.align(Alignment.End)
) {
TextButton(
modifier = Modifier.padding(end = 8.dp),
colors = textButtonColors(),
onClick = {
onReject()
}
) {
Text("Cancel")
}
PrimaryButton(
modifier = Modifier.focusOrder(buttonFieldFocusRequester) {
this.previous = textFieldFocusRequester
this.next = textFieldFocusRequester
},
enabled = textField.isNotBlank(),
onClick = {
onAccept(textField)
},
text = "Stash"
)
}
}
}
LaunchedEffect(Unit) {
textFieldFocusRequester.requestFocus()
}
}

View File

@ -16,7 +16,9 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import app.theme.outlinedTextFieldColors
import app.theme.primaryTextColor import app.theme.primaryTextColor
import app.theme.textButtonColors
import app.ui.components.PrimaryButton import app.ui.components.PrimaryButton
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@ -33,7 +35,9 @@ fun UserPasswordDialog(
val acceptDialog = { val acceptDialog = {
onAccept(userField, passwordField) onAccept(userField, passwordField)
} }
MaterialDialog { MaterialDialog(
onCloseRequested = onReject
) {
Column( Column(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colors.background), .background(MaterialTheme.colors.background),
@ -64,6 +68,7 @@ fun UserPasswordDialog(
}, },
value = userField, value = userField,
singleLine = true, singleLine = true,
colors = outlinedTextFieldColors(),
label = { Text("User", fontSize = 14.sp) }, label = { Text("User", fontSize = 14.sp) },
textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor), textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
onValueChange = { onValueChange = {
@ -89,6 +94,7 @@ fun UserPasswordDialog(
singleLine = true, singleLine = true,
label = { Text("Password", fontSize = 14.sp) }, label = { Text("Password", fontSize = 14.sp) },
textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor), textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
colors = outlinedTextFieldColors(),
onValueChange = { onValueChange = {
passwordField = it passwordField = it
}, },
@ -102,6 +108,7 @@ fun UserPasswordDialog(
) { ) {
TextButton( TextButton(
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.padding(end = 8.dp),
colors = textButtonColors(),
onClick = { onClick = {
onReject() onReject()
} }

View File

@ -30,6 +30,7 @@ import androidx.compose.ui.input.key.*
import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.PointerIconDefaults import androidx.compose.ui.input.pointer.PointerIconDefaults
import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
@ -156,6 +157,7 @@ fun Log(
graphWidth = graphWidth, graphWidth = graphWidth,
scrollState = verticalScrollState, scrollState = verticalScrollState,
hasUncommitedChanges = hasUncommitedChanges, hasUncommitedChanges = hasUncommitedChanges,
commitsLimit = logStatus.commitsLimit,
) )
// The commits' messages list overlaps with the graph list to catch all the click events but leaves // The commits' messages list overlaps with the graph list to catch all the click events but leaves
@ -171,9 +173,11 @@ fun Log(
commitList = commitList, commitList = commitList,
logViewModel = logViewModel, logViewModel = logViewModel,
graphWidth = graphWidth, graphWidth = graphWidth,
commitsLimit = logStatus.commitsLimit,
onShowLogDialog = { dialog -> onShowLogDialog = { dialog ->
logViewModel.showDialog(dialog) logViewModel.showDialog(dialog)
}) }
)
DividerLog( DividerLog(
modifier = Modifier.draggable( modifier = Modifier.draggable(
@ -189,7 +193,7 @@ fun Log(
HorizontalScrollbar( HorizontalScrollbar(
modifier = Modifier.align(Alignment.BottomStart).width(graphWidth) modifier = Modifier.align(Alignment.BottomStart).width(graphWidth)
.padding(start = 4.dp, bottom = 4.dp), style = LocalScrollbarStyle.current.copy( .padding(start = 4.dp, bottom = 4.dp), style = LocalScrollbarStyle.current.copy(
unhoverColor = MaterialTheme.colors.scrollbarUnhover, unhoverColor = MaterialTheme.colors.scrollbarNormal,
hoverColor = MaterialTheme.colors.scrollbarHover, hoverColor = MaterialTheme.colors.scrollbarHover,
), adapter = rememberScrollbarAdapter(horizontalScrollState) ), adapter = rememberScrollbarAdapter(horizontalScrollState)
) )
@ -226,8 +230,7 @@ fun SearchFilter(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(64.dp) .height(64.dp),
.background(MaterialTheme.colors.graphHeaderBackground),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
TextField( TextField(
@ -260,7 +263,7 @@ fun SearchFilter(
label = { label = {
Text("Search by message, author name or commit ID") Text("Search by message, author name or commit ID")
}, },
colors = TextFieldDefaults.textFieldColors(backgroundColor = MaterialTheme.colors.background), colors = textFieldColors(),
textStyle = TextStyle.Default.copy(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor), textStyle = TextStyle.Default.copy(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
trailingIcon = { trailingIcon = {
Row( Row(
@ -320,6 +323,7 @@ fun MessagesList(
selectedItem: SelectedItem, selectedItem: SelectedItem,
commitList: GraphCommitList, commitList: GraphCommitList,
logViewModel: LogViewModel, logViewModel: LogViewModel,
commitsLimit: Int,
onShowLogDialog: (LogDialog) -> Unit, onShowLogDialog: (LogDialog) -> Unit,
graphWidth: Dp, graphWidth: Dp,
) { ) {
@ -354,6 +358,25 @@ fun MessagesList(
) )
} }
if (commitsLimit >= 0 && commitsLimit <= commitList.count()) {
item {
Box(
modifier = Modifier
.padding(start = graphWidth + 24.dp)
.height(40.dp),
contentAlignment = Alignment.CenterStart,
) {
Text(
text = "The commits list has been limited to $commitsLimit. Access the settings to change it.",
color = MaterialTheme.colors.primaryTextColor,
fontSize = 14.sp,
fontStyle = FontStyle.Italic,
maxLines = 1,
)
}
}
}
item { item {
Box(modifier = Modifier.height(20.dp)) Box(modifier = Modifier.height(20.dp))
} }
@ -369,6 +392,7 @@ fun GraphList(
hasUncommitedChanges: Boolean, hasUncommitedChanges: Boolean,
selectedCommit: RevCommit?, selectedCommit: RevCommit?,
selectedItem: SelectedItem, selectedItem: SelectedItem,
commitsLimit: Int,
) { ) {
val maxLinePosition = if (commitList.isNotEmpty()) val maxLinePosition = if (commitList.isNotEmpty())
commitList.maxLine commitList.maxLine
@ -429,6 +453,16 @@ fun GraphList(
} }
} }
// Spacing when the commits limit is present
if (commitsLimit >= 0 && commitsLimit <= commitList.count()) {
item {
Box(
modifier = Modifier
.height(40.dp),
)
}
}
item { item {
Box(modifier = Modifier.height(20.dp)) Box(modifier = Modifier.height(20.dp))
} }
@ -496,11 +530,14 @@ fun GraphHeader(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth().height(48.dp).background(MaterialTheme.colors.graphHeaderBackground), modifier = Modifier
.fillMaxWidth()
.height(40.dp)
.background(MaterialTheme.colors.headerBackground),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
modifier = Modifier.width(graphWidth).padding(start = 8.dp), modifier = Modifier.width(graphWidth).padding(start = 16.dp),
text = "Graph", text = "Graph",
color = MaterialTheme.colors.headerText, color = MaterialTheme.colors.headerText,
fontSize = 14.sp, fontSize = 14.sp,
@ -516,7 +553,9 @@ fun GraphHeader(
) )
Text( Text(
modifier = Modifier.padding(start = 8.dp).weight(1f), modifier = Modifier
.padding(start = 16.dp)
.weight(1f),
text = "Message", text = "Message",
color = MaterialTheme.colors.headerText, color = MaterialTheme.colors.headerText,
fontSize = 14.sp, fontSize = 14.sp,
@ -551,11 +590,9 @@ fun UncommitedChangesLine(
modifier = Modifier.height(40.dp) modifier = Modifier.height(40.dp)
.fillMaxWidth() .fillMaxWidth()
.clickable { onUncommitedChangesSelected() } .clickable { onUncommitedChangesSelected() }
.padding( .padding(start = graphWidth)
start = graphWidth + DIVIDER_WIDTH.dp, .backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected)
end = 4.dp, .padding(DIVIDER_WIDTH.dp),
)
.backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
val text = when { val text = when {
@ -800,7 +837,10 @@ fun DividerLog(modifier: Modifier, graphWidth: Dp) {
.pointerHoverIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR))) .pointerHoverIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR)))
) { ) {
Box( Box(
modifier = Modifier.fillMaxHeight().width(1.dp).background(color = MaterialTheme.colors.primary) modifier = Modifier
.fillMaxHeight()
.width(1.dp)
.background(color = MaterialTheme.colors.primaryVariant)
.align(Alignment.Center) .align(Alignment.Center)
) )
} }
@ -823,6 +863,10 @@ fun CommitsGraphLine(
val passingLanes = plotCommit.passingLanes val passingLanes = plotCommit.passingLanes
val forkingOffLanes = plotCommit.forkingOffLanes val forkingOffLanes = plotCommit.forkingOffLanes
val mergingLanes = plotCommit.mergingLanes val mergingLanes = plotCommit.mergingLanes
val density = LocalDensity.current.density
val laneWidthWithDensity = remember(density) {
LANE_WIDTH * density
}
Box( Box(
modifier = modifier modifier = modifier
@ -837,40 +881,40 @@ fun CommitsGraphLine(
if (plotCommit.childCount > 0) { if (plotCommit.childCount > 0) {
drawLine( drawLine(
color = colors[itemPosition % colors.size], color = colors[itemPosition % colors.size],
start = Offset(30f * (itemPosition + 1), this.center.y), start = Offset( laneWidthWithDensity * (itemPosition + 1), this.center.y),
end = Offset(30f * (itemPosition + 1), 0f), end = Offset( laneWidthWithDensity * (itemPosition + 1), 0f),
) )
} }
forkingOffLanes.forEach { plotLane -> forkingOffLanes.forEach { plotLane ->
drawLine( drawLine(
color = colors[plotLane.position % colors.size], color = colors[plotLane.position % colors.size],
start = Offset(30f * (itemPosition + 1), this.center.y), start = Offset(laneWidthWithDensity * (itemPosition + 1), this.center.y),
end = Offset(30f * (plotLane.position + 1), 0f), end = Offset(laneWidthWithDensity * (plotLane.position + 1), 0f),
) )
} }
mergingLanes.forEach { plotLane -> mergingLanes.forEach { plotLane ->
drawLine( drawLine(
color = colors[plotLane.position % colors.size], color = colors[plotLane.position % colors.size],
start = Offset(30f * (plotLane.position + 1), this.size.height), start = Offset(laneWidthWithDensity * (plotLane.position + 1), this.size.height),
end = Offset(30f * (itemPosition + 1), this.center.y), end = Offset(laneWidthWithDensity * (itemPosition + 1), this.center.y),
) )
} }
if (plotCommit.parentCount > 0) { if (plotCommit.parentCount > 0) {
drawLine( drawLine(
color = colors[itemPosition % colors.size], color = colors[itemPosition % colors.size],
start = Offset(30f * (itemPosition + 1), this.center.y), start = Offset(laneWidthWithDensity * (itemPosition + 1), this.center.y),
end = Offset(30f * (itemPosition + 1), this.size.height), end = Offset(laneWidthWithDensity * (itemPosition + 1), this.size.height),
) )
} }
passingLanes.forEach { plotLane -> passingLanes.forEach { plotLane ->
drawLine( drawLine(
color = colors[plotLane.position % colors.size], color = colors[plotLane.position % colors.size],
start = Offset(30f * (plotLane.position + 1), 0f), start = Offset(laneWidthWithDensity * (plotLane.position + 1), 0f),
end = Offset(30f * (plotLane.position + 1), this.size.height), end = Offset(laneWidthWithDensity * (plotLane.position + 1), this.size.height),
) )
} }
} }
@ -907,6 +951,11 @@ fun UncommitedChangesGraphNode(
hasPreviousCommits: Boolean, hasPreviousCommits: Boolean,
isSelected: Boolean, isSelected: Boolean,
) { ) {
val density = LocalDensity.current.density
val laneWidthWithDensity = remember(density) {
LANE_WIDTH * density
}
Box( Box(
modifier = modifier modifier = modifier
.backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected) .backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected)
@ -918,14 +967,14 @@ fun UncommitedChangesGraphNode(
if (hasPreviousCommits) drawLine( if (hasPreviousCommits) drawLine(
color = colors[0], color = colors[0],
start = Offset(30f, this.center.y), start = Offset(laneWidthWithDensity, this.center.y),
end = Offset(30f, this.size.height), end = Offset(laneWidthWithDensity, this.size.height),
) )
drawCircle( drawCircle(
color = colors[0], color = colors[0],
radius = 15f, radius = 15f,
center = Offset(30f, this.center.y), center = Offset(laneWidthWithDensity, this.center.y),
) )
} }
} }
@ -969,7 +1018,7 @@ fun BranchChip(
painter = painterResource("location.svg"), painter = painterResource("location.svg"),
contentDescription = null, contentDescription = null,
modifier = Modifier.padding(end = 6.dp), modifier = Modifier.padding(end = 6.dp),
tint = MaterialTheme.colors.primary, tint = MaterialTheme.colors.primaryVariant,
) )
} }
} }
@ -1042,7 +1091,7 @@ fun RefChip(
modifier = Modifier.padding(6.dp).size(14.dp), modifier = Modifier.padding(6.dp).size(14.dp),
painter = painterResource(icon), painter = painterResource(icon),
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colors.inversePrimaryTextColor, tint = MaterialTheme.colors.background,
) )
} }
Text( Text(

View File

@ -1,15 +1,17 @@
package app.viewmodels package app.viewmodels
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import app.AppPreferences
import app.git.* import app.git.*
import app.git.graph.GraphCommitList import app.git.graph.GraphCommitList
import app.git.graph.GraphNode import app.git.graph.GraphNode
import app.ui.SelectedItem import app.ui.SelectedItem
import app.ui.log.LogDialog import app.ui.log.LogDialog
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
@ -36,6 +38,7 @@ class LogViewModel @Inject constructor(
private val mergeManager: MergeManager, private val mergeManager: MergeManager,
private val remoteOperationsManager: RemoteOperationsManager, private val remoteOperationsManager: RemoteOperationsManager,
private val tabState: TabState, private val tabState: TabState,
private val appPreferences: AppPreferences,
) { ) {
private val _logStatus = MutableStateFlow<LogStatus>(LogStatus.Loading) private val _logStatus = MutableStateFlow<LogStatus>(LogStatus.Loading)
@ -57,9 +60,24 @@ class LogViewModel @Inject constructor(
) )
) )
private val scope = CoroutineScope(Dispatchers.IO)
private val _logSearchFilterResults = MutableStateFlow<LogSearch>(LogSearch.NotSearching) private val _logSearchFilterResults = MutableStateFlow<LogSearch>(LogSearch.NotSearching)
val logSearchFilterResults: StateFlow<LogSearch> = _logSearchFilterResults val logSearchFilterResults: StateFlow<LogSearch> = _logSearchFilterResults
init {
scope.launch {
appPreferences.commitsLimitEnabledFlow.collect {
tabState.refreshData(RefreshType.ONLY_LOG)
}
}
scope.launch {
appPreferences.commitsLimitFlow.collect {
tabState.refreshData(RefreshType.ONLY_LOG)
}
}
}
private suspend fun loadLog(git: Git) { private suspend fun loadLog(git: Git) {
_logStatus.value = LogStatus.Loading _logStatus.value = LogStatus.Loading
@ -70,9 +88,19 @@ class LogViewModel @Inject constructor(
) )
val hasUncommitedChanges = statusSummary.total > 0 val hasUncommitedChanges = statusSummary.total > 0
val log = logManager.loadLog(git, currentBranch, hasUncommitedChanges) val commitsLimit = if(appPreferences.commitsLimitEnabled) {
appPreferences.commitsLimit
} else
Int.MAX_VALUE
_logStatus.value = LogStatus.Loaded(hasUncommitedChanges, log, currentBranch, statusSummary) val commitsLimitDisplayed = if(appPreferences.commitsLimitEnabled) {
appPreferences.commitsLimit
} else
-1
val log = logManager.loadLog(git, currentBranch, hasUncommitedChanges, commitsLimit)
_logStatus.value = LogStatus.Loaded(hasUncommitedChanges, log, currentBranch, statusSummary, commitsLimitDisplayed)
// Remove search filter if the log has been updated // Remove search filter if the log has been updated
_logSearchFilterResults.value = LogSearch.NotSearching _logSearchFilterResults.value = LogSearch.NotSearching
@ -182,6 +210,7 @@ class LogViewModel @Inject constructor(
plotCommitList = previousLogStatusValue.plotCommitList, plotCommitList = previousLogStatusValue.plotCommitList,
currentBranch = currentBranch, currentBranch = currentBranch,
statusSummary = statsSummary, statusSummary = statsSummary,
commitsLimit = previousLogStatusValue.commitsLimit,
) )
_logStatus.value = newLogStatusValue _logStatus.value = newLogStatusValue
@ -329,6 +358,7 @@ sealed class LogStatus {
val plotCommitList: GraphCommitList, val plotCommitList: GraphCommitList,
val currentBranch: Ref?, val currentBranch: Ref?,
val statusSummary: StatusSummary, val statusSummary: StatusSummary,
val commitsLimit: Int,
) : LogStatus() ) : LogStatus()
} }

View File

@ -35,7 +35,14 @@ class MenuViewModel @Inject constructor(
refreshType = RefreshType.UNCOMMITED_CHANGES_AND_LOG, refreshType = RefreshType.UNCOMMITED_CHANGES_AND_LOG,
) { git -> ) { git ->
statusManager.stageUntrackedFiles(git) statusManager.stageUntrackedFiles(git)
stashManager.stash(git) stashManager.stash(git, null)
}
fun stashWithMessage(message: String) = tabState.safeProcessing(
refreshType = RefreshType.UNCOMMITED_CHANGES_AND_LOG,
) { git ->
statusManager.stageUntrackedFiles(git)
stashManager.stash(git, message)
} }
fun popStash() = tabState.safeProcessing( fun popStash() = tabState.safeProcessing(

View File

@ -1,19 +1,21 @@
package app.viewmodels package app.viewmodels
import app.extensions.isMerging
import app.git.* import app.git.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.RepositoryState
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
class StatusViewModel @Inject constructor( class StatusViewModel @Inject constructor(
private val tabState: TabState, private val tabState: TabState,
private val statusManager: StatusManager, private val statusManager: StatusManager,
private val branchesManager: BranchesManager,
private val repositoryManager: RepositoryManager,
private val rebaseManager: RebaseManager, private val rebaseManager: RebaseManager,
private val mergeManager: MergeManager, private val mergeManager: MergeManager,
private val logManager: LogManager, private val logManager: LogManager,
@ -21,11 +23,30 @@ class StatusViewModel @Inject constructor(
private val _stageStatus = MutableStateFlow<StageStatus>(StageStatus.Loaded(listOf(), listOf())) private val _stageStatus = MutableStateFlow<StageStatus>(StageStatus.Loaded(listOf(), listOf()))
val stageStatus: StateFlow<StageStatus> = _stageStatus val stageStatus: StateFlow<StageStatus> = _stageStatus
var savedCommitMessage: String = "" var savedCommitMessage = CommitMessage("", MessageType.NORMAL)
var hasPreviousCommits = true // When false, disable "amend previous commit" var hasPreviousCommits = true // When false, disable "amend previous commit"
private var lastUncommitedChangesState = false private var lastUncommitedChangesState = false
/**
* Notify the UI that the commit message has been changed by the view model
*/
private val _commitMessageChangesFlow = MutableSharedFlow<String>()
val commitMessageChangesFlow: SharedFlow<String> = _commitMessageChangesFlow
private fun persistMessage() = tabState.runOperation(
refreshType = RefreshType.NONE,
) { git ->
val messageToPersist = savedCommitMessage.message.ifBlank { null }
if (git.repository.repositoryState.isMerging) {
git.repository.writeMergeCommitMsg(messageToPersist)
} else if (git.repository.repositoryState == RepositoryState.SAFE) {
git.repository.writeCommitEditMsg(messageToPersist)
}
}
fun stage(statusEntry: StatusEntry) = tabState.runOperation( fun stage(statusEntry: StatusEntry) = tabState.runOperation(
refreshType = RefreshType.UNCOMMITED_CHANGES, refreshType = RefreshType.UNCOMMITED_CHANGES,
) { git -> ) { git ->
@ -66,6 +87,21 @@ class StatusViewModel @Inject constructor(
private suspend fun loadStatus(git: Git) { private suspend fun loadStatus(git: Git) {
val previousStatus = _stageStatus.value val previousStatus = _stageStatus.value
val requiredMessageType = if (git.repository.repositoryState == RepositoryState.MERGING) {
MessageType.MERGE
} else {
MessageType.NORMAL
}
if (requiredMessageType != savedCommitMessage.messageType) {
savedCommitMessage = CommitMessage(messageByRepoState(git), requiredMessageType)
_commitMessageChangesFlow.emit(savedCommitMessage.message)
} else if (savedCommitMessage.message.isEmpty()) {
savedCommitMessage = savedCommitMessage.copy(message = messageByRepoState(git))
_commitMessageChangesFlow.emit(savedCommitMessage.message)
}
try { try {
_stageStatus.value = StageStatus.Loading _stageStatus.value = StageStatus.Loading
val status = statusManager.getStatus(git) val status = statusManager.getStatus(git)
@ -79,6 +115,17 @@ class StatusViewModel @Inject constructor(
} }
} }
private fun messageByRepoState(git: Git): String {
val message: String? = if (git.repository.repositoryState == RepositoryState.MERGING) {
git.repository.readMergeCommitMsg()
} else {
git.repository.readCommitEditMsg()
}
//TODO this replace is a workaround until this issue gets fixed https://github.com/JetBrains/compose-jb/issues/615
return message.orEmpty().replace("\t", " ")
}
private suspend fun loadHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) { private suspend fun loadHasUncommitedChanges(git: Git) = withContext(Dispatchers.IO) {
lastUncommitedChangesState = statusManager.hasUncommitedChanges(git) lastUncommitedChangesState = statusManager.hasUncommitedChanges(git)
} }
@ -92,6 +139,7 @@ class StatusViewModel @Inject constructor(
message message
statusManager.commit(git, commitMessage, amend) statusManager.commit(git, commitMessage, amend)
updateCommitMessage("")
} }
suspend fun refresh(git: Git) = withContext(Dispatchers.IO) { suspend fun refresh(git: Git) = withContext(Dispatchers.IO) {
@ -148,6 +196,11 @@ class StatusViewModel @Inject constructor(
fileToDelete.delete() fileToDelete.delete()
} }
fun updateCommitMessage(message: String) {
savedCommitMessage = savedCommitMessage.copy(message = message)
persistMessage()
}
} }
sealed class StageStatus { sealed class StageStatus {
@ -155,3 +208,9 @@ sealed class StageStatus {
data class Loaded(val staged: List<StatusEntry>, val unstaged: List<StatusEntry>) : StageStatus() data class Loaded(val staged: List<StatusEntry>, val unstaged: List<StatusEntry>) : StageStatus()
} }
data class CommitMessage(val message: String, val messageType: MessageType)
enum class MessageType {
NORMAL,
MERGE;
}