diff --git a/src/main/kotlin/app/App.kt b/src/main/kotlin/app/App.kt index 55839ea..b2a9716 100644 --- a/src/main/kotlin/app/App.kt +++ b/src/main/kotlin/app/App.kt @@ -4,18 +4,11 @@ package app import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -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.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi 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.pointerHoverIcon 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.application import androidx.compose.ui.window.rememberWindowState -import androidx.compose.ui.zIndex import app.di.DaggerAppComponent import app.theme.AppTheme import app.theme.primaryTextColor @@ -76,7 +68,7 @@ class App { ) { var showSettingsDialog by remember { mutableStateOf(false) } - AppTheme(theme = theme) { + AppTheme(selectedTheme = theme) { Box(modifier = Modifier.background(MaterialTheme.colors.background)) { AppTabs( onOpenSettings = { @@ -127,13 +119,8 @@ class App { ) { val tabs by tabsFlow.collectAsState() val tabsInformationList = tabs.sortedBy { it.key } - - println("Tabs count ${tabs.count()}") - val selectedTabKey = remember { mutableStateOf(0) } - println("Selected tab key: ${selectedTabKey.value}") - Column( modifier = Modifier.background(MaterialTheme.colors.background) ) { @@ -214,7 +201,7 @@ class App { painter = painterResource("settings.svg"), contentDescription = null, modifier = Modifier.fillMaxSize(), - tint = MaterialTheme.colors.primary, + tint = MaterialTheme.colors.primaryVariant, ) } } diff --git a/src/main/kotlin/app/AppPreferences.kt b/src/main/kotlin/app/AppPreferences.kt index e680b50..14c7b9c 100644 --- a/src/main/kotlin/app/AppPreferences.kt +++ b/src/main/kotlin/app/AppPreferences.kt @@ -12,6 +12,11 @@ private const val PREFERENCES_NAME = "GitnuroConfig" private const val PREF_LATEST_REPOSITORIES_TABS_OPENED = "latestRepositoriesTabsOpened" private const val PREF_LAST_OPENED_REPOSITORIES_PATH = "lastOpenedRepositoriesList" 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 class AppPreferences @Inject constructor() { @@ -20,6 +25,12 @@ class AppPreferences @Inject constructor() { private val _themeState = MutableStateFlow(theme) val themeState: StateFlow = _themeState + private val _commitsLimitEnabledFlow = MutableStateFlow(true) + val commitsLimitEnabledFlow: StateFlow = _commitsLimitEnabledFlow + + private val _commitsLimitFlow = MutableStateFlow(commitsLimit) + val commitsLimitFlow: StateFlow = _commitsLimitFlow + var latestTabsOpened: String get() = preferences.get(PREF_LATEST_REPOSITORIES_TABS_OPENED, "") set(value) { @@ -35,10 +46,33 @@ class AppPreferences @Inject constructor() { var theme: Themes get() { 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) { preferences.put(PREF_THEME, value.toString()) _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 + } } \ No newline at end of file diff --git a/src/main/kotlin/app/di/AppComponent.kt b/src/main/kotlin/app/di/AppComponent.kt index cab13ef..15c4cf7 100644 --- a/src/main/kotlin/app/di/AppComponent.kt +++ b/src/main/kotlin/app/di/AppComponent.kt @@ -1,6 +1,7 @@ package app.di import app.App +import app.AppPreferences import app.AppStateManager import dagger.Component import javax.inject.Singleton @@ -10,4 +11,6 @@ import javax.inject.Singleton interface AppComponent { fun inject(main: App) fun appStateManager(): AppStateManager + + fun appPreferences(): AppPreferences } \ No newline at end of file diff --git a/src/main/kotlin/app/di/TabComponent.kt b/src/main/kotlin/app/di/TabComponent.kt index 99f11ae..b2c624b 100644 --- a/src/main/kotlin/app/di/TabComponent.kt +++ b/src/main/kotlin/app/di/TabComponent.kt @@ -1,5 +1,6 @@ package app.di +import app.AppPreferences import app.di.modules.NetworkModule import app.ui.components.TabInformation import dagger.Component diff --git a/src/main/kotlin/app/git/FileChangesWatcher.kt b/src/main/kotlin/app/git/FileChangesWatcher.kt index c286132..4a6c575 100644 --- a/src/main/kotlin/app/git/FileChangesWatcher.kt +++ b/src/main/kotlin/app/git/FileChangesWatcher.kt @@ -52,12 +52,19 @@ class FileChangesWatcher @Inject constructor() { while (watchService.take().also { key = it } != null) { val events = key.pollEvents() - println("Polled events on dir ${keys[key]}") - val dir = keys[key] ?: return@withContext 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") _changesNotifier.emit(hasGitDirectoryChanged) @@ -86,4 +93,10 @@ class FileChangesWatcher @Inject constructor() { 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" + } } \ No newline at end of file diff --git a/src/main/kotlin/app/git/LogManager.kt b/src/main/kotlin/app/git/LogManager.kt index 73c6f4c..0078852 100644 --- a/src/main/kotlin/app/git/LogManager.kt +++ b/src/main/kotlin/app/git/LogManager.kt @@ -14,12 +14,12 @@ import javax.inject.Inject 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 repositoryState = git.repository.repositoryState println("Repository state ${repositoryState.description}") 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) @@ -36,7 +36,7 @@ class LogManager @Inject constructor() { commitList.addUncommitedChangesGraphCommit(logList.first()) 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() diff --git a/src/main/kotlin/app/git/StashManager.kt b/src/main/kotlin/app/git/StashManager.kt index c3b4fba..1fc906f 100644 --- a/src/main/kotlin/app/git/StashManager.kt +++ b/src/main/kotlin/app/git/StashManager.kt @@ -7,10 +7,14 @@ import org.eclipse.jgit.revwalk.RevCommit import javax.inject.Inject class StashManager @Inject constructor() { - suspend fun stash(git: Git) = withContext(Dispatchers.IO) { + suspend fun stash(git: Git, message: String?) = withContext(Dispatchers.IO) { git .stashCreate() .setIncludeUntracked(true) + .apply { + if (message != null) + setWorkingDirectoryMessage(message) + } .call() } diff --git a/src/main/kotlin/app/git/StatusManager.kt b/src/main/kotlin/app/git/StatusManager.kt index 1fdf581..f5aeb73 100644 --- a/src/main/kotlin/app/git/StatusManager.kt +++ b/src/main/kotlin/app/git/StatusManager.kt @@ -39,15 +39,10 @@ class StatusManager @Inject constructor( } suspend fun stage(git: Git, statusEntry: StatusEntry) = withContext(Dispatchers.IO) { - if (statusEntry.statusType == StatusType.REMOVED) { - git.rm() - .addFilepattern(statusEntry.filePath) - .call() - } else { - git.add() - .addFilepattern(statusEntry.filePath) - .call() - } + git.add() + .addFilepattern(statusEntry.filePath) + .setUpdate(statusEntry.statusType == StatusType.REMOVED) + .call() } 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 -> - val lineWithBreak = line +lineDelimiter.orEmpty() + val lineWithBreak = line + lineDelimiter.orEmpty() if (index == splitted.count() - 1 && !content.endsWith(lineWithBreak)) { line @@ -238,6 +233,12 @@ class StatusManager @Inject constructor( git .add() .addFilepattern(".") + .setUpdate(true) // Modified and deleted files + .call() + git + .add() + .addFilepattern(".") + .setUpdate(false) // For newly added files .call() } diff --git a/src/main/kotlin/app/theme/Color.kt b/src/main/kotlin/app/theme/Color.kt index 7e564cf..6cee88f 100644 --- a/src/main/kotlin/app/theme/Color.kt +++ b/src/main/kotlin/app/theme/Color.kt @@ -2,38 +2,75 @@ package app.theme import androidx.compose.ui.graphics.Color -val primaryLight = Color(0xFF9FD1FF) -val primary = Color(0xFF0070D8) -val primaryDark = Color(0xFF014F97) -val onPrimary = Color(0xFFFFFFFFF) -val secondaryLight = Color(0xFF9c27b0) -val secondaryDark = Color(0xFFe9c754) -val mainText = Color(0xFF212934) -val mainTextDark = Color(0xFFFFFFFF) -val secondaryText = Color(0xFF595858) -val secondaryTextDark = Color(0xFFCCCBCB) -val borderColorLight = Color(0xFF989898) -val borderColorDark = Color(0xFF989898) -val errorColor = Color(0xFFc93838) -val onErrorColor = Color(0xFFFFFFFF) +val lightTheme = ColorsScheme( + primary = Color(0xFF0070D8), + primaryVariant = Color(0xFF0070D8), + onPrimary = Color(0xFFFFFFFFF), + secondary = Color(0xFF9c27b0), + primaryText = Color(0xFF212934), + secondaryText = Color(0xFF595858), + error = Color(0xFFc93838), + onError = Color(0xFFFFFFFF), + background = Color(0xFFFFFFFF), + backgroundSelected = Color(0xC0cee1f2), + surface = Color(0xFFe9ecf7), + headerBackground = Color(0xFFF4F6FA), + 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 deleteFileLight = errorColor -val modifyFileLight = primary -val conflictFileLight = Color(0xFFFFB638) +val darkBlueTheme = ColorsScheme( + primary = Color(0xFF014F97), + primaryVariant = Color(0xFF9FD1FF), + 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 unhoverScrollbarColorLight = Color.LightGray -val unhoverScrollbarColorDark = Color.Gray -val hoverScrollbarColorLight = primary -val hoverScrollbarColorDark = Color.LightGray \ No newline at end of file +val darkGrayTheme = ColorsScheme( + primary = Color(0xFF014F97), + primaryVariant = Color(0xFFCDEAFF), + onPrimary = Color(0xFFFFFFFFF), + 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) +) \ No newline at end of file diff --git a/src/main/kotlin/app/theme/ColorsScheme.kt b/src/main/kotlin/app/theme/ColorsScheme.kt new file mode 100644 index 0000000..a8a7cd1 --- /dev/null +++ b/src/main/kotlin/app/theme/ColorsScheme.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/theme/ComponentsColors.kt b/src/main/kotlin/app/theme/ComponentsColors.kt new file mode 100644 index 0000000..5d66d79 --- /dev/null +++ b/src/main/kotlin/app/theme/ComponentsColors.kt @@ -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 +) \ No newline at end of file diff --git a/src/main/kotlin/app/theme/Theme.kt b/src/main/kotlin/app/theme/Theme.kt index 0583071..885d9e4 100644 --- a/src/main/kotlin/app/theme/Theme.kt +++ b/src/main/kotlin/app/theme/Theme.kt @@ -1,44 +1,27 @@ +@file:Suppress("unused") + package app.theme import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme -import androidx.compose.material.darkColors -import androidx.compose.material.lightColors import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import app.DropDownOption -private val DarkColorPalette = darkColors( - 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, -) +private var appTheme: ColorsScheme = darkGrayTheme @Composable -fun AppTheme(theme: Themes = Themes.LIGHT, content: @Composable() () -> Unit) { - val colors = when (theme) { - Themes.LIGHT -> LightColorPalette - Themes.DARK -> DarkColorPalette +fun AppTheme(selectedTheme: Themes = Themes.DARK, content: @Composable() () -> Unit) { + val theme = when (selectedTheme) { + Themes.LIGHT -> lightTheme + Themes.DARK -> darkBlueTheme + Themes.DARK_GRAY -> darkGrayTheme } + appTheme = theme + MaterialTheme( - colors = colors, + colors = theme.toComposeColors(), content = content, typography = typography, ) @@ -46,99 +29,71 @@ fun AppTheme(theme: Themes = Themes.LIGHT, content: @Composable() () -> Unit) { @get:Composable val Colors.backgroundSelected: Color - get() = if (isLight) backgroundColorSelectedLight else backgroundColorSelectedDark + get() = appTheme.backgroundSelected @get:Composable val Colors.primaryTextColor: Color - get() = if (isLight) mainText else mainTextDark - -@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() = appTheme.primaryText @get:Composable val Colors.secondaryTextColor: Color - get() = if (isLight) secondaryText else secondaryTextDark + get() = appTheme.secondaryText @get:Composable val Colors.borderColor: Color - get() = if (isLight) - borderColorLight - else - borderColorDark + get() = appTheme.borderColor @get:Composable val Colors.headerBackground: Color - get() { - return if (isLight) - headerBackgroundLight - else - headerBackgroundDark - } + get() = appTheme.headerBackground @get:Composable val Colors.graphHeaderBackground: Color - get() { - return if (isLight) - headerBackgroundLight - else - graphHeaderBackgroundDark - } + get() = appTheme.graphHeaderBackground @get:Composable val Colors.addFile: Color - get() = addFileLight + get() = appTheme.addFile @get:Composable val Colors.deleteFile: Color - get() = deleteFileLight + get() = appTheme.deletedFile @get:Composable val Colors.modifyFile: Color - get() = modifyFileLight + get() = appTheme.modifiedFile @get:Composable val Colors.conflictFile: Color - get() = conflictFileLight + get() = appTheme.conflictingFile @get:Composable val Colors.headerText: Color - get() = if (isLight) primary else mainTextDark - - -val Colors.tabColorActive: Color - get() = if (isLight) surfaceColorLight else surfaceColorDark - - -val Colors.tabColorInactive: Color - get() = if (isLight) backgroundColorLight else backgroundColorDark + get() = appTheme.onHeader val Colors.stageButton: Color - get() = if (isLight) primary else primaryDark + get() = appTheme.primary val Colors.unstageButton: Color - get() = error + get() = appTheme.error val Colors.abortButton: Color - get() = error + get() = appTheme.error -val Colors.confirmationButton: Color - get() = if (isLight) primary else primaryDark - -val Colors.scrollbarUnhover: Color - get() = if (isLight) unhoverScrollbarColorLight else unhoverScrollbarColorDark +val Colors.scrollbarNormal: Color + get() = appTheme.normalScrollbar 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 { LIGHT("Light"), - DARK("Dark"); + DARK("Dark"), + DARK_GRAY("Dark gray"); override val optionName: String get() = displayName @@ -147,4 +102,5 @@ enum class Themes(val displayName: String) : DropDownOption { val themesList = listOf( Themes.LIGHT, Themes.DARK, + Themes.DARK_GRAY, ) \ No newline at end of file diff --git a/src/main/kotlin/app/ui/AppTab.kt b/src/main/kotlin/app/ui/AppTab.kt index 8a77bd2..e7a27d1 100644 --- a/src/main/kotlin/app/ui/AppTab.kt +++ b/src/main/kotlin/app/ui/AppTab.kt @@ -61,7 +61,8 @@ fun AppTab( LinearProgressIndicator( modifier = Modifier .fillMaxWidth() - .alpha(linearProgressAlpha) + .alpha(linearProgressAlpha), + color = MaterialTheme.colors.primaryVariant ) CredentialsDialog(tabViewModel) diff --git a/src/main/kotlin/app/ui/Blame.kt b/src/main/kotlin/app/ui/Blame.kt index 7a92187..78c80b6 100644 --- a/src/main/kotlin/app/ui/Blame.kt +++ b/src/main/kotlin/app/ui/Blame.kt @@ -4,6 +4,7 @@ package app.ui import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.selection.DisableSelection 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.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember 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.focusRequester 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.pointerHoverIcon import androidx.compose.ui.res.painterResource @@ -24,6 +32,7 @@ import androidx.compose.ui.unit.sp import app.extensions.handMouseClickable import app.extensions.lineAt import app.extensions.toStringWithSpaces +import app.theme.headerBackground import app.theme.primaryTextColor import app.ui.components.PrimaryButton import app.ui.components.ScrollableLazyColumn @@ -37,7 +46,25 @@ fun Blame( onSelectCommit: (RevCommit) -> 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) SelectionContainer { ScrollableLazyColumn( @@ -130,7 +157,7 @@ fun MinimizedBlame( Row( modifier = Modifier .fillMaxWidth() - .height(50.dp) + .height(52.dp) .background(MaterialTheme.colors.surface), verticalAlignment = Alignment.CenterVertically, ) { @@ -181,9 +208,9 @@ private fun Header( Row( modifier = Modifier .fillMaxWidth() - .height(50.dp) - .padding(start = 8.dp, end = 8.dp, top = 8.dp) - .background(MaterialTheme.colors.surface), + .height(40.dp) + .background(MaterialTheme.colors.headerBackground) + .padding(start = 8.dp, end = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( diff --git a/src/main/kotlin/app/ui/Branches.kt b/src/main/kotlin/app/ui/Branches.kt index f8533e5..b21c6fd 100644 --- a/src/main/kotlin/app/ui/Branches.kt +++ b/src/main/kotlin/app/ui/Branches.kt @@ -114,7 +114,7 @@ private fun BranchLineEntry( painter = painterResource("location.svg"), contentDescription = null, modifier = Modifier.padding(horizontal = 4.dp), - tint = MaterialTheme.colors.primary, + tint = MaterialTheme.colors.primaryVariant, ) } } diff --git a/src/main/kotlin/app/ui/CommitChanges.kt b/src/main/kotlin/app/ui/CommitChanges.kt index 4ac0417..a542a38 100644 --- a/src/main/kotlin/app/ui/CommitChanges.kt +++ b/src/main/kotlin/app/ui/CommitChanges.kt @@ -42,7 +42,7 @@ fun CommitChanges( when (val commitChangesStatus = commitChangesStatusState.value) { CommitChangesStatus.Loading -> { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant) } is CommitChangesStatus.Loaded -> { CommitChangesView( @@ -75,7 +75,7 @@ fun CommitChangesView( Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 8.dp), + .padding(end = 8.dp), ) { SelectionContainer { Text( @@ -100,7 +100,7 @@ fun CommitChangesView( modifier = Modifier .fillMaxWidth() .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) ) { Text( diff --git a/src/main/kotlin/app/ui/Diff.kt b/src/main/kotlin/app/ui/Diff.kt index 4af3d70..32c802b 100644 --- a/src/main/kotlin/app/ui/Diff.kt +++ b/src/main/kotlin/app/ui/Diff.kt @@ -4,6 +4,7 @@ package app.ui import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items @@ -13,14 +14,17 @@ import androidx.compose.material.IconButton import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue +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.focusRequester import androidx.compose.ui.graphics.Color 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.pointerHoverIcon import androidx.compose.ui.res.loadImageBitmap @@ -38,6 +42,7 @@ import app.git.diff.DiffResult import app.git.diff.Hunk import app.git.diff.Line import app.git.diff.LineType +import app.theme.headerBackground import app.theme.primaryTextColor import app.theme.stageButton import app.theme.unstageButton @@ -58,11 +63,25 @@ fun Diff( ) { val diffResultState = diffViewModel.diffResult.collectAsState() val viewDiffResult = diffResultState.value ?: return + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } Column( modifier = Modifier .background(MaterialTheme.colors.background) .fillMaxSize() + .focusRequester(focusRequester) + .focusable() + .onPreviewKeyEvent { + if (it.key == Key.Escape) { + onCloseDiffView() + true + } else + false + } ) { when (viewDiffResult) { ViewDiffResult.DiffNotFound -> { onCloseDiffView() } @@ -97,7 +116,7 @@ fun Diff( } } ViewDiffResult.Loading, ViewDiffResult.None -> { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant) } } @@ -303,9 +322,9 @@ fun DiffHeader( Row( modifier = Modifier .fillMaxWidth() - .height(50.dp) - .padding(start = 8.dp, end = 8.dp, top = 8.dp) - .background(MaterialTheme.colors.surface), + .height(40.dp) + .background(MaterialTheme.colors.headerBackground) + .padding(start = 8.dp, end = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { val filePath = if (diffEntry.newPath != "/dev/null") @@ -322,7 +341,7 @@ fun DiffHeader( Spacer(modifier = Modifier.weight(1f)) - if(diffEntryType is DiffEntryType.UncommitedDiff) { + if (diffEntryType is DiffEntryType.UncommitedDiff) { val buttonText: String val color: Color @@ -404,7 +423,10 @@ fun DiffLine( } 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 .padding(start = 8.dp) .fillMaxSize(), diff --git a/src/main/kotlin/app/ui/FileHistory.kt b/src/main/kotlin/app/ui/FileHistory.kt index 8274813..a9c9c8b 100644 --- a/src/main/kotlin/app/ui/FileHistory.kt +++ b/src/main/kotlin/app/ui/FileHistory.kt @@ -4,20 +4,24 @@ package app.ui import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +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.focusRequester 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.pointerHoverIcon import androidx.compose.ui.res.painterResource @@ -27,6 +31,7 @@ import app.extensions.handMouseClickable import app.extensions.toSmartSystemString import app.extensions.toSystemDateTimeString import app.git.diff.DiffResult +import app.theme.headerBackground import app.theme.primaryTextColor import app.theme.secondaryTextColor import app.ui.components.AvatarImage @@ -45,9 +50,24 @@ fun FileHistory( ) { val historyState by historyViewModel.historyState.collectAsState() + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + Column( modifier = Modifier .fillMaxSize() + .focusRequester(focusRequester) + .focusable() + .onKeyEvent { + if (it.key == Key.Escape) { + onClose() + true + } else + false + }, ) { Header(filePath = historyState.filePath, onClose = onClose) @@ -67,9 +87,9 @@ private fun Header( Row( modifier = Modifier .fillMaxWidth() - .height(50.dp) - .padding(start = 8.dp, end = 8.dp, top = 8.dp) - .background(MaterialTheme.colors.surface), + .height(40.dp) + .background(MaterialTheme.colors.headerBackground) + .padding(start = 8.dp, end = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( diff --git a/src/main/kotlin/app/ui/Menu.kt b/src/main/kotlin/app/ui/Menu.kt index bc280e1..7192c2a 100644 --- a/src/main/kotlin/app/ui/Menu.kt +++ b/src/main/kotlin/app/ui/Menu.kt @@ -32,6 +32,7 @@ fun Menu( menuViewModel: MenuViewModel, onRepositoryOpen: () -> Unit, onCreateBranch: () -> Unit, + onStashWithMessage: () -> Unit, ) { var showAdditionalOptionsDropDownMenu by remember { mutableStateOf(false) } @@ -53,6 +54,7 @@ fun Menu( Spacer(modifier = Modifier.weight(1f)) ExtendedMenuButton( + modifier = Modifier.padding(end = 8.dp), title = "Pull", icon = painterResource("download.svg"), onClick = { menuViewModel.pull() }, @@ -80,7 +82,7 @@ fun Menu( ) ) - Spacer(modifier = Modifier.width(16.dp)) + Spacer(modifier = Modifier.width(24.dp)) MenuButton( 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", icon = painterResource("stash.svg"), onClick = { menuViewModel.stash() }, + extendedListItems = stashContextMenuItems( + onStashWithMessage = onStashWithMessage + ) ) MenuButton( @@ -146,16 +152,15 @@ fun MenuButton( onClick: () -> Unit ) { val iconColor = if (enabled) { - MaterialTheme.colors.primary + MaterialTheme.colors.primaryVariant } else { - MaterialTheme.colors.secondaryVariant + MaterialTheme.colors.secondaryVariant //todo this color isn't specified anywhere } Box( modifier = modifier - .padding(horizontal = 2.dp) .handMouseClickable { if (enabled) onClick() } - .border(ButtonDefaults.outlinedBorder, RoundedCornerShape(3.dp)) + .border(ButtonDefaults.outlinedBorder, RoundedCornerShape(4.dp)) .padding(vertical = 8.dp, horizontal = 16.dp), ) { Row( @@ -189,46 +194,42 @@ fun ExtendedMenuButton( extendedListItems: List, ) { val iconColor = if (enabled) { - MaterialTheme.colors.primary + MaterialTheme.colors.primaryVariant } else { MaterialTheme.colors.secondaryVariant } var showDropDownMenu by remember { mutableStateOf(false) } - Row(modifier = Modifier.height(IntrinsicSize.Min)) { - Box( - modifier = modifier + Row(modifier = modifier.height(IntrinsicSize.Min)) { + Row( + modifier = Modifier .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), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, ) { - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - Image( - painter = icon, - contentDescription = title, - modifier = Modifier - .padding(horizontal = 4.dp) - .size(24.dp), - colorFilter = ColorFilter.tint(iconColor), - ) - Text( - text = title, - fontSize = 12.sp, - color = MaterialTheme.colors.primaryTextColor - ) - } + Image( + painter = icon, + contentDescription = title, + modifier = Modifier + .padding(horizontal = 4.dp) + .size(24.dp), + colorFilter = ColorFilter.tint(iconColor), + ) + Text( + text = title, + fontSize = 12.sp, + color = MaterialTheme.colors.primaryTextColor + ) } Box( - modifier = modifier - .padding(end = 8.dp) + modifier = Modifier .width(20.dp) .fillMaxHeight() - .border(ButtonDefaults.outlinedBorder, RoundedCornerShape(topEnd = 3.dp, bottomEnd = 3.dp)) + .border(ButtonDefaults.outlinedBorder, RoundedCornerShape(topEnd = 4.dp, bottomEnd = 4.dp)) .handMouseClickable { showDropDownMenu = true }, @@ -263,7 +264,7 @@ fun IconMenuButton( onClick: () -> Unit ) { val iconColor = if (enabled) { - MaterialTheme.colors.primary + MaterialTheme.colors.primaryVariant } else { MaterialTheme.colors.secondaryVariant } diff --git a/src/main/kotlin/app/ui/RebaseInteractive.kt b/src/main/kotlin/app/ui/RebaseInteractive.kt index 2dd5fd0..6d0ea28 100644 --- a/src/main/kotlin/app/ui/RebaseInteractive.kt +++ b/src/main/kotlin/app/ui/RebaseInteractive.kt @@ -11,7 +11,9 @@ import androidx.compose.ui.Modifier 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 import app.ui.components.ScrollableLazyColumn import app.viewmodels.RebaseInteractiveState @@ -88,7 +90,8 @@ fun RebaseStateLoaded( modifier = Modifier.padding(end = 8.dp), onClick = { onCancel() - } + }, + colors = textButtonColors(), ) { Text("Cancel") } @@ -139,7 +142,7 @@ fun RebaseCommit( newMessage = it onMessageChanged(it) }, - colors = TextFieldDefaults.textFieldColors(backgroundColor = MaterialTheme.colors.background), + colors = outlinedTextFieldColors(), textStyle = TextStyle.Default.copy(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor), ) diff --git a/src/main/kotlin/app/ui/RepositoryOpen.kt b/src/main/kotlin/app/ui/RepositoryOpen.kt index 7b4279f..6ef2874 100644 --- a/src/main/kotlin/app/ui/RepositoryOpen.kt +++ b/src/main/kotlin/app/ui/RepositoryOpen.kt @@ -2,6 +2,7 @@ package app.ui +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -9,12 +10,17 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* 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 app.git.DiffEntryType import app.theme.borderColor import app.theme.primaryTextColor import app.ui.dialogs.NewBranchDialog import app.ui.dialogs.RebaseInteractive +import app.ui.dialogs.StashWithMessageDialog import app.ui.log.Log import app.viewmodels.BlameState import app.viewmodels.TabViewModel @@ -23,7 +29,9 @@ import org.eclipse.jgit.lib.RepositoryState import org.eclipse.jgit.revwalk.RevCommit import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.HorizontalSplitPane +import org.jetbrains.compose.splitpane.SplitterScope import org.jetbrains.compose.splitpane.rememberSplitPaneState +import java.awt.Cursor @OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class) @@ -36,6 +44,7 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { val showHistory by tabViewModel.showHistory.collectAsState() var showNewBranchDialog by remember { mutableStateOf(false) } + var showStashWithMessageDialog by remember { mutableStateOf(false) } if (showNewBranchDialog) { NewBranchDialog( @@ -47,6 +56,16 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { showNewBranchDialog = false } ) + } else if (showStashWithMessageDialog) { + StashWithMessageDialog( + onReject = { + showStashWithMessageDialog = false + }, + onAccept = { stashMessage -> + tabViewModel.menuViewModel.stashWithMessage(stashMessage) + showStashWithMessageDialog = false + } + ) } Column { @@ -65,7 +84,8 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { onRepositoryOpen = { openRepositoryDialog(tabViewModel = tabViewModel) }, - onCreateBranch = { showNewBranchDialog = true } + onCreateBranch = { showNewBranchDialog = true }, + onStashWithMessage = { showStashWithMessageDialog = true }, ) RepoContent(tabViewModel, diffSelected, selectedItem, repositoryState, blameState, showHistory) @@ -83,10 +103,10 @@ fun RepoContent( blameState: BlameState, showHistory: Boolean, ) { - if(showHistory) { + if (showHistory) { val historyViewModel = tabViewModel.historyViewModel - if(historyViewModel != null) { + if (historyViewModel != null) { FileHistory( historyViewModel = historyViewModel, onClose = { @@ -117,11 +137,9 @@ fun MainContentView( ) { Row { HorizontalSplitPane { - first(minSize = 200.dp) { + first(minSize = 250.dp) { Column( modifier = Modifier - .widthIn(min = 300.dp) - .weight(0.15f) .fillMaxHeight() ) { Branches( @@ -139,6 +157,10 @@ fun MainContentView( } } + splitter { + this.repositorySplitter() + } + second { HorizontalSplitPane( splitPaneState = rememberSplitPaneState(0.9f) @@ -147,11 +169,6 @@ fun MainContentView( Box( modifier = Modifier .fillMaxSize() - .border( - width = 2.dp, - color = MaterialTheme.colors.borderColor, - shape = RoundedCornerShape(4.dp) - ) ) { if (blameState is BlameState.Loaded && !blameState.isMinimized) { Blame( @@ -191,6 +208,10 @@ fun MainContentView( } } + splitter { + this.repositorySplitter() + } + second(minSize = 300.dp) { Box( 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 { object None : SelectedItem() object UncommitedChanges : SelectedItem() diff --git a/src/main/kotlin/app/ui/UncommitedChanges.kt b/src/main/kotlin/app/ui/UncommitedChanges.kt index 4ecdad5..1f96b25 100644 --- a/src/main/kotlin/app/ui/UncommitedChanges.kt +++ b/src/main/kotlin/app/ui/UncommitedChanges.kt @@ -40,6 +40,7 @@ import app.ui.components.SecondaryButton import app.ui.context_menu.* import app.viewmodels.StageStatus import app.viewmodels.StatusViewModel +import kotlinx.coroutines.flow.collect import org.eclipse.jgit.lib.RepositoryState @Composable @@ -53,7 +54,7 @@ fun UncommitedChanges( onHistoryFile: (String) -> Unit, ) { 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 staged: List @@ -70,31 +71,36 @@ fun UncommitedChanges( val doCommit = { amend: Boolean -> statusViewModel.commit(commitMessage, amend) onStagedDiffEntrySelected(null) - statusViewModel.savedCommitMessage = "" commitMessage = "" } val canCommit = commitMessage.isNotEmpty() && staged.isNotEmpty() val canAmend = (commitMessage.isNotEmpty() || staged.isNotEmpty()) && statusViewModel.hasPreviousCommits + LaunchedEffect(Unit) { + statusViewModel.commitMessageChangesFlow.collect { newCommitMessage -> + commitMessage = newCommitMessage + } + } + Column { AnimatedVisibility( visible = stageStatus is StageStatus.Loading, enter = fadeIn(), exit = fadeOut(), ) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant) } EntriesList( modifier = Modifier - .padding(start = 8.dp, end = 8.dp, bottom = 4.dp) + .padding(end = 8.dp, bottom = 4.dp) .weight(5f) .fillMaxWidth(), title = "Staged", allActionTitle = "Unstage all", actionTitle = "Unstage", - selectedEntryType = if(selectedEntryType is DiffEntryType.StagedDiff) selectedEntryType else null, + selectedEntryType = if (selectedEntryType is DiffEntryType.StagedDiff) selectedEntryType else null, actionColor = MaterialTheme.colors.unstageButton, statusEntries = staged, onDiffEntrySelected = onStagedDiffEntrySelected, @@ -117,12 +123,12 @@ fun UncommitedChanges( EntriesList( modifier = Modifier - .padding(start = 8.dp, end = 8.dp, top = 4.dp) + .padding(end = 8.dp, top = 8.dp) .weight(5f) .fillMaxWidth(), title = "Unstaged", actionTitle = "Stage", - selectedEntryType = if(selectedEntryType is DiffEntryType.UnstagedDiff) selectedEntryType else null, + selectedEntryType = if (selectedEntryType is DiffEntryType.UnstagedDiff) selectedEntryType else null, actionColor = MaterialTheme.colors.stageButton, statusEntries = unstaged, onDiffEntrySelected = onUnstagedDiffEntrySelected, @@ -149,7 +155,7 @@ fun UncommitedChanges( Column( modifier = Modifier - .padding(8.dp) + .padding(top = 8.dp, bottom = 8.dp, end = 8.dp) .run { // When rebasing, we don't need a fixed size as we don't show the message TextField if (!repositoryState.isRebasing) { @@ -175,23 +181,30 @@ fun UncommitedChanges( value = commitMessage, onValueChange = { commitMessage = it - statusViewModel.savedCommitMessage = it + + statusViewModel.updateCommitMessage(it) }, 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), ) when { repositoryState.isMerging -> MergeButtons( haveConflictsBeenSolved = unstaged.isEmpty(), - onAbort = { statusViewModel.abortMerge() }, + onAbort = { + statusViewModel.abortMerge() + statusViewModel.updateCommitMessage("") + }, onMerge = { doCommit(false) } ) repositoryState.isRebasing -> RebasingButtons( canContinue = staged.isNotEmpty() || unstaged.isNotEmpty(), haveConflictsBeenSolved = unstaged.isEmpty(), - onAbort = { statusViewModel.abortRebase() }, + onAbort = { + statusViewModel.abortRebase() + statusViewModel.updateCommitMessage("") + }, onContinue = { statusViewModel.continueRebase() }, onSkip = { statusViewModel.skipRebase() }, ) @@ -237,7 +250,7 @@ fun UncommitedChangesButtons( modifier = Modifier .height(40.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 } ) { Icon( @@ -367,7 +380,7 @@ fun ConfirmationButton( enabled = enabled, shape = shape, colors = ButtonDefaults.buttonColors( - backgroundColor = MaterialTheme.colors.confirmationButton, + backgroundColor = MaterialTheme.colors.primary, contentColor = Color.White ) ) { @@ -502,7 +515,7 @@ private fun FileEntry( tint = statusEntry.iconColor, ) - if(statusEntry.parentDirectoryPath.isNotEmpty()) { + if (statusEntry.parentDirectoryPath.isNotEmpty()) { Text( text = statusEntry.parentDirectoryPath, modifier = Modifier.weight(1f, fill = false), diff --git a/src/main/kotlin/app/ui/WelcomePage.kt b/src/main/kotlin/app/ui/WelcomePage.kt index 4d5aba4..7d27f68 100644 --- a/src/main/kotlin/app/ui/WelcomePage.kt +++ b/src/main/kotlin/app/ui/WelcomePage.kt @@ -26,6 +26,7 @@ import app.extensions.dirPath import app.extensions.openUrlInBrowser import app.theme.primaryTextColor import app.theme.secondaryTextColor +import app.theme.textButtonColors import app.ui.dialogs.AppInfoDialog import app.ui.dialogs.CloneDialog import app.updates.Update @@ -225,13 +226,13 @@ fun RecentRepositories(appStateManager: AppStateManager, tabViewModel: TabViewMo TextButton( onClick = { tabViewModel.openRepository(repo) - } + }, + colors = textButtonColors(), ) { Text( text = repoDirName, fontSize = 14.sp, maxLines = 1, - color = MaterialTheme.colors.primary, ) } @@ -270,12 +271,13 @@ fun ButtonTile( .size(24.dp), painter = painter, contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colors.primary), + colorFilter = ColorFilter.tint(MaterialTheme.colors.primaryVariant), ) Text( text = title, maxLines = 1, + color = MaterialTheme.colors.primaryVariant, ) } } @@ -286,12 +288,13 @@ fun IconTextButton( modifier: Modifier = Modifier, title: String, painter: Painter, - iconColor: Color = MaterialTheme.colors.primary, + iconColor: Color = MaterialTheme.colors.primaryVariant, onClick: () -> Unit, ) { TextButton( onClick = onClick, - modifier = modifier.size(width = 280.dp, height = 40.dp) + modifier = modifier.size(width = 280.dp, height = 40.dp), + colors = textButtonColors(), ) { Row( modifier = Modifier.fillMaxSize(), diff --git a/src/main/kotlin/app/ui/components/PrimaryButton.kt b/src/main/kotlin/app/ui/components/PrimaryButton.kt index 0799db4..496ea3a 100644 --- a/src/main/kotlin/app/ui/components/PrimaryButton.kt +++ b/src/main/kotlin/app/ui/components/PrimaryButton.kt @@ -21,7 +21,7 @@ fun PrimaryButton( modifier = modifier, enabled = enabled, colors = ButtonDefaults.buttonColors( - backgroundColor = MaterialTheme.colors.primaryVariant, + backgroundColor = MaterialTheme.colors.primary, contentColor = textColor ), ) { diff --git a/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt b/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt index b79e3a7..8b506c7 100644 --- a/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt +++ b/src/main/kotlin/app/ui/components/RepositoriesTabPanel.kt @@ -28,8 +28,6 @@ import app.di.AppComponent import app.di.DaggerTabComponent import app.extensions.handMouseClickable import app.theme.primaryTextColor -import app.theme.tabColorActive -import app.theme.tabColorInactive import app.viewmodels.TabViewModel import javax.inject.Inject import kotlin.io.path.Path @@ -44,9 +42,7 @@ fun RepositoriesTabPanel( onTabClosed: (Int) -> Unit, newTabContent: (key: Int) -> TabInformation, ) { - var tabsIdentifier by remember { - mutableStateOf(tabs.count()) - } + var tabsIdentifier by remember { mutableStateOf(tabs.count()) } TabPanel( modifier = modifier, @@ -123,7 +119,7 @@ fun TabPanel( Icon( imageVector = Icons.Default.Add, contentDescription = null, - tint = MaterialTheme.colors.primary + tint = MaterialTheme.colors.primaryVariant, ) } } @@ -138,9 +134,9 @@ fun Tab(title: MutableState, selected: Boolean, onClick: () -> Unit, onC 0.dp Box { val backgroundColor = if (selected) - MaterialTheme.colors.tabColorActive + MaterialTheme.colors.surface else - MaterialTheme.colors.tabColorInactive + MaterialTheme.colors.background Row( modifier = Modifier diff --git a/src/main/kotlin/app/ui/components/ScrollableLazyColumn.kt b/src/main/kotlin/app/ui/components/ScrollableLazyColumn.kt index 112bf9d..ad630c9 100644 --- a/src/main/kotlin/app/ui/components/ScrollableLazyColumn.kt +++ b/src/main/kotlin/app/ui/components/ScrollableLazyColumn.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import app.theme.scrollbarHover -import app.theme.scrollbarUnhover +import app.theme.scrollbarNormal @Composable fun ScrollableLazyColumn( @@ -35,9 +35,9 @@ fun ScrollableLazyColumn( modifier = Modifier .align(Alignment.CenterEnd) .fillMaxHeight() - .padding(end = 4.dp), + .padding(end = 2.dp), style = LocalScrollbarStyle.current.copy( - unhoverColor = MaterialTheme.colors.scrollbarUnhover, + unhoverColor = MaterialTheme.colors.scrollbarNormal, hoverColor = MaterialTheme.colors.scrollbarHover, ), adapter = rememberScrollbarAdapter( diff --git a/src/main/kotlin/app/ui/components/SideMenuSubentry.kt b/src/main/kotlin/app/ui/components/SideMenuSubentry.kt index 2dcee39..2ba2729 100644 --- a/src/main/kotlin/app/ui/components/SideMenuSubentry.kt +++ b/src/main/kotlin/app/ui/components/SideMenuSubentry.kt @@ -47,7 +47,7 @@ fun SideMenuSubentry( modifier = Modifier .padding(horizontal = 8.dp) .size(16.dp), - tint = MaterialTheme.colors.primary, + tint = MaterialTheme.colors.primaryVariant, ) Text( diff --git a/src/main/kotlin/app/ui/components/TextLink.kt b/src/main/kotlin/app/ui/components/TextLink.kt index f2e0a19..86505f9 100644 --- a/src/main/kotlin/app/ui/components/TextLink.kt +++ b/src/main/kotlin/app/ui/components/TextLink.kt @@ -26,7 +26,7 @@ fun TextLink( val textColor = if (isHovered == colorsInverted) { MaterialTheme.colors.primaryTextColor } else { - MaterialTheme.colors.primary + MaterialTheme.colors.primaryVariant } Text( diff --git a/src/main/kotlin/app/ui/context_menu/StashContextMenu.kt b/src/main/kotlin/app/ui/context_menu/StashContextMenu.kt new file mode 100644 index 0000000..37653a0 --- /dev/null +++ b/src/main/kotlin/app/ui/context_menu/StashContextMenu.kt @@ -0,0 +1,15 @@ +package app.ui.context_menu + +import androidx.compose.foundation.ExperimentalFoundationApi + +@OptIn(ExperimentalFoundationApi::class) +fun stashContextMenuItems( + onStashWithMessage: () -> Unit, +): List { + return mutableListOf( + DropDownContentData( + label = "Stash with message", + onClick = onStashWithMessage, + ), + ) +} diff --git a/src/main/kotlin/app/ui/dialogs/AppInfoDialog.kt b/src/main/kotlin/app/ui/dialogs/AppInfoDialog.kt index b75588e..544c8e5 100644 --- a/src/main/kotlin/app/ui/dialogs/AppInfoDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/AppInfoDialog.kt @@ -14,6 +14,7 @@ import app.AppConstants import app.AppConstants.openSourceProjects import app.Project import app.theme.primaryTextColor +import app.theme.textButtonColors import app.ui.components.ScrollableLazyColumn import app.ui.components.TextLink @@ -21,7 +22,7 @@ import app.ui.components.TextLink fun AppInfoDialog( onClose: () -> Unit, ) { - MaterialDialog { + MaterialDialog(onCloseRequested = onClose) { Column( modifier = Modifier .width(600.dp) @@ -64,7 +65,8 @@ fun AppInfoDialog( modifier = Modifier .padding(top = 16.dp, end = 8.dp) .align(Alignment.End), - onClick = onClose + onClick = onClose, + colors = textButtonColors(), ) { Text("Close") } diff --git a/src/main/kotlin/app/ui/dialogs/CloneDialog.kt b/src/main/kotlin/app/ui/dialogs/CloneDialog.kt index 83567ae..7643284 100644 --- a/src/main/kotlin/app/ui/dialogs/CloneDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/CloneDialog.kt @@ -18,7 +18,9 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.git.CloneStatus +import app.theme.outlinedTextFieldColors import app.theme.primaryTextColor +import app.theme.textButtonColors import app.ui.components.PrimaryButton import app.viewmodels.CloneViewModel import openDirectoryDialog @@ -33,7 +35,7 @@ fun CloneDialog( val cloneStatus = cloneViewModel.cloneStatus.collectAsState() val cloneStatusValue = cloneStatus.value - MaterialDialog { + MaterialDialog(onCloseRequested = onClose) { Box( modifier = Modifier .width(400.dp) @@ -106,6 +108,7 @@ private fun CloneInput( textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor), maxLines = 1, value = url, + colors = outlinedTextFieldColors(), onValueChange = { cloneViewModel.resetStateIfError() url = it @@ -131,6 +134,7 @@ private fun CloneInput( maxLines = 1, label = { Text("Directory") }, value = directory, + colors = outlinedTextFieldColors(), onValueChange = { cloneViewModel.resetStateIfError() directory = it @@ -190,6 +194,7 @@ private fun CloneInput( previous = cloneButtonFocusRequester next = urlFocusRequester }, + colors = textButtonColors(), onClick = { onClose() } @@ -248,6 +253,7 @@ private fun Cloning(cloneViewModel: CloneViewModel, cloneStatusValue: CloneStatu end = 8.dp ) .align(Alignment.End), + colors = textButtonColors(), onClick = { cloneViewModel.cancelClone() } diff --git a/src/main/kotlin/app/ui/dialogs/EditRemotesDialog.kt b/src/main/kotlin/app/ui/dialogs/EditRemotesDialog.kt index 858f439..d5043a4 100644 --- a/src/main/kotlin/app/ui/dialogs/EditRemotesDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/EditRemotesDialog.kt @@ -1,11 +1,9 @@ package app.ui.dialogs import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons 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.unit.dp import app.extensions.handMouseClickable -import app.theme.borderColor -import app.theme.primaryTextColor -import app.theme.secondaryTextColor +import app.theme.* import app.ui.components.PrimaryButton import app.viewmodels.RemotesViewModel import org.eclipse.jgit.transport.RemoteConfig @@ -70,6 +66,7 @@ fun EditRemotesDialog( MaterialDialog( paddingVertical = 8.dp, paddingHorizontal = 16.dp, + onCloseRequested = onDismiss ) { Column( modifier = Modifier @@ -104,11 +101,6 @@ fun EditRemotesDialog( Row( modifier = Modifier .padding(bottom = 8.dp) - .border( - width = 1.dp, - shape = RoundedCornerShape(5.dp), - color = MaterialTheme.colors.borderColor, - ) .background(MaterialTheme.colors.surface) ) { Column( @@ -230,6 +222,7 @@ fun EditRemotesDialog( }, textStyle = TextStyle.Default.copy(color = MaterialTheme.colors.primaryTextColor), maxLines = 1, + colors = outlinedTextFieldColors(), modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp) @@ -251,6 +244,7 @@ fun EditRemotesDialog( }, textStyle = TextStyle.Default.copy(color = MaterialTheme.colors.primaryTextColor), maxLines = 1, + colors = outlinedTextFieldColors(), modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp) @@ -271,6 +265,7 @@ fun EditRemotesDialog( }, textStyle = TextStyle.Default.copy(color = MaterialTheme.colors.primaryTextColor), maxLines = 1, + colors = outlinedTextFieldColors(), modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp) @@ -289,6 +284,7 @@ fun EditRemotesDialog( TextButton( modifier = Modifier.padding(end = 8.dp), enabled = remoteChanged, + colors = textButtonColors(), onClick = { remotesEditorData = remotesEditorData.copy( selectedRemote = selectedRemote.copy( diff --git a/src/main/kotlin/app/ui/dialogs/MaterialDialog.kt b/src/main/kotlin/app/ui/dialogs/MaterialDialog.kt index 0f0c7fa..10220c7 100644 --- a/src/main/kotlin/app/ui/dialogs/MaterialDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/MaterialDialog.kt @@ -1,25 +1,37 @@ +@file:OptIn(ExperimentalComposeUiApi::class) + package app.ui.dialogs import androidx.compose.foundation.background +import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier 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.window.Popup import androidx.compose.ui.window.PopupPositionProvider -import app.theme.dialogBackgroundColor +import app.theme.dialogOverlay @Composable fun MaterialDialog( alignment: Alignment = Alignment.Center, paddingHorizontal: Dp = 16.dp, paddingVertical: Dp = 16.dp, + onCloseRequested: () -> Unit = {}, content: @Composable () -> Unit ) { Popup( @@ -33,10 +45,26 @@ fun MaterialDialog( ): IntOffset = IntOffset.Zero } ) { + + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + Box( modifier = Modifier .fillMaxSize() - .background(dialogBackgroundColor), + .background(MaterialTheme.colors.dialogOverlay) + .focusRequester(focusRequester) + .focusable() + .onPreviewKeyEvent { + if (it.key == Key.Escape) { + onCloseRequested() + true + } else + false + }, contentAlignment = alignment, ) { Box( diff --git a/src/main/kotlin/app/ui/dialogs/MergeDialog.kt b/src/main/kotlin/app/ui/dialogs/MergeDialog.kt index f91319d..172eab5 100644 --- a/src/main/kotlin/app/ui/dialogs/MergeDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/MergeDialog.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.input.pointer.isPrimaryPressed import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.theme.primaryTextColor +import app.theme.textButtonColors import app.ui.components.PrimaryButton @OptIn(ExperimentalFoundationApi::class) @@ -28,7 +29,7 @@ fun MergeDialog( ) { var fastForwardCheck by remember { mutableStateOf(fastForward) } - MaterialDialog { + MaterialDialog(onCloseRequested = onReject) { Column( modifier = Modifier .background(MaterialTheme.colors.background), @@ -89,6 +90,7 @@ fun MergeDialog( ) { TextButton( modifier = Modifier.padding(end = 8.dp), + colors = textButtonColors(), onClick = { onReject() } diff --git a/src/main/kotlin/app/ui/dialogs/NewBranchDialog.kt b/src/main/kotlin/app/ui/dialogs/NewBranchDialog.kt index 932f444..884ff41 100644 --- a/src/main/kotlin/app/ui/dialogs/NewBranchDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/NewBranchDialog.kt @@ -15,7 +15,9 @@ 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) @@ -28,7 +30,7 @@ fun NewBranchDialog( val branchFieldFocusRequester = remember { FocusRequester() } val buttonFieldFocusRequester = remember { FocusRequester() } - MaterialDialog { + MaterialDialog(onCloseRequested = onReject) { Column( modifier = Modifier .background(MaterialTheme.colors.background), @@ -42,7 +44,7 @@ fun NewBranchDialog( } .width(300.dp) .onPreviewKeyEvent { - if (it.key == Key.Enter) { + if (it.key == Key.Enter && branchField.isNotBlank()) { onAccept(branchField) true } else { @@ -53,6 +55,7 @@ fun NewBranchDialog( singleLine = true, label = { Text("New branch name", fontSize = 14.sp) }, textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor), + colors = outlinedTextFieldColors(), onValueChange = { branchField = it }, @@ -64,6 +67,7 @@ fun NewBranchDialog( ) { TextButton( modifier = Modifier.padding(end = 8.dp), + colors = textButtonColors(), onClick = { onReject() } @@ -75,7 +79,7 @@ fun NewBranchDialog( this.previous = branchFieldFocusRequester this.next = branchFieldFocusRequester }, - enabled = branchField.isNotEmpty(), + enabled = branchField.isNotBlank(), onClick = { onAccept(branchField) }, diff --git a/src/main/kotlin/app/ui/dialogs/NewTagDialog.kt b/src/main/kotlin/app/ui/dialogs/NewTagDialog.kt index 647b86c..493552e 100644 --- a/src/main/kotlin/app/ui/dialogs/NewTagDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/NewTagDialog.kt @@ -15,7 +15,9 @@ 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) @@ -28,7 +30,7 @@ fun NewTagDialog( val tagFieldFocusRequester = remember { FocusRequester() } val buttonFieldFocusRequester = remember { FocusRequester() } - MaterialDialog { + MaterialDialog(onCloseRequested = onReject) { Column( modifier = Modifier .background(MaterialTheme.colors.background), @@ -42,7 +44,7 @@ fun NewTagDialog( } .width(300.dp) .onPreviewKeyEvent { - if (it.key == Key.Enter) { + if (it.key == Key.Enter && tagField.isBlank()) { onAccept(tagField) true } else { @@ -53,6 +55,7 @@ fun NewTagDialog( singleLine = true, label = { Text("New tag name", fontSize = 14.sp) }, textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor), + colors = outlinedTextFieldColors(), onValueChange = { tagField = it }, @@ -64,6 +67,7 @@ fun NewTagDialog( ) { TextButton( modifier = Modifier.padding(end = 8.dp), + colors = textButtonColors(), onClick = { onReject() } @@ -75,7 +79,7 @@ fun NewTagDialog( this.previous = tagFieldFocusRequester this.next = tagFieldFocusRequester }, - enabled = tagField.isNotEmpty(), + enabled = tagField.isBlank(), onClick = { onAccept(tagField) }, diff --git a/src/main/kotlin/app/ui/dialogs/PasswordDialog.kt b/src/main/kotlin/app/ui/dialogs/PasswordDialog.kt index 5b026dd..09c9a7f 100644 --- a/src/main/kotlin/app/ui/dialogs/PasswordDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/PasswordDialog.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import app.theme.outlinedTextFieldColors import app.theme.primaryTextColor import app.ui.components.PrimaryButton @@ -29,7 +30,7 @@ fun PasswordDialog( val passwordFieldFocusRequester = remember { FocusRequester() } val buttonFieldFocusRequester = remember { FocusRequester() } - MaterialDialog { + MaterialDialog(onCloseRequested = onReject) { Column( modifier = Modifier .background(MaterialTheme.colors.background), @@ -61,6 +62,7 @@ fun PasswordDialog( singleLine = true, label = { Text("Password", fontSize = 14.sp) }, textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor), + colors = outlinedTextFieldColors(), onValueChange = { passwordField = it }, diff --git a/src/main/kotlin/app/ui/dialogs/RebaseDialog.kt b/src/main/kotlin/app/ui/dialogs/RebaseDialog.kt index ee05da8..8cf9fae 100644 --- a/src/main/kotlin/app/ui/dialogs/RebaseDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/RebaseDialog.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.theme.primaryTextColor +import app.theme.textButtonColors import app.ui.components.PrimaryButton @OptIn(ExperimentalFoundationApi::class) @@ -25,7 +26,7 @@ fun RebaseDialog( onReject: () -> Unit, onAccept: () -> Unit ) { - MaterialDialog { + MaterialDialog(onCloseRequested = onReject) { Column( modifier = Modifier .background(MaterialTheme.colors.background), @@ -67,6 +68,7 @@ fun RebaseDialog( ) { TextButton( modifier = Modifier.padding(end = 8.dp), + colors = textButtonColors(), onClick = { onReject() } diff --git a/src/main/kotlin/app/ui/dialogs/ResetDialog.kt b/src/main/kotlin/app/ui/dialogs/ResetDialog.kt index 30b73e6..ec62f78 100644 --- a/src/main/kotlin/app/ui/dialogs/ResetDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/ResetDialog.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.unit.dp import app.git.ResetType import app.theme.primaryTextColor +import app.theme.textButtonColors import app.ui.components.PrimaryButton @Composable @@ -26,7 +27,7 @@ fun ResetBranchDialog( ) { var resetType by remember { mutableStateOf(ResetType.MIXED) } - MaterialDialog { + MaterialDialog(onCloseRequested = onReject) { Column( modifier = Modifier .background(MaterialTheme.colors.background), @@ -60,6 +61,7 @@ fun ResetBranchDialog( ) { TextButton( modifier = Modifier.padding(end = 8.dp), + colors = textButtonColors(), onClick = { onReject() } diff --git a/src/main/kotlin/app/ui/dialogs/SettingsDialog.kt b/src/main/kotlin/app/ui/dialogs/SettingsDialog.kt index 63fcdfc..b7b1ad7 100644 --- a/src/main/kotlin/app/ui/dialogs/SettingsDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/SettingsDialog.kt @@ -1,16 +1,23 @@ package app.ui.dialogs import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment 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.sp import app.AppPreferences import app.DropDownOption -import app.theme.Themes +import app.theme.outlinedTextFieldColors import app.theme.primaryTextColor +import app.theme.textButtonColors import app.theme.themesList +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @Composable fun SettingsDialog( @@ -18,16 +25,30 @@ fun SettingsDialog( onDismiss: () -> Unit, ) { val currentTheme by appPreferences.themeState.collectAsState() + val commitsLimitEnabled by appPreferences.commitsLimitEnabledFlow.collectAsState() + var commitsLimit by remember { mutableStateOf(appPreferences.commitsLimit) } - MaterialDialog { - Column(modifier = Modifier.width(500.dp)) { + MaterialDialog( + onCloseRequested = { + savePendingSettings( + appPreferences = appPreferences, + commitsLimit = commitsLimit, + ) + + onDismiss() + } + ) { + Column(modifier = Modifier.width(720.dp)) { Text( text = "Settings", color = MaterialTheme.colors.primaryTextColor, + fontSize = 20.sp, + modifier = Modifier.padding(top = 8.dp, bottom = 16.dp, start = 8.dp) ) SettingDropDown( title = "Theme", + subtitle = "Select the UI theme between light and dark mode", dropDownOptions = themesList, currentOption = currentTheme, 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( modifier = Modifier .padding(end = 8.dp) .align(Alignment.End), - onClick = onDismiss + colors = textButtonColors(), + onClick = { + savePendingSettings( + appPreferences = appPreferences, + commitsLimit = commitsLimit, + ) + + onDismiss() + } ) { Text("Close") } @@ -47,9 +95,19 @@ fun SettingsDialog( } } +fun savePendingSettings( + appPreferences: AppPreferences, + commitsLimit: Int, +) { + if (appPreferences.commitsLimit != commitsLimit) { + appPreferences.commitsLimit = commitsLimit + } +} + @Composable -fun SettingDropDown( +fun SettingDropDown( title: String, + subtitle: String, dropDownOptions: List, onOptionSelected: (T) -> Unit, currentOption: T, @@ -59,16 +117,28 @@ fun SettingDropDown( modifier = Modifier.padding(vertical = 8.dp, horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically ) { - Text( - text = title, - color = MaterialTheme.colors.primaryTextColor, - ) + 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)) Box { OutlinedButton(onClick = { showThemeDropdown = true }) { Text( - currentOption.optionName, + text = currentOption.optionName, color = MaterialTheme.colors.primaryTextColor, + fontSize = 14.sp, ) } @@ -93,46 +163,111 @@ fun SettingDropDown( } @Composable -fun SettingTextInput( +fun SettingToogle( title: String, - dropDownOptions: List, - onOptionSelected: (T) -> Unit, - currentOption: T, + subtitle: String, + value: Boolean, + onValueChanged: (Boolean) -> Unit, ) { - var showThemeDropdown by remember { mutableStateOf(false) } Row( modifier = Modifier.padding(vertical = 8.dp, horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically ) { - Text( - text = title, - color = MaterialTheme.colors.primaryTextColor, - ) - Spacer(modifier = Modifier.width(300.dp)) - Box { - OutlinedButton(onClick = { showThemeDropdown = true }) { - Text( - currentOption.optionName, - color = MaterialTheme.colors.primaryTextColor, - ) - } + Column( + verticalArrangement = Arrangement.Center + ) { + Text( + text = title, + color = MaterialTheme.colors.primaryTextColor, + fontSize = 16.sp, + ) - DropdownMenu( - expanded = showThemeDropdown, - onDismissRequest = { showThemeDropdown = false }, - ) { - for (dropDownOption in dropDownOptions) { - DropdownMenuItem( - onClick = { - showThemeDropdown = false - onOptionSelected(dropDownOption) - } - ) { - Text(dropDownOption.optionName) + Text( + text = subtitle, + color = MaterialTheme.colors.primaryTextColor, + modifier = Modifier.padding(top = 4.dp), + fontSize = 12.sp, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Switch(value, onCheckedChange = onValueChanged) + } +} + +@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 } } \ No newline at end of file diff --git a/src/main/kotlin/app/ui/dialogs/StashWithMessageDialog.kt b/src/main/kotlin/app/ui/dialogs/StashWithMessageDialog.kt new file mode 100644 index 0000000..0ce3d1c --- /dev/null +++ b/src/main/kotlin/app/ui/dialogs/StashWithMessageDialog.kt @@ -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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/ui/dialogs/UserPasswordDialog.kt b/src/main/kotlin/app/ui/dialogs/UserPasswordDialog.kt index 83ada7e..943663b 100644 --- a/src/main/kotlin/app/ui/dialogs/UserPasswordDialog.kt +++ b/src/main/kotlin/app/ui/dialogs/UserPasswordDialog.kt @@ -16,7 +16,9 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.PasswordVisualTransformation 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) @@ -33,7 +35,9 @@ fun UserPasswordDialog( val acceptDialog = { onAccept(userField, passwordField) } - MaterialDialog { + MaterialDialog( + onCloseRequested = onReject + ) { Column( modifier = Modifier .background(MaterialTheme.colors.background), @@ -64,6 +68,7 @@ fun UserPasswordDialog( }, value = userField, singleLine = true, + colors = outlinedTextFieldColors(), label = { Text("User", fontSize = 14.sp) }, textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor), onValueChange = { @@ -89,6 +94,7 @@ fun UserPasswordDialog( singleLine = true, label = { Text("Password", fontSize = 14.sp) }, textStyle = TextStyle(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor), + colors = outlinedTextFieldColors(), onValueChange = { passwordField = it }, @@ -102,6 +108,7 @@ fun UserPasswordDialog( ) { TextButton( modifier = Modifier.padding(end = 8.dp), + colors = textButtonColors(), onClick = { onReject() } diff --git a/src/main/kotlin/app/ui/log/Log.kt b/src/main/kotlin/app/ui/log/Log.kt index 653c282..2a462a6 100644 --- a/src/main/kotlin/app/ui/log/Log.kt +++ b/src/main/kotlin/app/ui/log/Log.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.input.key.* import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.PointerIconDefaults import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle @@ -156,6 +157,7 @@ fun Log( graphWidth = graphWidth, scrollState = verticalScrollState, hasUncommitedChanges = hasUncommitedChanges, + commitsLimit = logStatus.commitsLimit, ) // 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, logViewModel = logViewModel, graphWidth = graphWidth, + commitsLimit = logStatus.commitsLimit, onShowLogDialog = { dialog -> logViewModel.showDialog(dialog) - }) + } + ) DividerLog( modifier = Modifier.draggable( @@ -189,7 +193,7 @@ fun Log( HorizontalScrollbar( modifier = Modifier.align(Alignment.BottomStart).width(graphWidth) .padding(start = 4.dp, bottom = 4.dp), style = LocalScrollbarStyle.current.copy( - unhoverColor = MaterialTheme.colors.scrollbarUnhover, + unhoverColor = MaterialTheme.colors.scrollbarNormal, hoverColor = MaterialTheme.colors.scrollbarHover, ), adapter = rememberScrollbarAdapter(horizontalScrollState) ) @@ -226,8 +230,7 @@ fun SearchFilter( Row( modifier = Modifier .fillMaxWidth() - .height(64.dp) - .background(MaterialTheme.colors.graphHeaderBackground), + .height(64.dp), verticalAlignment = Alignment.CenterVertically, ) { TextField( @@ -260,7 +263,7 @@ fun SearchFilter( label = { 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), trailingIcon = { Row( @@ -320,6 +323,7 @@ fun MessagesList( selectedItem: SelectedItem, commitList: GraphCommitList, logViewModel: LogViewModel, + commitsLimit: Int, onShowLogDialog: (LogDialog) -> Unit, 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 { Box(modifier = Modifier.height(20.dp)) } @@ -369,6 +392,7 @@ fun GraphList( hasUncommitedChanges: Boolean, selectedCommit: RevCommit?, selectedItem: SelectedItem, + commitsLimit: Int, ) { val maxLinePosition = if (commitList.isNotEmpty()) 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 { Box(modifier = Modifier.height(20.dp)) } @@ -496,11 +530,14 @@ fun GraphHeader( modifier = Modifier.fillMaxWidth() ) { Row( - modifier = Modifier.fillMaxWidth().height(48.dp).background(MaterialTheme.colors.graphHeaderBackground), + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .background(MaterialTheme.colors.headerBackground), verticalAlignment = Alignment.CenterVertically, ) { Text( - modifier = Modifier.width(graphWidth).padding(start = 8.dp), + modifier = Modifier.width(graphWidth).padding(start = 16.dp), text = "Graph", color = MaterialTheme.colors.headerText, fontSize = 14.sp, @@ -516,7 +553,9 @@ fun GraphHeader( ) Text( - modifier = Modifier.padding(start = 8.dp).weight(1f), + modifier = Modifier + .padding(start = 16.dp) + .weight(1f), text = "Message", color = MaterialTheme.colors.headerText, fontSize = 14.sp, @@ -551,11 +590,9 @@ fun UncommitedChangesLine( modifier = Modifier.height(40.dp) .fillMaxWidth() .clickable { onUncommitedChangesSelected() } - .padding( - start = graphWidth + DIVIDER_WIDTH.dp, - end = 4.dp, - ) - .backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected), + .padding(start = graphWidth) + .backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected) + .padding(DIVIDER_WIDTH.dp), verticalAlignment = Alignment.CenterVertically, ) { val text = when { @@ -800,7 +837,10 @@ fun DividerLog(modifier: Modifier, graphWidth: Dp) { .pointerHoverIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR))) ) { 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) ) } @@ -823,6 +863,10 @@ fun CommitsGraphLine( val passingLanes = plotCommit.passingLanes val forkingOffLanes = plotCommit.forkingOffLanes val mergingLanes = plotCommit.mergingLanes + val density = LocalDensity.current.density + val laneWidthWithDensity = remember(density) { + LANE_WIDTH * density + } Box( modifier = modifier @@ -837,40 +881,40 @@ fun CommitsGraphLine( if (plotCommit.childCount > 0) { drawLine( color = colors[itemPosition % colors.size], - start = Offset(30f * (itemPosition + 1), this.center.y), - end = Offset(30f * (itemPosition + 1), 0f), + start = Offset( laneWidthWithDensity * (itemPosition + 1), this.center.y), + end = Offset( laneWidthWithDensity * (itemPosition + 1), 0f), ) } forkingOffLanes.forEach { plotLane -> drawLine( color = colors[plotLane.position % colors.size], - start = Offset(30f * (itemPosition + 1), this.center.y), - end = Offset(30f * (plotLane.position + 1), 0f), + start = Offset(laneWidthWithDensity * (itemPosition + 1), this.center.y), + end = Offset(laneWidthWithDensity * (plotLane.position + 1), 0f), ) } mergingLanes.forEach { plotLane -> drawLine( color = colors[plotLane.position % colors.size], - start = Offset(30f * (plotLane.position + 1), this.size.height), - end = Offset(30f * (itemPosition + 1), this.center.y), + start = Offset(laneWidthWithDensity * (plotLane.position + 1), this.size.height), + end = Offset(laneWidthWithDensity * (itemPosition + 1), this.center.y), ) } if (plotCommit.parentCount > 0) { drawLine( color = colors[itemPosition % colors.size], - start = Offset(30f * (itemPosition + 1), this.center.y), - end = Offset(30f * (itemPosition + 1), this.size.height), + start = Offset(laneWidthWithDensity * (itemPosition + 1), this.center.y), + end = Offset(laneWidthWithDensity * (itemPosition + 1), this.size.height), ) } passingLanes.forEach { plotLane -> drawLine( color = colors[plotLane.position % colors.size], - start = Offset(30f * (plotLane.position + 1), 0f), - end = Offset(30f * (plotLane.position + 1), this.size.height), + start = Offset(laneWidthWithDensity * (plotLane.position + 1), 0f), + end = Offset(laneWidthWithDensity * (plotLane.position + 1), this.size.height), ) } } @@ -907,6 +951,11 @@ fun UncommitedChangesGraphNode( hasPreviousCommits: Boolean, isSelected: Boolean, ) { + val density = LocalDensity.current.density + + val laneWidthWithDensity = remember(density) { + LANE_WIDTH * density + } Box( modifier = modifier .backgroundIf(isSelected, MaterialTheme.colors.backgroundSelected) @@ -918,14 +967,14 @@ fun UncommitedChangesGraphNode( if (hasPreviousCommits) drawLine( color = colors[0], - start = Offset(30f, this.center.y), - end = Offset(30f, this.size.height), + start = Offset(laneWidthWithDensity, this.center.y), + end = Offset(laneWidthWithDensity, this.size.height), ) drawCircle( color = colors[0], radius = 15f, - center = Offset(30f, this.center.y), + center = Offset(laneWidthWithDensity, this.center.y), ) } } @@ -969,7 +1018,7 @@ fun BranchChip( painter = painterResource("location.svg"), contentDescription = null, 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), painter = painterResource(icon), contentDescription = null, - tint = MaterialTheme.colors.inversePrimaryTextColor, + tint = MaterialTheme.colors.background, ) } Text( diff --git a/src/main/kotlin/app/viewmodels/LogViewModel.kt b/src/main/kotlin/app/viewmodels/LogViewModel.kt index 4c985b8..b10489a 100644 --- a/src/main/kotlin/app/viewmodels/LogViewModel.kt +++ b/src/main/kotlin/app/viewmodels/LogViewModel.kt @@ -1,15 +1,17 @@ package app.viewmodels import androidx.compose.foundation.lazy.LazyListState +import app.AppPreferences import app.git.* import app.git.graph.GraphCommitList import app.git.graph.GraphNode import app.ui.SelectedItem import app.ui.log.LogDialog -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.revwalk.RevCommit @@ -36,6 +38,7 @@ class LogViewModel @Inject constructor( private val mergeManager: MergeManager, private val remoteOperationsManager: RemoteOperationsManager, private val tabState: TabState, + private val appPreferences: AppPreferences, ) { private val _logStatus = MutableStateFlow(LogStatus.Loading) @@ -57,9 +60,24 @@ class LogViewModel @Inject constructor( ) ) + private val scope = CoroutineScope(Dispatchers.IO) + private val _logSearchFilterResults = MutableStateFlow(LogSearch.NotSearching) val logSearchFilterResults: StateFlow = _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) { _logStatus.value = LogStatus.Loading @@ -70,9 +88,19 @@ class LogViewModel @Inject constructor( ) 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 _logSearchFilterResults.value = LogSearch.NotSearching @@ -182,6 +210,7 @@ class LogViewModel @Inject constructor( plotCommitList = previousLogStatusValue.plotCommitList, currentBranch = currentBranch, statusSummary = statsSummary, + commitsLimit = previousLogStatusValue.commitsLimit, ) _logStatus.value = newLogStatusValue @@ -329,6 +358,7 @@ sealed class LogStatus { val plotCommitList: GraphCommitList, val currentBranch: Ref?, val statusSummary: StatusSummary, + val commitsLimit: Int, ) : LogStatus() } diff --git a/src/main/kotlin/app/viewmodels/MenuViewModel.kt b/src/main/kotlin/app/viewmodels/MenuViewModel.kt index c21fdfe..f788752 100644 --- a/src/main/kotlin/app/viewmodels/MenuViewModel.kt +++ b/src/main/kotlin/app/viewmodels/MenuViewModel.kt @@ -35,7 +35,14 @@ class MenuViewModel @Inject constructor( refreshType = RefreshType.UNCOMMITED_CHANGES_AND_LOG, ) { 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( diff --git a/src/main/kotlin/app/viewmodels/StatusViewModel.kt b/src/main/kotlin/app/viewmodels/StatusViewModel.kt index 7120e68..cf686a6 100644 --- a/src/main/kotlin/app/viewmodels/StatusViewModel.kt +++ b/src/main/kotlin/app/viewmodels/StatusViewModel.kt @@ -1,19 +1,21 @@ package app.viewmodels +import app.extensions.isMerging import app.git.* import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.RepositoryState import java.io.File import javax.inject.Inject class StatusViewModel @Inject constructor( private val tabState: TabState, private val statusManager: StatusManager, - private val branchesManager: BranchesManager, - private val repositoryManager: RepositoryManager, private val rebaseManager: RebaseManager, private val mergeManager: MergeManager, private val logManager: LogManager, @@ -21,11 +23,30 @@ class StatusViewModel @Inject constructor( private val _stageStatus = MutableStateFlow(StageStatus.Loaded(listOf(), listOf())) val stageStatus: StateFlow = _stageStatus - var savedCommitMessage: String = "" + var savedCommitMessage = CommitMessage("", MessageType.NORMAL) + var hasPreviousCommits = true // When false, disable "amend previous commit" private var lastUncommitedChangesState = false + /** + * Notify the UI that the commit message has been changed by the view model + */ + private val _commitMessageChangesFlow = MutableSharedFlow() + val commitMessageChangesFlow: SharedFlow = _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( refreshType = RefreshType.UNCOMMITED_CHANGES, ) { git -> @@ -66,6 +87,21 @@ class StatusViewModel @Inject constructor( private suspend fun loadStatus(git: Git) { 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 { _stageStatus.value = StageStatus.Loading 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) { lastUncommitedChangesState = statusManager.hasUncommitedChanges(git) } @@ -92,6 +139,7 @@ class StatusViewModel @Inject constructor( message statusManager.commit(git, commitMessage, amend) + updateCommitMessage("") } suspend fun refresh(git: Git) = withContext(Dispatchers.IO) { @@ -148,6 +196,11 @@ class StatusViewModel @Inject constructor( fileToDelete.delete() } + + fun updateCommitMessage(message: String) { + savedCommitMessage = savedCommitMessage.copy(message = message) + persistMessage() + } } sealed class StageStatus { @@ -155,3 +208,9 @@ sealed class StageStatus { data class Loaded(val staged: List, val unstaged: List) : StageStatus() } +data class CommitMessage(val message: String, val messageType: MessageType) + +enum class MessageType { + NORMAL, + MERGE; +} \ No newline at end of file