Merge branch 'JetpackDuba:main' into main

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

View File

@ -4,18 +4,11 @@ package app
import androidx.compose.foundation.background
import androidx.compose.foundation.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,
)
}
}

View File

@ -12,6 +12,11 @@ private const val PREFERENCES_NAME = "GitnuroConfig"
private const val PREF_LATEST_REPOSITORIES_TABS_OPENED = "latestRepositoriesTabsOpened"
private const val PREF_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<Themes> = _themeState
private val _commitsLimitEnabledFlow = MutableStateFlow(true)
val commitsLimitEnabledFlow: StateFlow<Boolean> = _commitsLimitEnabledFlow
private val _commitsLimitFlow = MutableStateFlow(commitsLimit)
val commitsLimitFlow: StateFlow<Int> = _commitsLimitFlow
var latestTabsOpened: String
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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,44 +1,27 @@
@file:Suppress("unused")
package app.theme
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,
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<DropDownContentData>,
) {
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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import app.AppConstants
import app.AppConstants.openSourceProjects
import app.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")
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <T: DropDownOption> SettingDropDown(
fun <T : DropDownOption> SettingDropDown(
title: String,
subtitle: String,
dropDownOptions: List<T>,
onOptionSelected: (T) -> Unit,
currentOption: T,
@ -59,16 +117,28 @@ fun <T: DropDownOption> 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 <T: DropDownOption> SettingDropDown(
}
@Composable
fun <T: DropDownOption> SettingTextInput(
fun SettingToogle(
title: String,
dropDownOptions: List<T>,
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,
)
}
DropdownMenu(
expanded = showThemeDropdown,
onDismissRequest = { showThemeDropdown = false },
) {
for (dropDownOption in dropDownOptions) {
DropdownMenuItem(
onClick = {
showThemeDropdown = false
onOptionSelected(dropDownOption)
}
) {
Text(dropDownOption.optionName)
}
}
}
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))
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
}
}

View File

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

View File

@ -16,7 +16,9 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.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()
}

View File

@ -30,6 +30,7 @@ import androidx.compose.ui.input.key.*
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.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(

View File

@ -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>(LogStatus.Loading)
@ -57,9 +60,24 @@ class LogViewModel @Inject constructor(
)
)
private val scope = CoroutineScope(Dispatchers.IO)
private val _logSearchFilterResults = MutableStateFlow<LogSearch>(LogSearch.NotSearching)
val logSearchFilterResults: StateFlow<LogSearch> = _logSearchFilterResults
init {
scope.launch {
appPreferences.commitsLimitEnabledFlow.collect {
tabState.refreshData(RefreshType.ONLY_LOG)
}
}
scope.launch {
appPreferences.commitsLimitFlow.collect {
tabState.refreshData(RefreshType.ONLY_LOG)
}
}
}
private suspend fun loadLog(git: Git) {
_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()
}

View File

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

View File

@ -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>(StageStatus.Loaded(listOf(), listOf()))
val stageStatus: StateFlow<StageStatus> = _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<String>()
val commitMessageChangesFlow: SharedFlow<String> = _commitMessageChangesFlow
private fun persistMessage() = tabState.runOperation(
refreshType = RefreshType.NONE,
) { git ->
val messageToPersist = savedCommitMessage.message.ifBlank { null }
if (git.repository.repositoryState.isMerging) {
git.repository.writeMergeCommitMsg(messageToPersist)
} else if (git.repository.repositoryState == RepositoryState.SAFE) {
git.repository.writeCommitEditMsg(messageToPersist)
}
}
fun stage(statusEntry: StatusEntry) = tabState.runOperation(
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<StatusEntry>, val unstaged: List<StatusEntry>) : StageStatus()
}
data class CommitMessage(val message: String, val messageType: MessageType)
enum class MessageType {
NORMAL,
MERGE;
}