Merge branch 'JetpackDuba:main' into main
This commit is contained in:
commit
7f22c833dc
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package app.di
|
||||
|
||||
import app.AppPreferences
|
||||
import app.di.modules.NetworkModule
|
||||
import app.ui.components.TabInformation
|
||||
import dagger.Component
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -39,16 +39,11 @@ 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)
|
||||
.setUpdate(statusEntry.statusType == StatusType.REMOVED)
|
||||
.call()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stageHunk(git: Git, diffEntry: DiffEntry, hunk: Hunk) = withContext(Dispatchers.IO) {
|
||||
val repository = git.repository
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
)
|
47
src/main/kotlin/app/theme/ColorsScheme.kt
Normal file
47
src/main/kotlin/app/theme/ColorsScheme.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
29
src/main/kotlin/app/theme/ComponentsColors.kt
Normal file
29
src/main/kotlin/app/theme/ComponentsColors.kt
Normal 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
|
||||
)
|
@ -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,
|
||||
)
|
@ -61,7 +61,8 @@ fun AppTab(
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.alpha(linearProgressAlpha)
|
||||
.alpha(linearProgressAlpha),
|
||||
color = MaterialTheme.colors.primaryVariant
|
||||
)
|
||||
|
||||
CredentialsDialog(tabViewModel)
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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")
|
||||
@ -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(),
|
||||
|
@ -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(
|
||||
|
@ -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,21 +194,19 @@ 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
|
||||
.handMouseClickable { if (enabled) onClick() }
|
||||
.border(ButtonDefaults.outlinedBorder, RoundedCornerShape(topStart = 3.dp, bottomStart = 3.dp))
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
) {
|
||||
Row(modifier = modifier.height(IntrinsicSize.Min)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.handMouseClickable { if (enabled) onClick() }
|
||||
.border(ButtonDefaults.outlinedBorder, RoundedCornerShape(topStart = 4.dp, bottomStart = 4.dp))
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
@ -221,14 +224,12 @@ fun ExtendedMenuButton(
|
||||
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
|
||||
}
|
||||
|
@ -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),
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
@ -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()
|
||||
|
@ -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,25 +71,30 @@ 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",
|
||||
@ -117,7 +123,7 @@ 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",
|
||||
@ -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
|
||||
)
|
||||
) {
|
||||
|
@ -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(),
|
||||
|
@ -21,7 +21,7 @@ fun PrimaryButton(
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primaryVariant,
|
||||
backgroundColor = MaterialTheme.colors.primary,
|
||||
contentColor = textColor
|
||||
),
|
||||
) {
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -47,7 +47,7 @@ fun SideMenuSubentry(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.size(16.dp),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
tint = MaterialTheme.colors.primaryVariant,
|
||||
)
|
||||
|
||||
Text(
|
||||
|
@ -26,7 +26,7 @@ fun TextLink(
|
||||
val textColor = if (isHovered == colorsInverted) {
|
||||
MaterialTheme.colors.primaryTextColor
|
||||
} else {
|
||||
MaterialTheme.colors.primary
|
||||
MaterialTheme.colors.primaryVariant
|
||||
}
|
||||
|
||||
Text(
|
||||
|
15
src/main/kotlin/app/ui/context_menu/StashContextMenu.kt
Normal file
15
src/main/kotlin/app/ui/context_menu/StashContextMenu.kt
Normal 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,
|
||||
),
|
||||
)
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
},
|
||||
|
@ -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)
|
||||
},
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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(
|
||||
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
|
||||
) {
|
||||
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
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
color = MaterialTheme.colors.primaryTextColor,
|
||||
fontSize = 16.sp,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(300.dp))
|
||||
Box {
|
||||
OutlinedButton(onClick = { showThemeDropdown = true }) {
|
||||
|
||||
Text(
|
||||
currentOption.optionName,
|
||||
text = subtitle,
|
||||
color = MaterialTheme.colors.primaryTextColor,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showThemeDropdown,
|
||||
onDismissRequest = { showThemeDropdown = false },
|
||||
) {
|
||||
for (dropDownOption in dropDownOptions) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showThemeDropdown = false
|
||||
onOptionSelected(dropDownOption)
|
||||
}
|
||||
) {
|
||||
Text(dropDownOption.optionName)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
94
src/main/kotlin/app/ui/dialogs/StashWithMessageDialog.kt
Normal file
94
src/main/kotlin/app/ui/dialogs/StashWithMessageDialog.kt
Normal 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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user