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