diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/App.kt b/src/main/kotlin/com/jetpackduba/gitnuro/App.kt index e98a856..1108b72 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/App.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/App.kt @@ -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 diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/credentials/HttpCredentialsProvider.kt b/src/main/kotlin/com/jetpackduba/gitnuro/credentials/HttpCredentialsProvider.kt index 2c3de26..ccae849 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/credentials/HttpCredentialsProvider.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/credentials/HttpCredentialsProvider.kt @@ -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 ): 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 diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/di/AppComponent.kt b/src/main/kotlin/com/jetpackduba/gitnuro/di/AppComponent.kt index 7d704f7..e93eac0 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/di/AppComponent.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/di/AppComponent.kt @@ -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 } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/di/TabComponent.kt b/src/main/kotlin/com/jetpackduba/gitnuro/di/TabComponent.kt index 43db155..a5ad365 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/di/TabComponent.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/di/TabComponent.kt @@ -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 diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/di/modules/ShellModule.kt b/src/main/kotlin/com/jetpackduba/gitnuro/di/modules/ShellModule.kt new file mode 100644 index 0000000..889cdf3 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/di/modules/ShellModule.kt @@ -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, + flatpakShellManager: Provider, + ): IShellManager { + return if (appEnvInfo.isFlatpak) + flatpakShellManager.get() + else + shellManager.get() + } + + @Provides + fun provideTerminalProvider( + linuxTerminalProvider: Provider, + windowsTerminalProvider: Provider, + macTerminalProvider: Provider, + ): ITerminalProvider { + return when (getCurrentOs()) { + OS.LINUX -> linuxTerminalProvider.get() + OS.WINDOWS -> windowsTerminalProvider.get() + OS.MAC -> macTerminalProvider.get() + OS.UNKNOWN -> throw NotImplementedError("Unknown operating system") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/di/modules/TerminalModule.kt b/src/main/kotlin/com/jetpackduba/gitnuro/di/modules/TerminalModule.kt deleted file mode 100644 index 17dfa4f..0000000 --- a/src/main/kotlin/com/jetpackduba/gitnuro/di/modules/TerminalModule.kt +++ /dev/null @@ -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, - windowsTerminalProvider: Provider, - macTerminalProvider: Provider, - flatpakTerminalProvider: Provider, - 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") - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/extensions/Shell.kt b/src/main/kotlin/com/jetpackduba/gitnuro/extensions/Shell.kt deleted file mode 100644 index 4e0aa2e..0000000 --- a/src/main/kotlin/com/jetpackduba/gitnuro/extensions/Shell.kt +++ /dev/null @@ -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, path: String) { - val processBuilder = ProcessBuilder(command).apply { - directory(File(path)) - } - - processBuilder.start() -} - -fun runCommandWithoutResult(command: String, args: String, file: String): Boolean { - val parts: Array = 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 { - val parts: MutableList = 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() -} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/extensions/StringExtensions.kt b/src/main/kotlin/com/jetpackduba/gitnuro/extensions/StringExtensions.kt index 75f80ae..abdcf51 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/extensions/StringExtensions.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/extensions/StringExtensions.kt @@ -1,5 +1,6 @@ package com.jetpackduba.gitnuro.extensions +import com.jetpackduba.gitnuro.system.systemSeparator import java.math.BigInteger import java.security.MessageDigest diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/extensions/SystemUtils.kt b/src/main/kotlin/com/jetpackduba/gitnuro/extensions/SystemUtils.kt deleted file mode 100644 index 5a16ac8..0000000 --- a/src/main/kotlin/com/jetpackduba/gitnuro/extensions/SystemUtils.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/FileChangesWatcher.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/FileChangesWatcher.kt index 368f0be..29df27b 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/FileChangesWatcher.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/FileChangesWatcher.kt @@ -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 diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/RawFileManager.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/RawFileManager.kt index 1f144a5..8ddd7ef 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/RawFileManager.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/RawFileManager.kt @@ -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 diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/TabState.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/TabState.kt index 3687f2c..9486a15 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/TabState.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/TabState.kt @@ -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.* diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/keybindings/Keybinding.kt b/src/main/kotlin/com/jetpackduba/gitnuro/keybindings/Keybinding.kt index 7e29315..4a761e3 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/keybindings/Keybinding.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/keybindings/Keybinding.kt @@ -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, diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/AppFilesManager.kt b/src/main/kotlin/com/jetpackduba/gitnuro/managers/AppFilesManager.kt similarity index 89% rename from src/main/kotlin/com/jetpackduba/gitnuro/AppFilesManager.kt rename to src/main/kotlin/com/jetpackduba/gitnuro/managers/AppFilesManager.kt index c8954e6..d8ecd7e 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/AppFilesManager.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/managers/AppFilesManager.kt @@ -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 diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/AppStateManager.kt b/src/main/kotlin/com/jetpackduba/gitnuro/managers/AppStateManager.kt similarity index 97% rename from src/main/kotlin/com/jetpackduba/gitnuro/AppStateManager.kt rename to src/main/kotlin/com/jetpackduba/gitnuro/managers/AppStateManager.kt index cd01f46..7abf7c4 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/AppStateManager.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/managers/AppStateManager.kt @@ -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 diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ErrorsManager.kt b/src/main/kotlin/com/jetpackduba/gitnuro/managers/ErrorsManager.kt similarity index 96% rename from src/main/kotlin/com/jetpackduba/gitnuro/ErrorsManager.kt rename to src/main/kotlin/com/jetpackduba/gitnuro/managers/ErrorsManager.kt index 7446045..feeeb51 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ErrorsManager.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/managers/ErrorsManager.kt @@ -1,4 +1,4 @@ -package com.jetpackduba.gitnuro +package com.jetpackduba.gitnuro.managers import com.jetpackduba.gitnuro.di.TabScope import kotlinx.coroutines.Dispatchers diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/managers/ShellManager.kt b/src/main/kotlin/com/jetpackduba/gitnuro/managers/ShellManager.kt new file mode 100644 index 0000000..447cbef --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/managers/ShellManager.kt @@ -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? + fun runCommandInPath(command: List, path: String) + fun runCommandWithoutResult(command: List): Boolean + fun runCommandProcess(command: List): Process +} + +class ShellManager @Inject constructor() : IShellManager { + override fun runCommand(command: List): 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, path: String) { + printLog(TAG, "runCommandInPath: " + command.joinToString(" ")) + + val processBuilder = ProcessBuilder(command).apply { + directory(File(path)) + } + + processBuilder.start() + } + + override fun runCommandWithoutResult(command: List): 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): 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? { + return shellManager.runCommand(flatpakPrefix + command) + } + + override fun runCommandInPath(command: List, path: String) { + shellManager.runCommandInPath(flatpakPrefix + command, path) + } + + override fun runCommandWithoutResult(command: List): Boolean { + return shellManager.runCommandWithoutResult(flatpakPrefix + command) + } + + override fun runCommandProcess(command: List): Process { + return shellManager.runCommandProcess(flatpakPrefix + command) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/TempFilesManager.kt b/src/main/kotlin/com/jetpackduba/gitnuro/managers/TempFilesManager.kt similarity index 92% rename from src/main/kotlin/com/jetpackduba/gitnuro/TempFilesManager.kt rename to src/main/kotlin/com/jetpackduba/gitnuro/managers/TempFilesManager.kt index 5d6c151..8ce2087 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/TempFilesManager.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/managers/TempFilesManager.kt @@ -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 diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/system/OS.kt b/src/main/kotlin/com/jetpackduba/gitnuro/system/OS.kt new file mode 100644 index 0000000..5646fdf --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/system/OS.kt @@ -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 +} diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/system/OpenFileInExternalAppUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/system/OpenFileInExternalAppUseCase.kt new file mode 100644 index 0000000..a931a18 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/system/OpenFileInExternalAppUseCase.kt @@ -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() + } + } +} diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/system/OpenFilePickerUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/system/OpenFilePickerUseCase.kt new file mode 100644 index 0000000..ad76f66 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/system/OpenFilePickerUseCase.kt @@ -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); +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/system/OpenPathInSystemUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/system/OpenPathInSystemUseCase.kt new file mode 100644 index 0000000..fe40a56 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/system/OpenPathInSystemUseCase.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/system/OpenUrlInBrowserUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/system/OpenUrlInBrowserUseCase.kt new file mode 100644 index 0000000..91d17c3 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/system/OpenUrlInBrowserUseCase.kt @@ -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() + } + } +} diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/terminal/FlatpakTerminalProvider.kt b/src/main/kotlin/com/jetpackduba/gitnuro/terminal/FlatpakTerminalProvider.kt deleted file mode 100644 index 17b12ac..0000000 --- a/src/main/kotlin/com/jetpackduba/gitnuro/terminal/FlatpakTerminalProvider.kt +++ /dev/null @@ -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 { - 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) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/terminal/LinuxTerminalProvider.kt b/src/main/kotlin/com/jetpackduba/gitnuro/terminal/LinuxTerminalProvider.kt index 6a0cac9..2b096c1 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/terminal/LinuxTerminalProvider.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/terminal/LinuxTerminalProvider.kt @@ -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 { 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) } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/terminal/MacTerminalProvider.kt b/src/main/kotlin/com/jetpackduba/gitnuro/terminal/MacTerminalProvider.kt index 8bdcee5..132f590 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/terminal/MacTerminalProvider.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/terminal/MacTerminalProvider.kt @@ -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 { 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) } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/terminal/OpenRepositoryInTerminalUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/terminal/OpenRepositoryInTerminalUseCase.kt index 05c386b..b6feed9 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/terminal/OpenRepositoryInTerminalUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/terminal/OpenRepositoryInTerminalUseCase.kt @@ -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 diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/terminal/WindowsTerminalProvider.kt b/src/main/kotlin/com/jetpackduba/gitnuro/terminal/WindowsTerminalProvider.kt index ad5e938..cc528b1 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/terminal/WindowsTerminalProvider.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/terminal/WindowsTerminalProvider.kt @@ -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 { 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) } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt index fc737b3..92180be 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt @@ -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 ) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/SystemDialogs.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/SystemDialogs.kt deleted file mode 100644 index 9400736..0000000 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/SystemDialogs.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/WelcomePage.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/WelcomePage.kt index 9bbd5f5..5d3eac8 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/WelcomePage.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/WelcomePage.kt @@ -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) } ) } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/RepositoriesTabPanel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/RepositoriesTabPanel.kt index ad2cfe0..687624c 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/RepositoriesTabPanel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/RepositoriesTabPanel.kt @@ -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 diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/TextLink.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/TextLink.kt index 60e70b6..89cd352 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/TextLink.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/TextLink.kt @@ -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 ) } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/AppInfoDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/AppInfoDialog.kt index 9a5f645..be716f7 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/AppInfoDialog.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/AppInfoDialog.kt @@ -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) } ) } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/CloneDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/CloneDialog.kt index dc40b13..856d5bb 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/CloneDialog.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/CloneDialog.kt @@ -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 diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/ErrorDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/ErrorDialog.kt index 427c944..09065fd 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/ErrorDialog.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/ErrorDialog.kt @@ -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 diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt index 57f7819..db1b8a9 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt @@ -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) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/diff/Diff.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/diff/Diff.kt index 9a56b24..58cbdd7 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/diff/Diff.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/diff/Diff.kt @@ -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(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() } ) } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/CloneViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/CloneViewModel.kt index b09e76c..398bda5 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/CloneViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/CloneViewModel.kt @@ -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.None) @@ -92,4 +95,8 @@ class CloneViewModel @Inject constructor( fun resetStateIfError() { _cloneStatus.value = CloneStatus.None } + + fun openDirectoryPicker(): String? { + return openFilePickerUseCase(PickerType.DIRECTORIES, null) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/DiffViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/DiffViewModel.kt index b594d28..b41fc55 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/DiffViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/DiffViewModel.kt @@ -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) { diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt index 5ebcd07..8495a91 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt @@ -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) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModel.kt index d3abe18..6c544e2 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModel.kt @@ -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) + } }