Refactored how commands are executed to support Flatpak properly
Fixes #93
This commit is contained in:
parent
a64ee57283
commit
c2b19a04d2
@ -25,10 +25,11 @@ import androidx.compose.ui.window.application
|
||||
import androidx.compose.ui.window.rememberWindowState
|
||||
import com.jetpackduba.gitnuro.di.DaggerAppComponent
|
||||
import com.jetpackduba.gitnuro.extensions.preferenceValue
|
||||
import com.jetpackduba.gitnuro.extensions.systemSeparator
|
||||
import com.jetpackduba.gitnuro.system.systemSeparator
|
||||
import com.jetpackduba.gitnuro.extensions.toWindowPlacement
|
||||
import com.jetpackduba.gitnuro.git.AppGpgSigner
|
||||
import com.jetpackduba.gitnuro.logging.printError
|
||||
import com.jetpackduba.gitnuro.managers.AppStateManager
|
||||
import com.jetpackduba.gitnuro.preferences.AppSettings
|
||||
import com.jetpackduba.gitnuro.theme.AppTheme
|
||||
import com.jetpackduba.gitnuro.theme.Theme
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.jetpackduba.gitnuro.credentials
|
||||
|
||||
import com.jetpackduba.gitnuro.exceptions.NotSupportedHelper
|
||||
import com.jetpackduba.gitnuro.managers.IShellManager
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.eclipse.jgit.api.Git
|
||||
@ -16,6 +17,7 @@ private const val TIMEOUT_MIN = 1L
|
||||
|
||||
class HttpCredentialsProvider @AssistedInject constructor(
|
||||
private val credentialsStateManager: CredentialsStateManager,
|
||||
private val shellManager: IShellManager,
|
||||
@Assisted val git: Git?,
|
||||
) : CredentialsProvider() {
|
||||
override fun isInteractive(): Boolean {
|
||||
@ -82,8 +84,7 @@ class HttpCredentialsProvider @AssistedInject constructor(
|
||||
externalCredentialsHelper: ExternalCredentialsHelper,
|
||||
credentials: CredentialsAccepted.HttpCredentialsAccepted
|
||||
) {
|
||||
val process = Runtime.getRuntime()
|
||||
.exec(String.format("${externalCredentialsHelper.path} %s", "store"))
|
||||
val process = shellManager.runCommandProcess(listOf(externalCredentialsHelper.path, "store"))
|
||||
|
||||
val output = process.outputStream // write to the input stream of the helper
|
||||
val bufferedWriter = BufferedWriter(OutputStreamWriter(output))
|
||||
@ -119,8 +120,7 @@ class HttpCredentialsProvider @AssistedInject constructor(
|
||||
uri: URIish,
|
||||
items: Array<out CredentialItem>
|
||||
): ExternalCredentialsRequestResult {
|
||||
val process = Runtime.getRuntime()
|
||||
.exec(String.format("${externalCredentialsHelper.path} %s", "get"))
|
||||
val process = shellManager.runCommandProcess(listOf(externalCredentialsHelper.path, "get"))
|
||||
|
||||
val output = process.outputStream // write to the input stream of the helper
|
||||
val input = process.inputStream // reads from the output stream of the helper
|
||||
|
@ -2,10 +2,13 @@ package com.jetpackduba.gitnuro.di
|
||||
|
||||
import com.jetpackduba.gitnuro.App
|
||||
import com.jetpackduba.gitnuro.AppEnvInfo
|
||||
import com.jetpackduba.gitnuro.AppStateManager
|
||||
import com.jetpackduba.gitnuro.credentials.CredentialsStateManager
|
||||
import com.jetpackduba.gitnuro.di.modules.AppModule
|
||||
import com.jetpackduba.gitnuro.di.modules.ShellModule
|
||||
import com.jetpackduba.gitnuro.managers.AppStateManager
|
||||
import com.jetpackduba.gitnuro.managers.IShellManager
|
||||
import com.jetpackduba.gitnuro.preferences.AppSettings
|
||||
import com.jetpackduba.gitnuro.terminal.ITerminalProvider
|
||||
import com.jetpackduba.gitnuro.ui.TabsManager
|
||||
import com.jetpackduba.gitnuro.viewmodels.SettingsViewModel
|
||||
import dagger.Component
|
||||
@ -14,7 +17,8 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
@Component(
|
||||
modules = [
|
||||
AppModule::class
|
||||
AppModule::class,
|
||||
ShellModule::class,
|
||||
]
|
||||
)
|
||||
interface AppComponent {
|
||||
@ -28,4 +32,8 @@ interface AppComponent {
|
||||
fun appEnvInfo(): AppEnvInfo
|
||||
|
||||
fun tabsManager(): TabsManager
|
||||
|
||||
fun shellManager(): IShellManager
|
||||
|
||||
fun terminalProvider(): ITerminalProvider
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package com.jetpackduba.gitnuro.di
|
||||
|
||||
import com.jetpackduba.gitnuro.di.modules.NetworkModule
|
||||
import com.jetpackduba.gitnuro.di.modules.ShellModule
|
||||
import com.jetpackduba.gitnuro.di.modules.TabModule
|
||||
import com.jetpackduba.gitnuro.di.modules.TerminalModule
|
||||
import com.jetpackduba.gitnuro.ui.components.TabInformation
|
||||
import dagger.Component
|
||||
|
||||
@ -11,7 +11,6 @@ import dagger.Component
|
||||
modules = [
|
||||
NetworkModule::class,
|
||||
TabModule::class,
|
||||
TerminalModule::class,
|
||||
],
|
||||
dependencies = [
|
||||
AppComponent::class
|
||||
|
@ -0,0 +1,44 @@
|
||||
package com.jetpackduba.gitnuro.di.modules
|
||||
|
||||
import com.jetpackduba.gitnuro.AppEnvInfo
|
||||
import com.jetpackduba.gitnuro.managers.FlatpakShellManager
|
||||
import com.jetpackduba.gitnuro.managers.IShellManager
|
||||
import com.jetpackduba.gitnuro.managers.ShellManager
|
||||
import com.jetpackduba.gitnuro.system.OS
|
||||
import com.jetpackduba.gitnuro.system.getCurrentOs
|
||||
import com.jetpackduba.gitnuro.terminal.ITerminalProvider
|
||||
import com.jetpackduba.gitnuro.terminal.LinuxTerminalProvider
|
||||
import com.jetpackduba.gitnuro.terminal.MacTerminalProvider
|
||||
import com.jetpackduba.gitnuro.terminal.WindowsTerminalProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Provider
|
||||
|
||||
@Module
|
||||
class ShellModule {
|
||||
@Provides
|
||||
fun provideShellManager(
|
||||
appEnvInfo: AppEnvInfo,
|
||||
shellManager: Provider<ShellManager>,
|
||||
flatpakShellManager: Provider<FlatpakShellManager>,
|
||||
): IShellManager {
|
||||
return if (appEnvInfo.isFlatpak)
|
||||
flatpakShellManager.get()
|
||||
else
|
||||
shellManager.get()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideTerminalProvider(
|
||||
linuxTerminalProvider: Provider<LinuxTerminalProvider>,
|
||||
windowsTerminalProvider: Provider<WindowsTerminalProvider>,
|
||||
macTerminalProvider: Provider<MacTerminalProvider>,
|
||||
): ITerminalProvider {
|
||||
return when (getCurrentOs()) {
|
||||
OS.LINUX -> linuxTerminalProvider.get()
|
||||
OS.WINDOWS -> windowsTerminalProvider.get()
|
||||
OS.MAC -> macTerminalProvider.get()
|
||||
OS.UNKNOWN -> throw NotImplementedError("Unknown operating system")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package com.jetpackduba.gitnuro.di.modules
|
||||
|
||||
import com.jetpackduba.gitnuro.AppEnvInfo
|
||||
import com.jetpackduba.gitnuro.extensions.OS
|
||||
import com.jetpackduba.gitnuro.extensions.getCurrentOs
|
||||
import com.jetpackduba.gitnuro.terminal.*
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Provider
|
||||
|
||||
@Module
|
||||
class TerminalModule {
|
||||
@Provides
|
||||
fun provideTerminalProvider(
|
||||
linuxTerminalProvider: Provider<LinuxTerminalProvider>,
|
||||
windowsTerminalProvider: Provider<WindowsTerminalProvider>,
|
||||
macTerminalProvider: Provider<MacTerminalProvider>,
|
||||
flatpakTerminalProvider: Provider<FlatpakTerminalProvider>,
|
||||
appEnvInfo: AppEnvInfo,
|
||||
): ITerminalProvider {
|
||||
|
||||
if (appEnvInfo.isFlatpak)
|
||||
return flatpakTerminalProvider.get()
|
||||
|
||||
return when (getCurrentOs()) {
|
||||
OS.LINUX -> linuxTerminalProvider.get()
|
||||
OS.WINDOWS -> windowsTerminalProvider.get()
|
||||
OS.MAC -> macTerminalProvider.get()
|
||||
OS.UNKNOWN -> throw NotImplementedError("Unknown operating system")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
package com.jetpackduba.gitnuro.extensions
|
||||
|
||||
import com.jetpackduba.gitnuro.logging.printError
|
||||
import com.jetpackduba.gitnuro.logging.printLog
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
private const val TAG = "Shell"
|
||||
|
||||
fun runCommand(command: String): String? {
|
||||
return try {
|
||||
var result: String?
|
||||
Runtime.getRuntime().exec(command).inputStream.use { inputStream ->
|
||||
Scanner(inputStream).useDelimiter("\\A").use { s ->
|
||||
result = if (s.hasNext())
|
||||
s.next()
|
||||
else
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
} catch (ex: Exception) {
|
||||
ex.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun runCommandInPath(command: List<String>, path: String) {
|
||||
val processBuilder = ProcessBuilder(command).apply {
|
||||
directory(File(path))
|
||||
}
|
||||
|
||||
processBuilder.start()
|
||||
}
|
||||
|
||||
fun runCommandWithoutResult(command: String, args: String, file: String): Boolean {
|
||||
val parts: Array<String> = prepareCommand(command, args, file)
|
||||
|
||||
printLog(TAG, "Running command ${parts.joinToString()}")
|
||||
|
||||
return try {
|
||||
val p = Runtime.getRuntime().exec(parts) ?: return false
|
||||
try {
|
||||
val exitValue = p.exitValue()
|
||||
|
||||
if (exitValue == 0) {
|
||||
printLog(TAG, "Process ended immediately.")
|
||||
false
|
||||
} else {
|
||||
printError(TAG, "Process crashed.")
|
||||
false
|
||||
}
|
||||
} catch (itse: IllegalThreadStateException) {
|
||||
printLog(TAG, "Process is running.")
|
||||
true
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
printError(TAG, "Error running command: ${e.message}", e)
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun prepareCommand(command: String, args: String?, file: String): Array<String> {
|
||||
val parts: MutableList<String> = ArrayList()
|
||||
parts.add(command)
|
||||
|
||||
if (args != null) {
|
||||
for (s in args.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
|
||||
val stringFormatted = String.format(s, file) // put in the filename thing
|
||||
parts.add(stringFormatted.trim { it <= ' ' })
|
||||
}
|
||||
}
|
||||
return parts.toTypedArray()
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package com.jetpackduba.gitnuro.extensions
|
||||
|
||||
import com.jetpackduba.gitnuro.system.systemSeparator
|
||||
import java.math.BigInteger
|
||||
import java.security.MessageDigest
|
||||
|
||||
|
@ -1,89 +0,0 @@
|
||||
package com.jetpackduba.gitnuro.extensions
|
||||
|
||||
import com.jetpackduba.gitnuro.logging.printError
|
||||
import com.jetpackduba.gitnuro.logging.printLog
|
||||
import java.awt.Desktop
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.nio.file.FileSystems
|
||||
|
||||
private const val TAG = "SystemUtils"
|
||||
|
||||
val systemSeparator: String by lazy {
|
||||
FileSystems.getDefault().separator
|
||||
}
|
||||
|
||||
fun openUrlInBrowser(url: String) {
|
||||
if (!openSystemSpecific(url)) {
|
||||
openUrlInBrowserJdk(url)
|
||||
}
|
||||
}
|
||||
|
||||
fun openFileWithExternalApp(filePath: String) {
|
||||
if (!openSystemSpecific(filePath)) {
|
||||
openFileJdk(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openSystemSpecific(url: String): Boolean {
|
||||
when (getCurrentOs()) {
|
||||
OS.LINUX -> {
|
||||
if (runCommandWithoutResult("xdg-open", "%s", url))
|
||||
return true
|
||||
if (runCommandWithoutResult("kde-open", "%s", url))
|
||||
return true
|
||||
if (runCommandWithoutResult("gnome-open", "%s", url))
|
||||
return true
|
||||
}
|
||||
|
||||
OS.WINDOWS -> if (runCommandWithoutResult("explorer", "%s", url)) return true
|
||||
OS.MAC -> if (runCommandWithoutResult("open", "%s", url)) return true
|
||||
else -> printError(TAG, "Unknown OS")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun openUrlInBrowserJdk(url: String) {
|
||||
|
||||
try {
|
||||
Desktop.getDesktop().browse(URI(url))
|
||||
} catch (ex: Exception) {
|
||||
printError(TAG, "Failed to open URL in browser")
|
||||
ex.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openFileJdk(filePath: String) {
|
||||
try {
|
||||
Desktop.getDesktop().open(File(filePath))
|
||||
} catch (ex: Exception) {
|
||||
printError(TAG, "Failed to open URL in browser")
|
||||
ex.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
enum class OS {
|
||||
LINUX,
|
||||
WINDOWS,
|
||||
MAC,
|
||||
UNKNOWN;
|
||||
|
||||
fun isLinux() = this == LINUX
|
||||
|
||||
fun isWindows() = this == WINDOWS
|
||||
|
||||
fun isMac() = this == MAC
|
||||
}
|
||||
|
||||
fun getCurrentOs(): OS {
|
||||
val os = System.getProperty("os.name").lowercase()
|
||||
printLog(TAG, "OS is $os")
|
||||
|
||||
return when {
|
||||
os.contains("linux") -> OS.LINUX
|
||||
os.contains("windows") -> OS.WINDOWS
|
||||
os.contains("mac") -> OS.MAC
|
||||
else -> OS.UNKNOWN
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package com.jetpackduba.gitnuro.git
|
||||
|
||||
import com.jetpackduba.gitnuro.extensions.systemSeparator
|
||||
import com.jetpackduba.gitnuro.system.systemSeparator
|
||||
import com.jetpackduba.gitnuro.logging.printLog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
@ -1,6 +1,6 @@
|
||||
package com.jetpackduba.gitnuro.git
|
||||
|
||||
import com.jetpackduba.gitnuro.TempFilesManager
|
||||
import com.jetpackduba.gitnuro.managers.TempFilesManager
|
||||
import com.jetpackduba.gitnuro.extensions.fileName
|
||||
import org.eclipse.jgit.diff.ContentSource
|
||||
import org.eclipse.jgit.diff.DiffEntry
|
||||
|
@ -1,9 +1,9 @@
|
||||
package com.jetpackduba.gitnuro.git
|
||||
|
||||
import com.jetpackduba.gitnuro.ErrorsManager
|
||||
import com.jetpackduba.gitnuro.managers.ErrorsManager
|
||||
import com.jetpackduba.gitnuro.di.TabScope
|
||||
import com.jetpackduba.gitnuro.extensions.delayedStateChange
|
||||
import com.jetpackduba.gitnuro.newErrorNow
|
||||
import com.jetpackduba.gitnuro.managers.newErrorNow
|
||||
import com.jetpackduba.gitnuro.ui.SelectedItem
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
@ -4,8 +4,8 @@ package com.jetpackduba.gitnuro.keybindings
|
||||
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.input.key.*
|
||||
import com.jetpackduba.gitnuro.extensions.OS
|
||||
import com.jetpackduba.gitnuro.extensions.getCurrentOs
|
||||
import com.jetpackduba.gitnuro.system.OS
|
||||
import com.jetpackduba.gitnuro.system.getCurrentOs
|
||||
|
||||
data class Keybinding(
|
||||
val alt: Boolean = false,
|
||||
|
@ -1,7 +1,7 @@
|
||||
package com.jetpackduba.gitnuro
|
||||
package com.jetpackduba.gitnuro.managers
|
||||
|
||||
import com.jetpackduba.gitnuro.extensions.OS
|
||||
import com.jetpackduba.gitnuro.extensions.getCurrentOs
|
||||
import com.jetpackduba.gitnuro.system.OS
|
||||
import com.jetpackduba.gitnuro.system.getCurrentOs
|
||||
import com.jetpackduba.gitnuro.logging.printError
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
@ -1,4 +1,4 @@
|
||||
package com.jetpackduba.gitnuro
|
||||
package com.jetpackduba.gitnuro.managers
|
||||
|
||||
import com.jetpackduba.gitnuro.di.qualifiers.AppCoroutineScope
|
||||
import com.jetpackduba.gitnuro.preferences.AppSettings
|
@ -1,4 +1,4 @@
|
||||
package com.jetpackduba.gitnuro
|
||||
package com.jetpackduba.gitnuro.managers
|
||||
|
||||
import com.jetpackduba.gitnuro.di.TabScope
|
||||
import kotlinx.coroutines.Dispatchers
|
110
src/main/kotlin/com/jetpackduba/gitnuro/managers/ShellManager.kt
Normal file
110
src/main/kotlin/com/jetpackduba/gitnuro/managers/ShellManager.kt
Normal file
@ -0,0 +1,110 @@
|
||||
package com.jetpackduba.gitnuro.managers
|
||||
|
||||
import com.jetpackduba.gitnuro.logging.printError
|
||||
import com.jetpackduba.gitnuro.logging.printLog
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "ShellManager"
|
||||
|
||||
interface IShellManager {
|
||||
fun runCommand(command: List<String>): String?
|
||||
fun runCommandInPath(command: List<String>, path: String)
|
||||
fun runCommandWithoutResult(command: List<String>): Boolean
|
||||
fun runCommandProcess(command: List<String>): Process
|
||||
}
|
||||
|
||||
class ShellManager @Inject constructor() : IShellManager {
|
||||
override fun runCommand(command: List<String>): String? {
|
||||
printLog(TAG, "runCommand: " + command.joinToString(" "))
|
||||
|
||||
return try {
|
||||
var result: String?
|
||||
|
||||
val processBuilder = ProcessBuilder(command)
|
||||
processBuilder.start().inputStream.use { inputStream ->
|
||||
Scanner(inputStream).useDelimiter("\\A").use { s ->
|
||||
result = if (s.hasNext())
|
||||
s.next()
|
||||
else
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
} catch (ex: Exception) {
|
||||
ex.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun runCommandInPath(command: List<String>, path: String) {
|
||||
printLog(TAG, "runCommandInPath: " + command.joinToString(" "))
|
||||
|
||||
val processBuilder = ProcessBuilder(command).apply {
|
||||
directory(File(path))
|
||||
}
|
||||
|
||||
processBuilder.start()
|
||||
}
|
||||
|
||||
override fun runCommandWithoutResult(command: List<String>): Boolean {
|
||||
printLog(TAG, "runCommandWithoutResult: " + command.joinToString(" "))
|
||||
return try {
|
||||
val processBuilder = ProcessBuilder(command)
|
||||
val p = processBuilder.start() ?: return false
|
||||
|
||||
try {
|
||||
val exitValue = p.exitValue()
|
||||
|
||||
if (exitValue == 0) {
|
||||
printLog(TAG, "Process ended immediately.")
|
||||
false
|
||||
} else {
|
||||
printError(TAG, "Process crashed.")
|
||||
false
|
||||
}
|
||||
} catch (itse: IllegalThreadStateException) {
|
||||
printLog(TAG, "Process is running.")
|
||||
true
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
printError(TAG, "Error running command: ${e.message}", e)
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun runCommandProcess(command: List<String>): Process {
|
||||
printLog(TAG, "runCommandProcess: " + command.joinToString(" "))
|
||||
return ProcessBuilder(command).start()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates [ShellManager] to add the required prefix to commands before running them in a flatpak sandbox environment.
|
||||
*/
|
||||
class FlatpakShellManager @Inject constructor(
|
||||
private val shellManager: ShellManager
|
||||
) : IShellManager {
|
||||
private val flatpakPrefix = listOf("/usr/bin/flatpak-spawn", "--host", "--env=TERM=xterm-256color")
|
||||
|
||||
override fun runCommand(command: List<String>): String? {
|
||||
return shellManager.runCommand(flatpakPrefix + command)
|
||||
}
|
||||
|
||||
override fun runCommandInPath(command: List<String>, path: String) {
|
||||
shellManager.runCommandInPath(flatpakPrefix + command, path)
|
||||
}
|
||||
|
||||
override fun runCommandWithoutResult(command: List<String>): Boolean {
|
||||
return shellManager.runCommandWithoutResult(flatpakPrefix + command)
|
||||
}
|
||||
|
||||
override fun runCommandProcess(command: List<String>): Process {
|
||||
return shellManager.runCommandProcess(flatpakPrefix + command)
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.jetpackduba.gitnuro
|
||||
package com.jetpackduba.gitnuro.managers
|
||||
|
||||
import com.jetpackduba.gitnuro.di.TabScope
|
||||
import com.jetpackduba.gitnuro.extensions.openDirectory
|
36
src/main/kotlin/com/jetpackduba/gitnuro/system/OS.kt
Normal file
36
src/main/kotlin/com/jetpackduba/gitnuro/system/OS.kt
Normal file
@ -0,0 +1,36 @@
|
||||
package com.jetpackduba.gitnuro.system
|
||||
|
||||
import com.jetpackduba.gitnuro.logging.printLog
|
||||
import java.nio.file.FileSystems
|
||||
|
||||
private const val TAG = "OS"
|
||||
|
||||
enum class OS {
|
||||
LINUX,
|
||||
WINDOWS,
|
||||
MAC,
|
||||
UNKNOWN;
|
||||
|
||||
fun isLinux() = this == LINUX
|
||||
|
||||
fun isWindows() = this == WINDOWS
|
||||
|
||||
fun isMac() = this == MAC
|
||||
}
|
||||
|
||||
fun getCurrentOs(): OS {
|
||||
val os = System.getProperty("os.name").lowercase()
|
||||
printLog(TAG, "OS is $os")
|
||||
|
||||
return when {
|
||||
os.contains("linux") -> OS.LINUX
|
||||
os.contains("windows") -> OS.WINDOWS
|
||||
os.contains("mac") -> OS.MAC
|
||||
else -> OS.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val systemSeparator: String by lazy {
|
||||
FileSystems.getDefault().separator
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package com.jetpackduba.gitnuro.system
|
||||
|
||||
import com.jetpackduba.gitnuro.logging.printError
|
||||
import java.awt.Desktop
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "SystemUtils"
|
||||
|
||||
/**
|
||||
* Opens a file with the default external app.
|
||||
* An example would be opening an image with the default image viewer
|
||||
*/
|
||||
class OpenFileInExternalAppUseCase @Inject constructor(
|
||||
private val openPathInSystemUseCase: OpenPathInSystemUseCase
|
||||
) {
|
||||
operator fun invoke(filePath: String) {
|
||||
if (!openPathInSystemUseCase(filePath)) {
|
||||
openFileJdk(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openFileJdk(filePath: String) {
|
||||
try {
|
||||
Desktop.getDesktop().open(File(filePath))
|
||||
} catch (ex: Exception) {
|
||||
printError(TAG, "Failed to open URL in browser")
|
||||
ex.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
package com.jetpackduba.gitnuro.system
|
||||
|
||||
import com.jetpackduba.gitnuro.logging.printLog
|
||||
import com.jetpackduba.gitnuro.managers.ShellManager
|
||||
import java.awt.FileDialog
|
||||
import javax.inject.Inject
|
||||
import javax.swing.JFileChooser
|
||||
import javax.swing.UIManager
|
||||
|
||||
private const val TAG = "SystemDialogs"
|
||||
|
||||
/**
|
||||
* Shows a picker dialog to select a file or directory
|
||||
*/
|
||||
class OpenFilePickerUseCase @Inject constructor(
|
||||
/**
|
||||
* We want specifically [ShellManager] implementation and not [com.jetpackduba.gitnuro.managers.IShellManager],
|
||||
* to run commands without any modification
|
||||
* (such as ones done by [com.jetpackduba.gitnuro.managers.FlatpakShellManager], because it has to run in the sandbox)
|
||||
*/
|
||||
private val shellManager: ShellManager
|
||||
) {
|
||||
operator fun invoke(pickerType: PickerType, basePath: String?): String? {
|
||||
val os = getCurrentOs()
|
||||
val isLinux = os.isLinux()
|
||||
val isMac = os.isMac()
|
||||
|
||||
return if (isLinux) {
|
||||
openDirectoryDialogLinux(pickerType)
|
||||
} else
|
||||
openJvmDialog(pickerType, basePath, false, isMac)
|
||||
}
|
||||
|
||||
private fun openDirectoryDialogLinux(pickerType: PickerType): String? {
|
||||
var dirToOpen: String? = null
|
||||
|
||||
val checkZenityInstalled = shellManager.runCommand(listOf("which", "zenity", "2>/dev/null"))
|
||||
val isZenityInstalled = !checkZenityInstalled.isNullOrEmpty()
|
||||
|
||||
printLog(TAG, "IsZenityInstalled $isZenityInstalled")
|
||||
|
||||
if (isZenityInstalled) {
|
||||
val command = when (pickerType) {
|
||||
PickerType.FILES, PickerType.FILES_AND_DIRECTORIES -> listOf("zenity", "--file-selection", "--title=Open")
|
||||
PickerType.DIRECTORIES -> listOf("zenity", "--file-selection", "--title=Open", "--directory")
|
||||
}
|
||||
|
||||
val openDirectory = shellManager.runCommand(command)?.replace("\n", "")
|
||||
|
||||
if (!openDirectory.isNullOrEmpty())
|
||||
dirToOpen = openDirectory
|
||||
} else
|
||||
dirToOpen = openJvmDialog(pickerType, "", isLinux = true, isMac = false)
|
||||
|
||||
return dirToOpen
|
||||
}
|
||||
|
||||
private fun openJvmDialog(
|
||||
pickerType: PickerType,
|
||||
basePath: String?,
|
||||
isLinux: Boolean,
|
||||
isMac: Boolean
|
||||
): String? {
|
||||
if (!isLinux) {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
|
||||
}
|
||||
|
||||
if (isMac) {
|
||||
System.setProperty("apple.awt.fileDialogForDirectories", "true")
|
||||
val fileChooser = if (basePath.isNullOrEmpty())
|
||||
FileDialog(null as java.awt.Frame?, "Open", FileDialog.LOAD)
|
||||
else
|
||||
FileDialog(null as java.awt.Frame?, "Open", FileDialog.LOAD).apply {
|
||||
directory = basePath
|
||||
}
|
||||
fileChooser.isMultipleMode = false
|
||||
fileChooser.isVisible = true
|
||||
System.setProperty("apple.awt.fileDialogForDirectories", "false")
|
||||
|
||||
if (fileChooser.file != null && fileChooser.directory != null) {
|
||||
return fileChooser.directory + fileChooser.file
|
||||
}
|
||||
|
||||
return null
|
||||
} else {
|
||||
val fileChooser = if (basePath.isNullOrEmpty())
|
||||
JFileChooser()
|
||||
else
|
||||
JFileChooser(basePath)
|
||||
fileChooser.fileSelectionMode = pickerType.value
|
||||
fileChooser.showOpenDialog(null)
|
||||
return if (fileChooser.selectedFile != null)
|
||||
fileChooser.selectedFile.absolutePath
|
||||
else
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class PickerType(val value: Int) {
|
||||
FILES(JFileChooser.FILES_ONLY),
|
||||
DIRECTORIES(JFileChooser.DIRECTORIES_ONLY),
|
||||
FILES_AND_DIRECTORIES(JFileChooser.FILES_AND_DIRECTORIES);
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package com.jetpackduba.gitnuro.system
|
||||
|
||||
import com.jetpackduba.gitnuro.logging.printError
|
||||
import com.jetpackduba.gitnuro.managers.ShellManager
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "OpenPathInSystemUseCase"
|
||||
|
||||
/**
|
||||
* Open a directory in the file explorer.
|
||||
* A use case for this is opening a repository's folder in the file explorer to view or modify the files
|
||||
*/
|
||||
class OpenPathInSystemUseCase @Inject constructor(
|
||||
/**
|
||||
* We want specifically [ShellManager] implementation and not [com.jetpackduba.gitnuro.managers.IShellManager],
|
||||
* to run commands without any modification
|
||||
* (such as ones done by [com.jetpackduba.gitnuro.managers.FlatpakShellManager], because it has to run in the sandbox)
|
||||
*/
|
||||
private val shellManager: ShellManager
|
||||
) {
|
||||
operator fun invoke(path: String): Boolean {
|
||||
when (getCurrentOs()) {
|
||||
OS.LINUX -> {
|
||||
if (shellManager.runCommandWithoutResult(listOf("xdg-open", path)))
|
||||
return true
|
||||
if (shellManager.runCommandWithoutResult(listOf("kde-open", path)))
|
||||
return true
|
||||
if (shellManager.runCommandWithoutResult(listOf("gnome-open", path)))
|
||||
return true
|
||||
}
|
||||
|
||||
OS.WINDOWS -> if (shellManager.runCommandWithoutResult(listOf("explorer", path))) return true
|
||||
OS.MAC -> if (shellManager.runCommandWithoutResult(listOf("open", path))) return true
|
||||
else -> printError(TAG, "Unknown OS")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package com.jetpackduba.gitnuro.system
|
||||
|
||||
import com.jetpackduba.gitnuro.logging.printError
|
||||
import java.awt.Desktop
|
||||
import java.net.URI
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "SystemUtils"
|
||||
|
||||
/**
|
||||
* Opens a URL in the default system browser
|
||||
*/
|
||||
class OpenUrlInBrowserUseCase @Inject constructor(
|
||||
private val openPathInSystemUseCase: OpenPathInSystemUseCase
|
||||
) {
|
||||
operator fun invoke(url: String) {
|
||||
if (!openPathInSystemUseCase(url)) {
|
||||
openUrlInBrowserJdk(url)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun openUrlInBrowserJdk(url: String) {
|
||||
try {
|
||||
Desktop.getDesktop().browse(URI(url))
|
||||
} catch (ex: Exception) {
|
||||
printError(TAG, "Failed to open URL in browser")
|
||||
ex.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package com.jetpackduba.gitnuro.terminal
|
||||
|
||||
import com.jetpackduba.gitnuro.extensions.runCommand
|
||||
import com.jetpackduba.gitnuro.extensions.runCommandInPath
|
||||
import javax.inject.Inject
|
||||
|
||||
private val FLATPAK_PREFIX = listOf("/usr/bin/flatpak-spawn", "--host", "--env=TERM=xterm-256color")
|
||||
|
||||
// TODO Test in flatpak
|
||||
class FlatpakTerminalProvider @Inject constructor(
|
||||
private val linuxTerminalProvider: LinuxTerminalProvider,
|
||||
) : ITerminalProvider {
|
||||
|
||||
override fun getTerminalEmulators(): List<TerminalEmulator> {
|
||||
return linuxTerminalProvider.getTerminalEmulators()
|
||||
}
|
||||
|
||||
override fun isTerminalInstalled(terminalEmulator: TerminalEmulator): Boolean {
|
||||
val checkTerminalInstalled = runCommand("$FLATPAK_PREFIX which ${terminalEmulator.path} 2>/dev/null")
|
||||
|
||||
return !checkTerminalInstalled.isNullOrEmpty()
|
||||
}
|
||||
|
||||
override fun startTerminal(terminalEmulator: TerminalEmulator, repositoryPath: String) {
|
||||
val command = FLATPAK_PREFIX.toMutableList()
|
||||
command.add(terminalEmulator.path)
|
||||
runCommandInPath(command, repositoryPath)
|
||||
}
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
package com.jetpackduba.gitnuro.terminal
|
||||
|
||||
import com.jetpackduba.gitnuro.extensions.runCommand
|
||||
import com.jetpackduba.gitnuro.extensions.runCommandInPath
|
||||
import com.jetpackduba.gitnuro.managers.IShellManager
|
||||
import javax.inject.Inject
|
||||
|
||||
class LinuxTerminalProvider @Inject constructor() : ITerminalProvider {
|
||||
class LinuxTerminalProvider @Inject constructor(
|
||||
private val shellManager: IShellManager
|
||||
) : ITerminalProvider {
|
||||
override fun getTerminalEmulators(): List<TerminalEmulator> {
|
||||
return listOf(
|
||||
TerminalEmulator("Gnome Terminal", "gnome-terminal"),
|
||||
@ -16,12 +17,12 @@ class LinuxTerminalProvider @Inject constructor() : ITerminalProvider {
|
||||
}
|
||||
|
||||
override fun isTerminalInstalled(terminalEmulator: TerminalEmulator): Boolean {
|
||||
val checkTerminalInstalled = runCommand("which ${terminalEmulator.path} 2>/dev/null")
|
||||
val checkTerminalInstalled = shellManager.runCommand(listOf("which", terminalEmulator.path, "2>/dev/null"))
|
||||
|
||||
return !checkTerminalInstalled.isNullOrEmpty()
|
||||
}
|
||||
|
||||
override fun startTerminal(terminalEmulator: TerminalEmulator, repositoryPath: String) {
|
||||
runCommandInPath(listOf(terminalEmulator.path), repositoryPath)
|
||||
shellManager.runCommandInPath(listOf(terminalEmulator.path), repositoryPath)
|
||||
}
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
package com.jetpackduba.gitnuro.terminal
|
||||
|
||||
import com.jetpackduba.gitnuro.extensions.runCommand
|
||||
import com.jetpackduba.gitnuro.extensions.runCommandInPath
|
||||
|
||||
import com.jetpackduba.gitnuro.managers.IShellManager
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO Test this on MacOS
|
||||
class MacTerminalProvider @Inject constructor() : ITerminalProvider {
|
||||
class MacTerminalProvider @Inject constructor(
|
||||
private val shellManager: IShellManager
|
||||
) : ITerminalProvider {
|
||||
override fun getTerminalEmulators(): List<TerminalEmulator> {
|
||||
return listOf(
|
||||
TerminalEmulator("MacOS Terminal", "Terminal")
|
||||
@ -13,12 +15,12 @@ class MacTerminalProvider @Inject constructor() : ITerminalProvider {
|
||||
}
|
||||
|
||||
override fun isTerminalInstalled(terminalEmulator: TerminalEmulator): Boolean {
|
||||
val checkTerminalInstalled = runCommand("which ${terminalEmulator.path} 2>/dev/null")
|
||||
val checkTerminalInstalled = shellManager.runCommand(listOf("which", terminalEmulator.path, "2>/dev/null"))
|
||||
|
||||
return !checkTerminalInstalled.isNullOrEmpty()
|
||||
}
|
||||
|
||||
override fun startTerminal(terminalEmulator: TerminalEmulator, repositoryPath: String) {
|
||||
runCommandInPath(listOf("open", "-a", terminalEmulator.path), repositoryPath)
|
||||
shellManager.runCommandInPath(listOf("open", "-a", terminalEmulator.path), repositoryPath)
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package com.jetpackduba.gitnuro.terminal
|
||||
|
||||
import com.jetpackduba.gitnuro.extensions.runCommandInPath
|
||||
import javax.inject.Inject
|
||||
|
||||
// For flatpak: https://github.com/flathub/com.visualstudio.code#use-host-shell-in-the-integrated-terminal
|
||||
|
@ -1,9 +1,11 @@
|
||||
package com.jetpackduba.gitnuro.terminal
|
||||
|
||||
import com.jetpackduba.gitnuro.extensions.runCommandInPath
|
||||
import com.jetpackduba.gitnuro.managers.IShellManager
|
||||
import javax.inject.Inject
|
||||
|
||||
class WindowsTerminalProvider @Inject constructor() : ITerminalProvider {
|
||||
class WindowsTerminalProvider @Inject constructor(
|
||||
private val shellManager: IShellManager
|
||||
) : ITerminalProvider {
|
||||
override fun getTerminalEmulators(): List<TerminalEmulator> {
|
||||
return listOf(
|
||||
TerminalEmulator("Powershell", "powershell.exe"),
|
||||
@ -16,6 +18,6 @@ class WindowsTerminalProvider @Inject constructor() : ITerminalProvider {
|
||||
}
|
||||
|
||||
override fun startTerminal(terminalEmulator: TerminalEmulator, repositoryPath: String) {
|
||||
runCommandInPath(listOf("cmd", "/c", "start", terminalEmulator.path), repositoryPath)
|
||||
shellManager.runCommandInPath(listOf("cmd", "/c", "start", terminalEmulator.path), repositoryPath)
|
||||
}
|
||||
}
|
@ -136,7 +136,13 @@ fun RepositoryOpenPage(
|
||||
.fillMaxWidth(),
|
||||
onCreateBranch = { showNewBranchDialog = true },
|
||||
onStashWithMessage = { showStashWithMessageDialog = true },
|
||||
onOpenAnotherRepository = { openRepositoryDialog(tabViewModel) },
|
||||
onOpenAnotherRepository = {
|
||||
val repo = tabViewModel.openDirectoryPicker()
|
||||
|
||||
if (repo != null) {
|
||||
tabViewModel.openRepository(repo)
|
||||
}
|
||||
},
|
||||
onQuickActions = { showQuickActionsDialog = true },
|
||||
onShowSettingsDialog = onShowSettingsDialog
|
||||
)
|
||||
|
@ -1,121 +0,0 @@
|
||||
package com.jetpackduba.gitnuro.ui
|
||||
|
||||
import com.jetpackduba.gitnuro.extensions.getCurrentOs
|
||||
import com.jetpackduba.gitnuro.extensions.runCommand
|
||||
import com.jetpackduba.gitnuro.logging.printLog
|
||||
import com.jetpackduba.gitnuro.viewmodels.TabViewModel
|
||||
import java.awt.FileDialog
|
||||
import javax.swing.JFileChooser
|
||||
import javax.swing.UIManager
|
||||
|
||||
private const val TAG = "SystemDialogs"
|
||||
|
||||
fun openDirectoryDialog(basePath: String? = null): String? {
|
||||
return openPickerDialog(
|
||||
pickerType = PickerType.DIRECTORIES,
|
||||
basePath = basePath,
|
||||
)
|
||||
}
|
||||
|
||||
fun openFileDialog(basePath: String? = null): String? {
|
||||
return openPickerDialog(
|
||||
pickerType = PickerType.FILES,
|
||||
basePath = basePath,
|
||||
)
|
||||
}
|
||||
|
||||
fun openRepositoryDialog(tabViewModel: TabViewModel) {
|
||||
val appStateManager = tabViewModel.appStateManager
|
||||
val latestDirectoryOpened = appStateManager.latestOpenedRepositoryPath
|
||||
|
||||
val dirToOpen = openDirectoryDialog(latestDirectoryOpened)
|
||||
if (dirToOpen != null)
|
||||
tabViewModel.openRepository(dirToOpen)
|
||||
}
|
||||
|
||||
private fun openPickerDialog(
|
||||
pickerType: PickerType,
|
||||
basePath: String?,
|
||||
): String? {
|
||||
val os = getCurrentOs()
|
||||
val isLinux = os.isLinux()
|
||||
val isMac = os.isMac()
|
||||
|
||||
return if (isLinux) {
|
||||
openDirectoryDialogLinux(pickerType)
|
||||
} else
|
||||
openJvmDialog(pickerType, basePath, false, isMac)
|
||||
}
|
||||
|
||||
enum class PickerType(val value: Int) {
|
||||
FILES(JFileChooser.FILES_ONLY),
|
||||
DIRECTORIES(JFileChooser.DIRECTORIES_ONLY),
|
||||
FILES_AND_DIRECTORIES(JFileChooser.FILES_AND_DIRECTORIES);
|
||||
}
|
||||
|
||||
|
||||
fun openDirectoryDialogLinux(pickerType: PickerType): String? {
|
||||
var dirToOpen: String? = null
|
||||
|
||||
val checkZenityInstalled = runCommand("which zenity 2>/dev/null")
|
||||
val isZenityInstalled = !checkZenityInstalled.isNullOrEmpty()
|
||||
|
||||
printLog(TAG, "IsZenityInstalled $isZenityInstalled")
|
||||
|
||||
if (isZenityInstalled) {
|
||||
|
||||
val command = when (pickerType) {
|
||||
PickerType.FILES, PickerType.FILES_AND_DIRECTORIES -> "zenity --file-selection --title=Open"
|
||||
PickerType.DIRECTORIES -> "zenity --file-selection --title=Open --directory"
|
||||
}
|
||||
|
||||
val openDirectory = runCommand(command)?.replace("\n", "")
|
||||
|
||||
if (!openDirectory.isNullOrEmpty())
|
||||
dirToOpen = openDirectory
|
||||
} else
|
||||
dirToOpen = openJvmDialog(pickerType, "", isLinux = true, isMac = false)
|
||||
|
||||
return dirToOpen
|
||||
}
|
||||
|
||||
private fun openJvmDialog(
|
||||
pickerType: PickerType,
|
||||
basePath: String?,
|
||||
isLinux: Boolean,
|
||||
isMac: Boolean
|
||||
): String? {
|
||||
if (!isLinux) {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
|
||||
}
|
||||
|
||||
if (isMac) {
|
||||
System.setProperty("apple.awt.fileDialogForDirectories", "true")
|
||||
val fileChooser = if (basePath.isNullOrEmpty())
|
||||
FileDialog(null as java.awt.Frame?, "Open", FileDialog.LOAD)
|
||||
else
|
||||
FileDialog(null as java.awt.Frame?, "Open", FileDialog.LOAD).apply {
|
||||
directory = basePath
|
||||
}
|
||||
fileChooser.isMultipleMode = false
|
||||
fileChooser.isVisible = true
|
||||
System.setProperty("apple.awt.fileDialogForDirectories", "false")
|
||||
|
||||
if (fileChooser.file != null && fileChooser.directory != null) {
|
||||
return fileChooser.directory + fileChooser.file
|
||||
}
|
||||
|
||||
return null
|
||||
} else {
|
||||
val fileChooser = if (basePath.isNullOrEmpty())
|
||||
JFileChooser()
|
||||
else
|
||||
JFileChooser(basePath)
|
||||
fileChooser.fileSelectionMode = pickerType.value
|
||||
fileChooser.showOpenDialog(null)
|
||||
return if (fileChooser.selectedFile != null)
|
||||
fileChooser.selectedFile.absolutePath
|
||||
else
|
||||
null
|
||||
}
|
||||
}
|
@ -24,8 +24,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.jetpackduba.gitnuro.AppConstants
|
||||
import com.jetpackduba.gitnuro.AppIcons
|
||||
import com.jetpackduba.gitnuro.AppStateManager
|
||||
import com.jetpackduba.gitnuro.extensions.*
|
||||
import com.jetpackduba.gitnuro.managers.AppStateManager
|
||||
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
|
||||
import com.jetpackduba.gitnuro.theme.textButtonColors
|
||||
import com.jetpackduba.gitnuro.ui.dialogs.AppInfoDialog
|
||||
@ -63,10 +63,24 @@ fun WelcomePage(
|
||||
) {
|
||||
HomeButtons(
|
||||
newUpdate = newUpdate,
|
||||
tabViewModel = tabViewModel,
|
||||
onOpenRepository = {
|
||||
val repo = tabViewModel.openDirectoryPicker()
|
||||
|
||||
if (repo != null) {
|
||||
tabViewModel.openRepository(repo)
|
||||
}
|
||||
},
|
||||
onStartRepository = {
|
||||
val dir = tabViewModel.openDirectoryPicker()
|
||||
|
||||
if (dir != null) {
|
||||
tabViewModel.initLocalRepository(dir)
|
||||
}
|
||||
},
|
||||
onShowCloneView = onShowCloneDialog,
|
||||
onShowAdditionalInfo = { showAdditionalInfo = true },
|
||||
onShowSettings = onShowSettings,
|
||||
onOpenUrlInBrowser = { url -> tabViewModel.openUrlInBrowser(url) }
|
||||
)
|
||||
|
||||
RecentRepositories(appStateManager, tabViewModel)
|
||||
@ -87,6 +101,7 @@ fun WelcomePage(
|
||||
if (showAdditionalInfo) {
|
||||
AppInfoDialog(
|
||||
onClose = { showAdditionalInfo = false },
|
||||
onOpenUrlInBrowser = { url -> tabViewModel.openUrlInBrowser(url) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -94,10 +109,12 @@ fun WelcomePage(
|
||||
@Composable
|
||||
fun HomeButtons(
|
||||
newUpdate: Update?,
|
||||
tabViewModel: TabViewModel,
|
||||
onOpenRepository: () -> Unit,
|
||||
onStartRepository: () -> Unit,
|
||||
onShowCloneView: () -> Unit,
|
||||
onShowAdditionalInfo: () -> Unit,
|
||||
onShowSettings: () -> Unit,
|
||||
onOpenUrlInBrowser: (String) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(end = 32.dp),
|
||||
@ -113,7 +130,8 @@ fun HomeButtons(
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
title = "Open a repository",
|
||||
painter = painterResource(AppIcons.OPEN),
|
||||
onClick = { openRepositoryDialog(tabViewModel) })
|
||||
onClick = onOpenRepository
|
||||
)
|
||||
|
||||
ButtonTile(
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
@ -126,10 +144,7 @@ fun HomeButtons(
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
title = "Start a local repository",
|
||||
painter = painterResource(AppIcons.OPEN),
|
||||
onClick = {
|
||||
val dir = openDirectoryDialog()
|
||||
if (dir != null) tabViewModel.initLocalRepository(dir)
|
||||
}
|
||||
onClick = onStartRepository
|
||||
)
|
||||
|
||||
Text(
|
||||
@ -142,7 +157,7 @@ fun HomeButtons(
|
||||
title = "Source code",
|
||||
painter = painterResource(AppIcons.CODE),
|
||||
onClick = {
|
||||
openUrlInBrowser("https://github.com/JetpackDuba/Gitnuro")
|
||||
onOpenUrlInBrowser("https://github.com/JetpackDuba/Gitnuro")
|
||||
}
|
||||
)
|
||||
|
||||
@ -150,7 +165,7 @@ fun HomeButtons(
|
||||
title = "Report a bug",
|
||||
painter = painterResource(AppIcons.BUG),
|
||||
onClick = {
|
||||
openUrlInBrowser("https://github.com/JetpackDuba/Gitnuro/issues")
|
||||
onOpenUrlInBrowser("https://github.com/JetpackDuba/Gitnuro/issues")
|
||||
}
|
||||
)
|
||||
|
||||
@ -172,7 +187,7 @@ fun HomeButtons(
|
||||
painter = painterResource(AppIcons.GRADE),
|
||||
iconColor = MaterialTheme.colors.secondary,
|
||||
onClick = {
|
||||
openUrlInBrowser(newUpdate.downloadUrl)
|
||||
onOpenUrlInBrowser(newUpdate.downloadUrl)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.jetpackduba.gitnuro.AppIcons
|
||||
import com.jetpackduba.gitnuro.AppStateManager
|
||||
import com.jetpackduba.gitnuro.managers.AppStateManager
|
||||
import com.jetpackduba.gitnuro.LocalTabScope
|
||||
import com.jetpackduba.gitnuro.di.AppComponent
|
||||
import com.jetpackduba.gitnuro.di.DaggerTabComponent
|
||||
|
@ -10,7 +10,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.jetpackduba.gitnuro.extensions.handMouseClickable
|
||||
import com.jetpackduba.gitnuro.extensions.openUrlInBrowser
|
||||
|
||||
|
||||
@Composable
|
||||
@ -19,6 +18,7 @@ fun TextLink(
|
||||
url: String,
|
||||
modifier: Modifier = Modifier,
|
||||
colorsInverted: Boolean = false,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val hoverInteraction = remember { MutableInteractionSource() }
|
||||
val isHovered by hoverInteraction.collectIsHoveredAsState()
|
||||
@ -29,14 +29,15 @@ fun TextLink(
|
||||
MaterialTheme.colors.primaryVariant
|
||||
}
|
||||
|
||||
Text(
|
||||
TooltipText(
|
||||
text = text,
|
||||
modifier = Modifier
|
||||
.hoverable(hoverInteraction)
|
||||
.handMouseClickable {
|
||||
openUrlInBrowser(url)
|
||||
onClick()
|
||||
}
|
||||
.then(modifier),
|
||||
color = textColor,
|
||||
tooltipTitle = url
|
||||
)
|
||||
}
|
@ -19,6 +19,7 @@ import com.jetpackduba.gitnuro.ui.components.TextLink
|
||||
@Composable
|
||||
fun AppInfoDialog(
|
||||
onClose: () -> Unit,
|
||||
onOpenUrlInBrowser: (String) -> Unit,
|
||||
) {
|
||||
MaterialDialog(onCloseRequested = onClose) {
|
||||
Column(
|
||||
@ -52,7 +53,7 @@ fun AppInfoDialog(
|
||||
}
|
||||
|
||||
items(openSourceProjects) {
|
||||
ProjectUsed(it)
|
||||
ProjectUsed(it, onOpenUrlInBrowser = onOpenUrlInBrowser)
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,7 +71,10 @@ fun AppInfoDialog(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProjectUsed(project: Project) {
|
||||
fun ProjectUsed(
|
||||
project: Project,
|
||||
onOpenUrlInBrowser: (String) -> Unit
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
@ -80,7 +84,8 @@ fun ProjectUsed(project: Project) {
|
||||
text = project.name,
|
||||
url = project.url,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.padding(vertical = 8.dp),
|
||||
onClick = { onOpenUrlInBrowser(project.url) }
|
||||
)
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
@ -90,7 +95,8 @@ fun ProjectUsed(project: Project) {
|
||||
url = project.license.url,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp),
|
||||
colorsInverted = true
|
||||
colorsInverted = true,
|
||||
onClick = { onOpenUrlInBrowser(project.license.url) }
|
||||
)
|
||||
}
|
||||
}
|
@ -27,7 +27,6 @@ import com.jetpackduba.gitnuro.theme.textButtonColors
|
||||
import com.jetpackduba.gitnuro.ui.components.AdjustableOutlinedTextField
|
||||
import com.jetpackduba.gitnuro.ui.components.PrimaryButton
|
||||
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
|
||||
import com.jetpackduba.gitnuro.ui.openDirectoryDialog
|
||||
import com.jetpackduba.gitnuro.viewmodels.CloneViewModel
|
||||
import java.io.File
|
||||
|
||||
@ -152,7 +151,7 @@ private fun CloneInput(
|
||||
IconButton(
|
||||
onClick = {
|
||||
cloneViewModel.resetStateIfError()
|
||||
val newDirectory = openDirectoryDialog()
|
||||
val newDirectory = cloneViewModel.openDirectoryPicker()
|
||||
if (newDirectory != null) {
|
||||
directory = newDirectory
|
||||
cloneViewModel.directory = directory
|
||||
|
@ -12,7 +12,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.jetpackduba.gitnuro.AppIcons
|
||||
import com.jetpackduba.gitnuro.Error
|
||||
import com.jetpackduba.gitnuro.managers.Error
|
||||
import com.jetpackduba.gitnuro.ui.components.PrimaryButton
|
||||
|
||||
@Composable
|
||||
|
@ -13,7 +13,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.jetpackduba.gitnuro.AppIcons
|
||||
import com.jetpackduba.gitnuro.Error
|
||||
import com.jetpackduba.gitnuro.managers.Error
|
||||
import com.jetpackduba.gitnuro.extensions.handMouseClickable
|
||||
import com.jetpackduba.gitnuro.preferences.DEFAULT_UI_SCALE
|
||||
import com.jetpackduba.gitnuro.theme.*
|
||||
@ -25,7 +25,6 @@ import com.jetpackduba.gitnuro.ui.dialogs.ErrorDialog
|
||||
import com.jetpackduba.gitnuro.ui.dialogs.MaterialDialog
|
||||
import com.jetpackduba.gitnuro.ui.dropdowns.DropDownOption
|
||||
import com.jetpackduba.gitnuro.ui.dropdowns.ScaleDropDown
|
||||
import com.jetpackduba.gitnuro.ui.openFileDialog
|
||||
import com.jetpackduba.gitnuro.viewmodels.SettingsViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@ -181,7 +180,7 @@ fun UiSettings(settingsViewModel: SettingsViewModel) {
|
||||
subtitle = "Select a JSON file to load the custom theme",
|
||||
buttonText = "Open file",
|
||||
onClick = {
|
||||
val filePath = openFileDialog()
|
||||
val filePath = settingsViewModel.openFileDialog()
|
||||
|
||||
if (filePath != null) {
|
||||
val error = settingsViewModel.saveCustomTheme(filePath)
|
||||
|
@ -158,7 +158,7 @@ fun Diff(
|
||||
)
|
||||
|
||||
is DiffResult.NonText -> {
|
||||
NonTextDiff(diffResult)
|
||||
NonTextDiff(diffResult, onOpenFileWithExternalApp = { path -> diffViewModel.openFileWithExternalApp(path) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -184,7 +184,7 @@ fun Diff(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NonTextDiff(diffResult: DiffResult.NonText) {
|
||||
fun NonTextDiff(diffResult: DiffResult.NonText, onOpenFileWithExternalApp: (String) -> Unit) {
|
||||
val oldBinaryContent = diffResult.oldBinaryContent
|
||||
val newBinaryContent = diffResult.newBinaryContent
|
||||
|
||||
@ -204,7 +204,7 @@ fun NonTextDiff(diffResult: DiffResult.NonText) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SideTitle("Old")
|
||||
SideDiff(oldBinaryContent)
|
||||
SideDiff(oldBinaryContent, onOpenFileWithExternalApp)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@ -213,7 +213,7 @@ fun NonTextDiff(diffResult: DiffResult.NonText) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SideTitle("New")
|
||||
SideDiff(newBinaryContent)
|
||||
SideDiff(newBinaryContent, onOpenFileWithExternalApp)
|
||||
}
|
||||
} else if (oldBinaryContent != EntryContent.Missing) {
|
||||
Box(
|
||||
@ -221,7 +221,7 @@ fun NonTextDiff(diffResult: DiffResult.NonText) {
|
||||
.fillMaxSize()
|
||||
.padding(all = 24.dp),
|
||||
) {
|
||||
SideDiff(oldBinaryContent)
|
||||
SideDiff(oldBinaryContent, onOpenFileWithExternalApp)
|
||||
}
|
||||
} else if (newBinaryContent != EntryContent.Missing) {
|
||||
Column(
|
||||
@ -232,7 +232,7 @@ fun NonTextDiff(diffResult: DiffResult.NonText) {
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
SideDiff(newBinaryContent)
|
||||
SideDiff(newBinaryContent, onOpenFileWithExternalApp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -248,10 +248,14 @@ fun SideTitle(text: String) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SideDiff(entryContent: EntryContent) {
|
||||
fun SideDiff(entryContent: EntryContent, onOpenFileWithExternalApp: (String) -> Unit) {
|
||||
when (entryContent) {
|
||||
EntryContent.Binary -> BinaryDiff()
|
||||
is EntryContent.ImageBinary -> ImageDiff(entryContent.imagePath, entryContent.contentType)
|
||||
is EntryContent.ImageBinary -> ImageDiff(
|
||||
entryContent.imagePath,
|
||||
entryContent.contentType,
|
||||
onOpenFileWithExternalApp = { onOpenFileWithExternalApp(entryContent.imagePath) }
|
||||
)
|
||||
else -> {
|
||||
}
|
||||
// is EntryContent.Text -> //TODO maybe have a text view if the file was a binary before?
|
||||
@ -260,16 +264,23 @@ fun SideDiff(entryContent: EntryContent) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImageDiff(imagePath: String, contentType: String) {
|
||||
private fun ImageDiff(
|
||||
imagePath: String,
|
||||
contentType: String,
|
||||
onOpenFileWithExternalApp: () -> Unit
|
||||
) {
|
||||
if (animatedImages.contains(contentType)) {
|
||||
AnimatedImage(imagePath)
|
||||
AnimatedImage(imagePath, onOpenFileWithExternalApp)
|
||||
} else {
|
||||
StaticImage(imagePath)
|
||||
StaticImage(imagePath, onOpenFileWithExternalApp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StaticImage(tempImagePath: String) {
|
||||
private fun StaticImage(
|
||||
tempImagePath: String,
|
||||
onOpenFileWithExternalApp: () -> Unit
|
||||
) {
|
||||
var image by remember(tempImagePath) { mutableStateOf<ImageBitmap?>(null) }
|
||||
|
||||
LaunchedEffect(tempImagePath) {
|
||||
@ -295,19 +306,22 @@ private fun StaticImage(tempImagePath: String) {
|
||||
}
|
||||
}
|
||||
.handMouseClickable {
|
||||
openFileWithExternalApp(tempImagePath)
|
||||
onOpenFileWithExternalApp()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimatedImage(imagePath: String) {
|
||||
private fun AnimatedImage(
|
||||
imagePath: String,
|
||||
onOpenFileWithExternalApp: () -> Unit
|
||||
) {
|
||||
Image(
|
||||
bitmap = loadOrNull(imagePath) { loadAnimatedImage(imagePath) }?.animate() ?: ImageBitmap.Blank,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
.handMouseClickable {
|
||||
openFileWithExternalApp(imagePath)
|
||||
onOpenFileWithExternalApp()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package com.jetpackduba.gitnuro.viewmodels
|
||||
import com.jetpackduba.gitnuro.git.CloneStatus
|
||||
import com.jetpackduba.gitnuro.git.TabState
|
||||
import com.jetpackduba.gitnuro.git.remote_operations.CloneRepositoryUseCase
|
||||
import com.jetpackduba.gitnuro.system.OpenFilePickerUseCase
|
||||
import com.jetpackduba.gitnuro.system.PickerType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
@ -15,6 +17,7 @@ import javax.inject.Inject
|
||||
class CloneViewModel @Inject constructor(
|
||||
private val tabState: TabState,
|
||||
private val cloneRepositoryUseCase: CloneRepositoryUseCase,
|
||||
private val openFilePickerUseCase: OpenFilePickerUseCase,
|
||||
) {
|
||||
|
||||
private val _cloneStatus = MutableStateFlow<CloneStatus>(CloneStatus.None)
|
||||
@ -92,4 +95,8 @@ class CloneViewModel @Inject constructor(
|
||||
fun resetStateIfError() {
|
||||
_cloneStatus.value = CloneStatus.None
|
||||
}
|
||||
|
||||
fun openDirectoryPicker(): String? {
|
||||
return openFilePickerUseCase(PickerType.DIRECTORIES, null)
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import com.jetpackduba.gitnuro.git.TabState
|
||||
import com.jetpackduba.gitnuro.git.diff.*
|
||||
import com.jetpackduba.gitnuro.git.workspace.*
|
||||
import com.jetpackduba.gitnuro.preferences.AppSettings
|
||||
import com.jetpackduba.gitnuro.system.OpenFileInExternalAppUseCase
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@ -30,6 +31,7 @@ class DiffViewModel @Inject constructor(
|
||||
private val resetHunkUseCase: ResetHunkUseCase,
|
||||
private val stageEntryUseCase: StageEntryUseCase,
|
||||
private val unstageEntryUseCase: UnstageEntryUseCase,
|
||||
private val openFileInExternalAppUseCase: OpenFileInExternalAppUseCase,
|
||||
private val settings: AppSettings,
|
||||
private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase,
|
||||
tabScope: CoroutineScope,
|
||||
@ -186,6 +188,10 @@ class DiffViewModel @Inject constructor(
|
||||
) { git ->
|
||||
unstageHunkLineUseCase(git, entry, hunk, line)
|
||||
}
|
||||
|
||||
fun openFileWithExternalApp(path: String) {
|
||||
openFileInExternalAppUseCase(path)
|
||||
}
|
||||
}
|
||||
|
||||
enum class TextDiffType(val value: Int) {
|
||||
|
@ -1,9 +1,11 @@
|
||||
package com.jetpackduba.gitnuro.viewmodels
|
||||
|
||||
import com.jetpackduba.gitnuro.Error
|
||||
import com.jetpackduba.gitnuro.di.qualifiers.AppCoroutineScope
|
||||
import com.jetpackduba.gitnuro.newErrorNow
|
||||
import com.jetpackduba.gitnuro.managers.Error
|
||||
import com.jetpackduba.gitnuro.managers.newErrorNow
|
||||
import com.jetpackduba.gitnuro.preferences.AppSettings
|
||||
import com.jetpackduba.gitnuro.system.OpenFilePickerUseCase
|
||||
import com.jetpackduba.gitnuro.system.PickerType
|
||||
import com.jetpackduba.gitnuro.theme.Theme
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
@ -13,6 +15,7 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class SettingsViewModel @Inject constructor(
|
||||
private val appSettings: AppSettings,
|
||||
private val openFilePickerUseCase: OpenFilePickerUseCase,
|
||||
@AppCoroutineScope private val appScope: CoroutineScope,
|
||||
) {
|
||||
// Temporary values to detect changed variables
|
||||
@ -75,4 +78,8 @@ class SettingsViewModel @Inject constructor(
|
||||
appSettings.setCommitsLimit(commitsLimit)
|
||||
}
|
||||
}
|
||||
|
||||
fun openFileDialog(): String? {
|
||||
return openFilePickerUseCase(PickerType.FILES, null)
|
||||
}
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
package com.jetpackduba.gitnuro.viewmodels
|
||||
|
||||
import com.jetpackduba.gitnuro.AppStateManager
|
||||
import com.jetpackduba.gitnuro.ErrorsManager
|
||||
import com.jetpackduba.gitnuro.credentials.CredentialsAccepted
|
||||
import com.jetpackduba.gitnuro.credentials.CredentialsState
|
||||
import com.jetpackduba.gitnuro.credentials.CredentialsStateManager
|
||||
@ -16,8 +14,13 @@ import com.jetpackduba.gitnuro.git.stash.StashChangesUseCase
|
||||
import com.jetpackduba.gitnuro.git.workspace.StageUntrackedFileUseCase
|
||||
import com.jetpackduba.gitnuro.logging.printDebug
|
||||
import com.jetpackduba.gitnuro.logging.printLog
|
||||
import com.jetpackduba.gitnuro.managers.AppStateManager
|
||||
import com.jetpackduba.gitnuro.managers.ErrorsManager
|
||||
import com.jetpackduba.gitnuro.managers.newErrorNow
|
||||
import com.jetpackduba.gitnuro.models.AuthorInfoSimple
|
||||
import com.jetpackduba.gitnuro.newErrorNow
|
||||
import com.jetpackduba.gitnuro.system.OpenFilePickerUseCase
|
||||
import com.jetpackduba.gitnuro.system.OpenUrlInBrowserUseCase
|
||||
import com.jetpackduba.gitnuro.system.PickerType
|
||||
import com.jetpackduba.gitnuro.ui.SelectedItem
|
||||
import com.jetpackduba.gitnuro.updates.Update
|
||||
import com.jetpackduba.gitnuro.updates.UpdatesRepository
|
||||
@ -62,6 +65,8 @@ class TabViewModel @Inject constructor(
|
||||
private val stashChangesUseCase: StashChangesUseCase,
|
||||
private val stageUntrackedFileUseCase: StageUntrackedFileUseCase,
|
||||
private val abortRebaseUseCase: AbortRebaseUseCase,
|
||||
private val openFilePickerUseCase: OpenFilePickerUseCase,
|
||||
private val openUrlInBrowserUseCase: OpenUrlInBrowserUseCase,
|
||||
private val tabScope: CoroutineScope,
|
||||
) {
|
||||
val errorsManager: ErrorsManager = tabState.errorsManager
|
||||
@ -302,11 +307,6 @@ class TabViewModel @Inject constructor(
|
||||
) {
|
||||
updateDiffEntry()
|
||||
tabState.refreshData(RefreshType.UNCOMMITED_CHANGES_AND_LOG)
|
||||
//
|
||||
// // Stashes list should only be updated if we are doing a stash operation, however it's a small operation
|
||||
// // that we can afford to do when doing other operations
|
||||
// stashesViewModel.refresh(git)
|
||||
// loadRepositoryState(git)
|
||||
}
|
||||
|
||||
private suspend fun refreshRepositoryInfo() {
|
||||
@ -347,6 +347,12 @@ class TabViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun openDirectoryPicker(): String? {
|
||||
val latestDirectoryOpened = appStateManager.latestOpenedRepositoryPath
|
||||
|
||||
return openFilePickerUseCase(PickerType.DIRECTORIES, latestDirectoryOpened)
|
||||
}
|
||||
|
||||
fun initLocalRepository(dir: String) = tabState.safeProcessingWithoutGit(
|
||||
showError = true,
|
||||
) {
|
||||
@ -467,6 +473,10 @@ class TabViewModel @Inject constructor(
|
||||
fun cancelOngoingTask() {
|
||||
tabState.cancelCurrentTask()
|
||||
}
|
||||
|
||||
fun openUrlInBrowser(url: String) {
|
||||
openUrlInBrowserUseCase(url)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user