Refactored how commands are executed to support Flatpak properly

Fixes #93
This commit is contained in:
Abdelilah El Aissaoui 2023-04-17 16:47:32 +02:00
parent a64ee57283
commit c2b19a04d2
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
42 changed files with 564 additions and 435 deletions

View File

@ -25,10 +25,11 @@ import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
import com.jetpackduba.gitnuro.di.DaggerAppComponent import com.jetpackduba.gitnuro.di.DaggerAppComponent
import com.jetpackduba.gitnuro.extensions.preferenceValue 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.extensions.toWindowPlacement
import com.jetpackduba.gitnuro.git.AppGpgSigner import com.jetpackduba.gitnuro.git.AppGpgSigner
import com.jetpackduba.gitnuro.logging.printError import com.jetpackduba.gitnuro.logging.printError
import com.jetpackduba.gitnuro.managers.AppStateManager
import com.jetpackduba.gitnuro.preferences.AppSettings import com.jetpackduba.gitnuro.preferences.AppSettings
import com.jetpackduba.gitnuro.theme.AppTheme import com.jetpackduba.gitnuro.theme.AppTheme
import com.jetpackduba.gitnuro.theme.Theme import com.jetpackduba.gitnuro.theme.Theme

View File

@ -1,6 +1,7 @@
package com.jetpackduba.gitnuro.credentials package com.jetpackduba.gitnuro.credentials
import com.jetpackduba.gitnuro.exceptions.NotSupportedHelper import com.jetpackduba.gitnuro.exceptions.NotSupportedHelper
import com.jetpackduba.gitnuro.managers.IShellManager
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
@ -16,6 +17,7 @@ private const val TIMEOUT_MIN = 1L
class HttpCredentialsProvider @AssistedInject constructor( class HttpCredentialsProvider @AssistedInject constructor(
private val credentialsStateManager: CredentialsStateManager, private val credentialsStateManager: CredentialsStateManager,
private val shellManager: IShellManager,
@Assisted val git: Git?, @Assisted val git: Git?,
) : CredentialsProvider() { ) : CredentialsProvider() {
override fun isInteractive(): Boolean { override fun isInteractive(): Boolean {
@ -82,8 +84,7 @@ class HttpCredentialsProvider @AssistedInject constructor(
externalCredentialsHelper: ExternalCredentialsHelper, externalCredentialsHelper: ExternalCredentialsHelper,
credentials: CredentialsAccepted.HttpCredentialsAccepted credentials: CredentialsAccepted.HttpCredentialsAccepted
) { ) {
val process = Runtime.getRuntime() val process = shellManager.runCommandProcess(listOf(externalCredentialsHelper.path, "store"))
.exec(String.format("${externalCredentialsHelper.path} %s", "store"))
val output = process.outputStream // write to the input stream of the helper val output = process.outputStream // write to the input stream of the helper
val bufferedWriter = BufferedWriter(OutputStreamWriter(output)) val bufferedWriter = BufferedWriter(OutputStreamWriter(output))
@ -119,8 +120,7 @@ class HttpCredentialsProvider @AssistedInject constructor(
uri: URIish, uri: URIish,
items: Array<out CredentialItem> items: Array<out CredentialItem>
): ExternalCredentialsRequestResult { ): ExternalCredentialsRequestResult {
val process = Runtime.getRuntime() val process = shellManager.runCommandProcess(listOf(externalCredentialsHelper.path, "get"))
.exec(String.format("${externalCredentialsHelper.path} %s", "get"))
val output = process.outputStream // write to the input stream of the helper val output = process.outputStream // write to the input stream of the helper
val input = process.inputStream // reads from the output stream of the helper val input = process.inputStream // reads from the output stream of the helper

View File

@ -2,10 +2,13 @@ package com.jetpackduba.gitnuro.di
import com.jetpackduba.gitnuro.App import com.jetpackduba.gitnuro.App
import com.jetpackduba.gitnuro.AppEnvInfo import com.jetpackduba.gitnuro.AppEnvInfo
import com.jetpackduba.gitnuro.AppStateManager
import com.jetpackduba.gitnuro.credentials.CredentialsStateManager import com.jetpackduba.gitnuro.credentials.CredentialsStateManager
import com.jetpackduba.gitnuro.di.modules.AppModule 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.preferences.AppSettings
import com.jetpackduba.gitnuro.terminal.ITerminalProvider
import com.jetpackduba.gitnuro.ui.TabsManager import com.jetpackduba.gitnuro.ui.TabsManager
import com.jetpackduba.gitnuro.viewmodels.SettingsViewModel import com.jetpackduba.gitnuro.viewmodels.SettingsViewModel
import dagger.Component import dagger.Component
@ -14,7 +17,8 @@ import javax.inject.Singleton
@Singleton @Singleton
@Component( @Component(
modules = [ modules = [
AppModule::class AppModule::class,
ShellModule::class,
] ]
) )
interface AppComponent { interface AppComponent {
@ -28,4 +32,8 @@ interface AppComponent {
fun appEnvInfo(): AppEnvInfo fun appEnvInfo(): AppEnvInfo
fun tabsManager(): TabsManager fun tabsManager(): TabsManager
fun shellManager(): IShellManager
fun terminalProvider(): ITerminalProvider
} }

View File

@ -1,8 +1,8 @@
package com.jetpackduba.gitnuro.di package com.jetpackduba.gitnuro.di
import com.jetpackduba.gitnuro.di.modules.NetworkModule 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.TabModule
import com.jetpackduba.gitnuro.di.modules.TerminalModule
import com.jetpackduba.gitnuro.ui.components.TabInformation import com.jetpackduba.gitnuro.ui.components.TabInformation
import dagger.Component import dagger.Component
@ -11,7 +11,6 @@ import dagger.Component
modules = [ modules = [
NetworkModule::class, NetworkModule::class,
TabModule::class, TabModule::class,
TerminalModule::class,
], ],
dependencies = [ dependencies = [
AppComponent::class AppComponent::class

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package com.jetpackduba.gitnuro.extensions package com.jetpackduba.gitnuro.extensions
import com.jetpackduba.gitnuro.system.systemSeparator
import java.math.BigInteger import java.math.BigInteger
import java.security.MessageDigest import java.security.MessageDigest

View File

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

View File

@ -1,6 +1,6 @@
package com.jetpackduba.gitnuro.git package com.jetpackduba.gitnuro.git
import com.jetpackduba.gitnuro.extensions.systemSeparator import com.jetpackduba.gitnuro.system.systemSeparator
import com.jetpackduba.gitnuro.logging.printLog import com.jetpackduba.gitnuro.logging.printLog
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow

View File

@ -1,6 +1,6 @@
package com.jetpackduba.gitnuro.git package com.jetpackduba.gitnuro.git
import com.jetpackduba.gitnuro.TempFilesManager import com.jetpackduba.gitnuro.managers.TempFilesManager
import com.jetpackduba.gitnuro.extensions.fileName import com.jetpackduba.gitnuro.extensions.fileName
import org.eclipse.jgit.diff.ContentSource import org.eclipse.jgit.diff.ContentSource
import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.diff.DiffEntry

View File

@ -1,9 +1,9 @@
package com.jetpackduba.gitnuro.git 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.di.TabScope
import com.jetpackduba.gitnuro.extensions.delayedStateChange import com.jetpackduba.gitnuro.extensions.delayedStateChange
import com.jetpackduba.gitnuro.newErrorNow import com.jetpackduba.gitnuro.managers.newErrorNow
import com.jetpackduba.gitnuro.ui.SelectedItem import com.jetpackduba.gitnuro.ui.SelectedItem
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*

View File

@ -4,8 +4,8 @@ package com.jetpackduba.gitnuro.keybindings
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.key.* import androidx.compose.ui.input.key.*
import com.jetpackduba.gitnuro.extensions.OS import com.jetpackduba.gitnuro.system.OS
import com.jetpackduba.gitnuro.extensions.getCurrentOs import com.jetpackduba.gitnuro.system.getCurrentOs
data class Keybinding( data class Keybinding(
val alt: Boolean = false, val alt: Boolean = false,

View File

@ -1,7 +1,7 @@
package com.jetpackduba.gitnuro package com.jetpackduba.gitnuro.managers
import com.jetpackduba.gitnuro.extensions.OS import com.jetpackduba.gitnuro.system.OS
import com.jetpackduba.gitnuro.extensions.getCurrentOs import com.jetpackduba.gitnuro.system.getCurrentOs
import com.jetpackduba.gitnuro.logging.printError import com.jetpackduba.gitnuro.logging.printError
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject

View File

@ -1,4 +1,4 @@
package com.jetpackduba.gitnuro package com.jetpackduba.gitnuro.managers
import com.jetpackduba.gitnuro.di.qualifiers.AppCoroutineScope import com.jetpackduba.gitnuro.di.qualifiers.AppCoroutineScope
import com.jetpackduba.gitnuro.preferences.AppSettings import com.jetpackduba.gitnuro.preferences.AppSettings

View File

@ -1,4 +1,4 @@
package com.jetpackduba.gitnuro package com.jetpackduba.gitnuro.managers
import com.jetpackduba.gitnuro.di.TabScope import com.jetpackduba.gitnuro.di.TabScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers

View 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)
}
}

View File

@ -1,4 +1,4 @@
package com.jetpackduba.gitnuro package com.jetpackduba.gitnuro.managers
import com.jetpackduba.gitnuro.di.TabScope import com.jetpackduba.gitnuro.di.TabScope
import com.jetpackduba.gitnuro.extensions.openDirectory import com.jetpackduba.gitnuro.extensions.openDirectory

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,11 @@
package com.jetpackduba.gitnuro.terminal package com.jetpackduba.gitnuro.terminal
import com.jetpackduba.gitnuro.extensions.runCommand import com.jetpackduba.gitnuro.managers.IShellManager
import com.jetpackduba.gitnuro.extensions.runCommandInPath
import javax.inject.Inject import javax.inject.Inject
class LinuxTerminalProvider @Inject constructor() : ITerminalProvider { class LinuxTerminalProvider @Inject constructor(
private val shellManager: IShellManager
) : ITerminalProvider {
override fun getTerminalEmulators(): List<TerminalEmulator> { override fun getTerminalEmulators(): List<TerminalEmulator> {
return listOf( return listOf(
TerminalEmulator("Gnome Terminal", "gnome-terminal"), TerminalEmulator("Gnome Terminal", "gnome-terminal"),
@ -16,12 +17,12 @@ class LinuxTerminalProvider @Inject constructor() : ITerminalProvider {
} }
override fun isTerminalInstalled(terminalEmulator: TerminalEmulator): Boolean { 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() return !checkTerminalInstalled.isNullOrEmpty()
} }
override fun startTerminal(terminalEmulator: TerminalEmulator, repositoryPath: String) { override fun startTerminal(terminalEmulator: TerminalEmulator, repositoryPath: String) {
runCommandInPath(listOf(terminalEmulator.path), repositoryPath) shellManager.runCommandInPath(listOf(terminalEmulator.path), repositoryPath)
} }
} }

View File

@ -1,11 +1,13 @@
package com.jetpackduba.gitnuro.terminal 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 import javax.inject.Inject
// TODO Test this on MacOS // TODO Test this on MacOS
class MacTerminalProvider @Inject constructor() : ITerminalProvider { class MacTerminalProvider @Inject constructor(
private val shellManager: IShellManager
) : ITerminalProvider {
override fun getTerminalEmulators(): List<TerminalEmulator> { override fun getTerminalEmulators(): List<TerminalEmulator> {
return listOf( return listOf(
TerminalEmulator("MacOS Terminal", "Terminal") TerminalEmulator("MacOS Terminal", "Terminal")
@ -13,12 +15,12 @@ class MacTerminalProvider @Inject constructor() : ITerminalProvider {
} }
override fun isTerminalInstalled(terminalEmulator: TerminalEmulator): Boolean { 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() return !checkTerminalInstalled.isNullOrEmpty()
} }
override fun startTerminal(terminalEmulator: TerminalEmulator, repositoryPath: String) { override fun startTerminal(terminalEmulator: TerminalEmulator, repositoryPath: String) {
runCommandInPath(listOf("open", "-a", terminalEmulator.path), repositoryPath) shellManager.runCommandInPath(listOf("open", "-a", terminalEmulator.path), repositoryPath)
} }
} }

View File

@ -1,6 +1,5 @@
package com.jetpackduba.gitnuro.terminal package com.jetpackduba.gitnuro.terminal
import com.jetpackduba.gitnuro.extensions.runCommandInPath
import javax.inject.Inject import javax.inject.Inject
// For flatpak: https://github.com/flathub/com.visualstudio.code#use-host-shell-in-the-integrated-terminal // For flatpak: https://github.com/flathub/com.visualstudio.code#use-host-shell-in-the-integrated-terminal

View File

@ -1,9 +1,11 @@
package com.jetpackduba.gitnuro.terminal package com.jetpackduba.gitnuro.terminal
import com.jetpackduba.gitnuro.extensions.runCommandInPath import com.jetpackduba.gitnuro.managers.IShellManager
import javax.inject.Inject import javax.inject.Inject
class WindowsTerminalProvider @Inject constructor() : ITerminalProvider { class WindowsTerminalProvider @Inject constructor(
private val shellManager: IShellManager
) : ITerminalProvider {
override fun getTerminalEmulators(): List<TerminalEmulator> { override fun getTerminalEmulators(): List<TerminalEmulator> {
return listOf( return listOf(
TerminalEmulator("Powershell", "powershell.exe"), TerminalEmulator("Powershell", "powershell.exe"),
@ -16,6 +18,6 @@ class WindowsTerminalProvider @Inject constructor() : ITerminalProvider {
} }
override fun startTerminal(terminalEmulator: TerminalEmulator, repositoryPath: String) { override fun startTerminal(terminalEmulator: TerminalEmulator, repositoryPath: String) {
runCommandInPath(listOf("cmd", "/c", "start", terminalEmulator.path), repositoryPath) shellManager.runCommandInPath(listOf("cmd", "/c", "start", terminalEmulator.path), repositoryPath)
} }
} }

View File

@ -136,7 +136,13 @@ fun RepositoryOpenPage(
.fillMaxWidth(), .fillMaxWidth(),
onCreateBranch = { showNewBranchDialog = true }, onCreateBranch = { showNewBranchDialog = true },
onStashWithMessage = { showStashWithMessageDialog = true }, onStashWithMessage = { showStashWithMessageDialog = true },
onOpenAnotherRepository = { openRepositoryDialog(tabViewModel) }, onOpenAnotherRepository = {
val repo = tabViewModel.openDirectoryPicker()
if (repo != null) {
tabViewModel.openRepository(repo)
}
},
onQuickActions = { showQuickActionsDialog = true }, onQuickActions = { showQuickActionsDialog = true },
onShowSettingsDialog = onShowSettingsDialog onShowSettingsDialog = onShowSettingsDialog
) )

View File

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

View File

@ -24,8 +24,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.AppConstants import com.jetpackduba.gitnuro.AppConstants
import com.jetpackduba.gitnuro.AppIcons import com.jetpackduba.gitnuro.AppIcons
import com.jetpackduba.gitnuro.AppStateManager
import com.jetpackduba.gitnuro.extensions.* import com.jetpackduba.gitnuro.extensions.*
import com.jetpackduba.gitnuro.managers.AppStateManager
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
import com.jetpackduba.gitnuro.theme.textButtonColors import com.jetpackduba.gitnuro.theme.textButtonColors
import com.jetpackduba.gitnuro.ui.dialogs.AppInfoDialog import com.jetpackduba.gitnuro.ui.dialogs.AppInfoDialog
@ -63,10 +63,24 @@ fun WelcomePage(
) { ) {
HomeButtons( HomeButtons(
newUpdate = newUpdate, 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, onShowCloneView = onShowCloneDialog,
onShowAdditionalInfo = { showAdditionalInfo = true }, onShowAdditionalInfo = { showAdditionalInfo = true },
onShowSettings = onShowSettings, onShowSettings = onShowSettings,
onOpenUrlInBrowser = { url -> tabViewModel.openUrlInBrowser(url) }
) )
RecentRepositories(appStateManager, tabViewModel) RecentRepositories(appStateManager, tabViewModel)
@ -87,6 +101,7 @@ fun WelcomePage(
if (showAdditionalInfo) { if (showAdditionalInfo) {
AppInfoDialog( AppInfoDialog(
onClose = { showAdditionalInfo = false }, onClose = { showAdditionalInfo = false },
onOpenUrlInBrowser = { url -> tabViewModel.openUrlInBrowser(url) }
) )
} }
} }
@ -94,10 +109,12 @@ fun WelcomePage(
@Composable @Composable
fun HomeButtons( fun HomeButtons(
newUpdate: Update?, newUpdate: Update?,
tabViewModel: TabViewModel, onOpenRepository: () -> Unit,
onStartRepository: () -> Unit,
onShowCloneView: () -> Unit, onShowCloneView: () -> Unit,
onShowAdditionalInfo: () -> Unit, onShowAdditionalInfo: () -> Unit,
onShowSettings: () -> Unit, onShowSettings: () -> Unit,
onOpenUrlInBrowser: (String) -> Unit,
) { ) {
Column( Column(
modifier = Modifier.padding(end = 32.dp), modifier = Modifier.padding(end = 32.dp),
@ -113,7 +130,8 @@ fun HomeButtons(
modifier = Modifier.padding(bottom = 8.dp), modifier = Modifier.padding(bottom = 8.dp),
title = "Open a repository", title = "Open a repository",
painter = painterResource(AppIcons.OPEN), painter = painterResource(AppIcons.OPEN),
onClick = { openRepositoryDialog(tabViewModel) }) onClick = onOpenRepository
)
ButtonTile( ButtonTile(
modifier = Modifier.padding(bottom = 8.dp), modifier = Modifier.padding(bottom = 8.dp),
@ -126,10 +144,7 @@ fun HomeButtons(
modifier = Modifier.padding(bottom = 8.dp), modifier = Modifier.padding(bottom = 8.dp),
title = "Start a local repository", title = "Start a local repository",
painter = painterResource(AppIcons.OPEN), painter = painterResource(AppIcons.OPEN),
onClick = { onClick = onStartRepository
val dir = openDirectoryDialog()
if (dir != null) tabViewModel.initLocalRepository(dir)
}
) )
Text( Text(
@ -142,7 +157,7 @@ fun HomeButtons(
title = "Source code", title = "Source code",
painter = painterResource(AppIcons.CODE), painter = painterResource(AppIcons.CODE),
onClick = { onClick = {
openUrlInBrowser("https://github.com/JetpackDuba/Gitnuro") onOpenUrlInBrowser("https://github.com/JetpackDuba/Gitnuro")
} }
) )
@ -150,7 +165,7 @@ fun HomeButtons(
title = "Report a bug", title = "Report a bug",
painter = painterResource(AppIcons.BUG), painter = painterResource(AppIcons.BUG),
onClick = { 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), painter = painterResource(AppIcons.GRADE),
iconColor = MaterialTheme.colors.secondary, iconColor = MaterialTheme.colors.secondary,
onClick = { onClick = {
openUrlInBrowser(newUpdate.downloadUrl) onOpenUrlInBrowser(newUpdate.downloadUrl)
} }
) )
} }

View File

@ -26,7 +26,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.AppIcons 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.LocalTabScope
import com.jetpackduba.gitnuro.di.AppComponent import com.jetpackduba.gitnuro.di.AppComponent
import com.jetpackduba.gitnuro.di.DaggerTabComponent import com.jetpackduba.gitnuro.di.DaggerTabComponent

View File

@ -10,7 +10,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.jetpackduba.gitnuro.extensions.handMouseClickable import com.jetpackduba.gitnuro.extensions.handMouseClickable
import com.jetpackduba.gitnuro.extensions.openUrlInBrowser
@Composable @Composable
@ -19,6 +18,7 @@ fun TextLink(
url: String, url: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
colorsInverted: Boolean = false, colorsInverted: Boolean = false,
onClick: () -> Unit,
) { ) {
val hoverInteraction = remember { MutableInteractionSource() } val hoverInteraction = remember { MutableInteractionSource() }
val isHovered by hoverInteraction.collectIsHoveredAsState() val isHovered by hoverInteraction.collectIsHoveredAsState()
@ -29,14 +29,15 @@ fun TextLink(
MaterialTheme.colors.primaryVariant MaterialTheme.colors.primaryVariant
} }
Text( TooltipText(
text = text, text = text,
modifier = Modifier modifier = Modifier
.hoverable(hoverInteraction) .hoverable(hoverInteraction)
.handMouseClickable { .handMouseClickable {
openUrlInBrowser(url) onClick()
} }
.then(modifier), .then(modifier),
color = textColor, color = textColor,
tooltipTitle = url
) )
} }

View File

@ -19,6 +19,7 @@ import com.jetpackduba.gitnuro.ui.components.TextLink
@Composable @Composable
fun AppInfoDialog( fun AppInfoDialog(
onClose: () -> Unit, onClose: () -> Unit,
onOpenUrlInBrowser: (String) -> Unit,
) { ) {
MaterialDialog(onCloseRequested = onClose) { MaterialDialog(onCloseRequested = onClose) {
Column( Column(
@ -52,7 +53,7 @@ fun AppInfoDialog(
} }
items(openSourceProjects) { items(openSourceProjects) {
ProjectUsed(it) ProjectUsed(it, onOpenUrlInBrowser = onOpenUrlInBrowser)
} }
} }
@ -70,7 +71,10 @@ fun AppInfoDialog(
} }
@Composable @Composable
fun ProjectUsed(project: Project) { fun ProjectUsed(
project: Project,
onOpenUrlInBrowser: (String) -> Unit
) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier.padding(horizontal = 16.dp)
@ -80,7 +84,8 @@ fun ProjectUsed(project: Project) {
text = project.name, text = project.name,
url = project.url, url = project.url,
modifier = Modifier modifier = Modifier
.padding(vertical = 8.dp) .padding(vertical = 8.dp),
onClick = { onOpenUrlInBrowser(project.url) }
) )
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
@ -90,7 +95,8 @@ fun ProjectUsed(project: Project) {
url = project.license.url, url = project.license.url,
modifier = Modifier modifier = Modifier
.padding(vertical = 8.dp), .padding(vertical = 8.dp),
colorsInverted = true colorsInverted = true,
onClick = { onOpenUrlInBrowser(project.license.url) }
) )
} }
} }

View File

@ -27,7 +27,6 @@ import com.jetpackduba.gitnuro.theme.textButtonColors
import com.jetpackduba.gitnuro.ui.components.AdjustableOutlinedTextField import com.jetpackduba.gitnuro.ui.components.AdjustableOutlinedTextField
import com.jetpackduba.gitnuro.ui.components.PrimaryButton import com.jetpackduba.gitnuro.ui.components.PrimaryButton
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
import com.jetpackduba.gitnuro.ui.openDirectoryDialog
import com.jetpackduba.gitnuro.viewmodels.CloneViewModel import com.jetpackduba.gitnuro.viewmodels.CloneViewModel
import java.io.File import java.io.File
@ -152,7 +151,7 @@ private fun CloneInput(
IconButton( IconButton(
onClick = { onClick = {
cloneViewModel.resetStateIfError() cloneViewModel.resetStateIfError()
val newDirectory = openDirectoryDialog() val newDirectory = cloneViewModel.openDirectoryPicker()
if (newDirectory != null) { if (newDirectory != null) {
directory = newDirectory directory = newDirectory
cloneViewModel.directory = directory cloneViewModel.directory = directory

View File

@ -12,7 +12,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.jetpackduba.gitnuro.AppIcons import com.jetpackduba.gitnuro.AppIcons
import com.jetpackduba.gitnuro.Error import com.jetpackduba.gitnuro.managers.Error
import com.jetpackduba.gitnuro.ui.components.PrimaryButton import com.jetpackduba.gitnuro.ui.components.PrimaryButton
@Composable @Composable

View File

@ -13,7 +13,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.AppIcons 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.extensions.handMouseClickable
import com.jetpackduba.gitnuro.preferences.DEFAULT_UI_SCALE import com.jetpackduba.gitnuro.preferences.DEFAULT_UI_SCALE
import com.jetpackduba.gitnuro.theme.* 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.dialogs.MaterialDialog
import com.jetpackduba.gitnuro.ui.dropdowns.DropDownOption import com.jetpackduba.gitnuro.ui.dropdowns.DropDownOption
import com.jetpackduba.gitnuro.ui.dropdowns.ScaleDropDown import com.jetpackduba.gitnuro.ui.dropdowns.ScaleDropDown
import com.jetpackduba.gitnuro.ui.openFileDialog
import com.jetpackduba.gitnuro.viewmodels.SettingsViewModel import com.jetpackduba.gitnuro.viewmodels.SettingsViewModel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -181,7 +180,7 @@ fun UiSettings(settingsViewModel: SettingsViewModel) {
subtitle = "Select a JSON file to load the custom theme", subtitle = "Select a JSON file to load the custom theme",
buttonText = "Open file", buttonText = "Open file",
onClick = { onClick = {
val filePath = openFileDialog() val filePath = settingsViewModel.openFileDialog()
if (filePath != null) { if (filePath != null) {
val error = settingsViewModel.saveCustomTheme(filePath) val error = settingsViewModel.saveCustomTheme(filePath)

View File

@ -158,7 +158,7 @@ fun Diff(
) )
is DiffResult.NonText -> { is DiffResult.NonText -> {
NonTextDiff(diffResult) NonTextDiff(diffResult, onOpenFileWithExternalApp = { path -> diffViewModel.openFileWithExternalApp(path) })
} }
} }
} }
@ -184,7 +184,7 @@ fun Diff(
} }
@Composable @Composable
fun NonTextDiff(diffResult: DiffResult.NonText) { fun NonTextDiff(diffResult: DiffResult.NonText, onOpenFileWithExternalApp: (String) -> Unit) {
val oldBinaryContent = diffResult.oldBinaryContent val oldBinaryContent = diffResult.oldBinaryContent
val newBinaryContent = diffResult.newBinaryContent val newBinaryContent = diffResult.newBinaryContent
@ -204,7 +204,7 @@ fun NonTextDiff(diffResult: DiffResult.NonText) {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
SideTitle("Old") SideTitle("Old")
SideDiff(oldBinaryContent) SideDiff(oldBinaryContent, onOpenFileWithExternalApp)
} }
Column( Column(
modifier = Modifier modifier = Modifier
@ -213,7 +213,7 @@ fun NonTextDiff(diffResult: DiffResult.NonText) {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
SideTitle("New") SideTitle("New")
SideDiff(newBinaryContent) SideDiff(newBinaryContent, onOpenFileWithExternalApp)
} }
} else if (oldBinaryContent != EntryContent.Missing) { } else if (oldBinaryContent != EntryContent.Missing) {
Box( Box(
@ -221,7 +221,7 @@ fun NonTextDiff(diffResult: DiffResult.NonText) {
.fillMaxSize() .fillMaxSize()
.padding(all = 24.dp), .padding(all = 24.dp),
) { ) {
SideDiff(oldBinaryContent) SideDiff(oldBinaryContent, onOpenFileWithExternalApp)
} }
} else if (newBinaryContent != EntryContent.Missing) { } else if (newBinaryContent != EntryContent.Missing) {
Column( Column(
@ -232,7 +232,7 @@ fun NonTextDiff(diffResult: DiffResult.NonText) {
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
SideDiff(newBinaryContent) SideDiff(newBinaryContent, onOpenFileWithExternalApp)
} }
} }
} }
@ -248,10 +248,14 @@ fun SideTitle(text: String) {
} }
@Composable @Composable
fun SideDiff(entryContent: EntryContent) { fun SideDiff(entryContent: EntryContent, onOpenFileWithExternalApp: (String) -> Unit) {
when (entryContent) { when (entryContent) {
EntryContent.Binary -> BinaryDiff() EntryContent.Binary -> BinaryDiff()
is EntryContent.ImageBinary -> ImageDiff(entryContent.imagePath, entryContent.contentType) is EntryContent.ImageBinary -> ImageDiff(
entryContent.imagePath,
entryContent.contentType,
onOpenFileWithExternalApp = { onOpenFileWithExternalApp(entryContent.imagePath) }
)
else -> { else -> {
} }
// is EntryContent.Text -> //TODO maybe have a text view if the file was a binary before? // 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 @Composable
private fun ImageDiff(imagePath: String, contentType: String) { private fun ImageDiff(
imagePath: String,
contentType: String,
onOpenFileWithExternalApp: () -> Unit
) {
if (animatedImages.contains(contentType)) { if (animatedImages.contains(contentType)) {
AnimatedImage(imagePath) AnimatedImage(imagePath, onOpenFileWithExternalApp)
} else { } else {
StaticImage(imagePath) StaticImage(imagePath, onOpenFileWithExternalApp)
} }
} }
@Composable @Composable
private fun StaticImage(tempImagePath: String) { private fun StaticImage(
tempImagePath: String,
onOpenFileWithExternalApp: () -> Unit
) {
var image by remember(tempImagePath) { mutableStateOf<ImageBitmap?>(null) } var image by remember(tempImagePath) { mutableStateOf<ImageBitmap?>(null) }
LaunchedEffect(tempImagePath) { LaunchedEffect(tempImagePath) {
@ -295,19 +306,22 @@ private fun StaticImage(tempImagePath: String) {
} }
} }
.handMouseClickable { .handMouseClickable {
openFileWithExternalApp(tempImagePath) onOpenFileWithExternalApp()
} }
) )
} }
@Composable @Composable
private fun AnimatedImage(imagePath: String) { private fun AnimatedImage(
imagePath: String,
onOpenFileWithExternalApp: () -> Unit
) {
Image( Image(
bitmap = loadOrNull(imagePath) { loadAnimatedImage(imagePath) }?.animate() ?: ImageBitmap.Blank, bitmap = loadOrNull(imagePath) { loadAnimatedImage(imagePath) }?.animate() ?: ImageBitmap.Blank,
contentDescription = null, contentDescription = null,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
.handMouseClickable { .handMouseClickable {
openFileWithExternalApp(imagePath) onOpenFileWithExternalApp()
} }
) )
} }

View File

@ -3,6 +3,8 @@ package com.jetpackduba.gitnuro.viewmodels
import com.jetpackduba.gitnuro.git.CloneStatus import com.jetpackduba.gitnuro.git.CloneStatus
import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.remote_operations.CloneRepositoryUseCase 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.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
@ -15,6 +17,7 @@ import javax.inject.Inject
class CloneViewModel @Inject constructor( class CloneViewModel @Inject constructor(
private val tabState: TabState, private val tabState: TabState,
private val cloneRepositoryUseCase: CloneRepositoryUseCase, private val cloneRepositoryUseCase: CloneRepositoryUseCase,
private val openFilePickerUseCase: OpenFilePickerUseCase,
) { ) {
private val _cloneStatus = MutableStateFlow<CloneStatus>(CloneStatus.None) private val _cloneStatus = MutableStateFlow<CloneStatus>(CloneStatus.None)
@ -92,4 +95,8 @@ class CloneViewModel @Inject constructor(
fun resetStateIfError() { fun resetStateIfError() {
_cloneStatus.value = CloneStatus.None _cloneStatus.value = CloneStatus.None
} }
fun openDirectoryPicker(): String? {
return openFilePickerUseCase(PickerType.DIRECTORIES, null)
}
} }

View File

@ -10,6 +10,7 @@ import com.jetpackduba.gitnuro.git.TabState
import com.jetpackduba.gitnuro.git.diff.* import com.jetpackduba.gitnuro.git.diff.*
import com.jetpackduba.gitnuro.git.workspace.* import com.jetpackduba.gitnuro.git.workspace.*
import com.jetpackduba.gitnuro.preferences.AppSettings import com.jetpackduba.gitnuro.preferences.AppSettings
import com.jetpackduba.gitnuro.system.OpenFileInExternalAppUseCase
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -30,6 +31,7 @@ class DiffViewModel @Inject constructor(
private val resetHunkUseCase: ResetHunkUseCase, private val resetHunkUseCase: ResetHunkUseCase,
private val stageEntryUseCase: StageEntryUseCase, private val stageEntryUseCase: StageEntryUseCase,
private val unstageEntryUseCase: UnstageEntryUseCase, private val unstageEntryUseCase: UnstageEntryUseCase,
private val openFileInExternalAppUseCase: OpenFileInExternalAppUseCase,
private val settings: AppSettings, private val settings: AppSettings,
private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase, private val generateSplitHunkFromDiffResultUseCase: GenerateSplitHunkFromDiffResultUseCase,
tabScope: CoroutineScope, tabScope: CoroutineScope,
@ -186,6 +188,10 @@ class DiffViewModel @Inject constructor(
) { git -> ) { git ->
unstageHunkLineUseCase(git, entry, hunk, line) unstageHunkLineUseCase(git, entry, hunk, line)
} }
fun openFileWithExternalApp(path: String) {
openFileInExternalAppUseCase(path)
}
} }
enum class TextDiffType(val value: Int) { enum class TextDiffType(val value: Int) {

View File

@ -1,9 +1,11 @@
package com.jetpackduba.gitnuro.viewmodels package com.jetpackduba.gitnuro.viewmodels
import com.jetpackduba.gitnuro.Error
import com.jetpackduba.gitnuro.di.qualifiers.AppCoroutineScope 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.preferences.AppSettings
import com.jetpackduba.gitnuro.system.OpenFilePickerUseCase
import com.jetpackduba.gitnuro.system.PickerType
import com.jetpackduba.gitnuro.theme.Theme import com.jetpackduba.gitnuro.theme.Theme
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -13,6 +15,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class SettingsViewModel @Inject constructor( class SettingsViewModel @Inject constructor(
private val appSettings: AppSettings, private val appSettings: AppSettings,
private val openFilePickerUseCase: OpenFilePickerUseCase,
@AppCoroutineScope private val appScope: CoroutineScope, @AppCoroutineScope private val appScope: CoroutineScope,
) { ) {
// Temporary values to detect changed variables // Temporary values to detect changed variables
@ -75,4 +78,8 @@ class SettingsViewModel @Inject constructor(
appSettings.setCommitsLimit(commitsLimit) appSettings.setCommitsLimit(commitsLimit)
} }
} }
fun openFileDialog(): String? {
return openFilePickerUseCase(PickerType.FILES, null)
}
} }

View File

@ -1,7 +1,5 @@
package com.jetpackduba.gitnuro.viewmodels 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.CredentialsAccepted
import com.jetpackduba.gitnuro.credentials.CredentialsState import com.jetpackduba.gitnuro.credentials.CredentialsState
import com.jetpackduba.gitnuro.credentials.CredentialsStateManager 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.git.workspace.StageUntrackedFileUseCase
import com.jetpackduba.gitnuro.logging.printDebug import com.jetpackduba.gitnuro.logging.printDebug
import com.jetpackduba.gitnuro.logging.printLog 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.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.ui.SelectedItem
import com.jetpackduba.gitnuro.updates.Update import com.jetpackduba.gitnuro.updates.Update
import com.jetpackduba.gitnuro.updates.UpdatesRepository import com.jetpackduba.gitnuro.updates.UpdatesRepository
@ -62,6 +65,8 @@ class TabViewModel @Inject constructor(
private val stashChangesUseCase: StashChangesUseCase, private val stashChangesUseCase: StashChangesUseCase,
private val stageUntrackedFileUseCase: StageUntrackedFileUseCase, private val stageUntrackedFileUseCase: StageUntrackedFileUseCase,
private val abortRebaseUseCase: AbortRebaseUseCase, private val abortRebaseUseCase: AbortRebaseUseCase,
private val openFilePickerUseCase: OpenFilePickerUseCase,
private val openUrlInBrowserUseCase: OpenUrlInBrowserUseCase,
private val tabScope: CoroutineScope, private val tabScope: CoroutineScope,
) { ) {
val errorsManager: ErrorsManager = tabState.errorsManager val errorsManager: ErrorsManager = tabState.errorsManager
@ -302,11 +307,6 @@ class TabViewModel @Inject constructor(
) { ) {
updateDiffEntry() updateDiffEntry()
tabState.refreshData(RefreshType.UNCOMMITED_CHANGES_AND_LOG) 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() { 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( fun initLocalRepository(dir: String) = tabState.safeProcessingWithoutGit(
showError = true, showError = true,
) { ) {
@ -467,6 +473,10 @@ class TabViewModel @Inject constructor(
fun cancelOngoingTask() { fun cancelOngoingTask() {
tabState.cancelCurrentTask() tabState.cancelCurrentTask()
} }
fun openUrlInBrowser(url: String) {
openUrlInBrowserUseCase(url)
}
} }