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

View File

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

View File

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

View File

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

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
import com.jetpackduba.gitnuro.system.systemSeparator
import java.math.BigInteger
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
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

View File

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

View File

@ -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.*

View File

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

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.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

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.preferences.AppSettings

View File

@ -1,4 +1,4 @@
package com.jetpackduba.gitnuro
package com.jetpackduba.gitnuro.managers
import com.jetpackduba.gitnuro.di.TabScope
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.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
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)
}
}

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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.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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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