mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
20 Commits
2.0.0-beta
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0020fede1 | ||
|
|
6f1eaab456 | ||
|
|
6173eae772 | ||
|
|
0bb366b1f7 | ||
|
|
9a4d6f7f4d | ||
|
|
a4ae11e301 | ||
|
|
5af0acb619 | ||
|
|
042434b8f8 | ||
|
|
eddc7ef0c6 | ||
|
|
c96ca2d424 | ||
|
|
45be9008fd | ||
|
|
057da4e297 | ||
|
|
e4e41667ff | ||
|
|
95ca0a4af7 | ||
|
|
702dee7983 | ||
|
|
165d544448 | ||
|
|
ecf61bedc4 | ||
|
|
66a81a5da3 | ||
|
|
574c816ebb | ||
|
|
e7cafb74e4 |
4
.github/workflows/osx-aarch64.yml
vendored
4
.github/workflows/osx-aarch64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install the Apple certificate
|
||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''
|
||||
env:
|
||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
- name: Setup the Notary information
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.APPLE_ID != ''"
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
4
.github/workflows/osx-x86-64.yml
vendored
4
.github/workflows/osx-x86-64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install the Apple certificate
|
||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''
|
||||
env:
|
||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
- name: Setup the Notary information
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.APPLE_ID != ''"
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
@@ -62,9 +62,6 @@ dependencies {
|
||||
testImplementation(libs.h2)
|
||||
testImplementation(libs.exposed.migration)
|
||||
|
||||
// implementation(platform(libs.koin.bom))
|
||||
// implementation(libs.koin.core)
|
||||
|
||||
api(kotlin("reflect"))
|
||||
api(libs.slf4j.api)
|
||||
api(libs.pty4j)
|
||||
@@ -105,7 +102,6 @@ dependencies {
|
||||
|
||||
api(libs.colorpicker)
|
||||
api(libs.mixpanel)
|
||||
api(libs.jSerialComm)
|
||||
api(libs.ini4j)
|
||||
api(libs.restart4j)
|
||||
api(libs.exposed.core)
|
||||
|
||||
@@ -4,7 +4,7 @@ slf4j = "2.0.17"
|
||||
pty4j = "0.13.6"
|
||||
tinylog = "2.7.0"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
flatlaf = "3.6"
|
||||
flatlaf = "3.6.1-SNAPSHOT"
|
||||
kotlinx-serialization-json = "1.9.0"
|
||||
commons-codec = "1.18.0"
|
||||
commons-lang3 = "3.17.0"
|
||||
@@ -22,9 +22,9 @@ jna = "5.17.0"
|
||||
jSystemThemeDetector = "3.9.1"
|
||||
commons-io = "2.19.0"
|
||||
jbr-api = "17.1.10.1"
|
||||
hutool = "5.8.37"
|
||||
jsch = "0.2.26"
|
||||
okhttp = "4.12.0"
|
||||
hutool = "5.8.39"
|
||||
jsch = "2.27.2"
|
||||
okhttp = "5.1.0"
|
||||
sshj = "0.39.0"
|
||||
sshd-core = "2.15.0"
|
||||
jgit = "7.2.0.202503040940-r"
|
||||
@@ -46,7 +46,7 @@ h2 = "2.3.232"
|
||||
sqlite = "3.50.2.0"
|
||||
jug = "5.1.0"
|
||||
semver4j = "6.0.0"
|
||||
jsvg = "1.4.0"
|
||||
jsvg = "2.0.0"
|
||||
dom4j = "2.2.0"
|
||||
|
||||
[libraries]
|
||||
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.4"
|
||||
project.version = "0.0.5"
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.termora.plugins.bg
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.database.DatabaseManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
@@ -96,9 +95,7 @@ internal class BackgroundManager private constructor() : Disposable, GlassPaneAw
|
||||
return
|
||||
}
|
||||
val body = response.body
|
||||
if (body != null) {
|
||||
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
|
||||
}
|
||||
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
|
||||
IOUtils.closeQuietly(body)
|
||||
return@use tempFile
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.1"
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
implementation("org.apache.commons:commons-pool2:2.12.1")
|
||||
testImplementation(project(":"))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import org.apache.commons.vfs2.Capability
|
||||
import org.apache.commons.vfs2.FileName
|
||||
import org.apache.commons.vfs2.FileSystem
|
||||
import org.apache.commons.vfs2.FileSystemOptions
|
||||
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
|
||||
|
||||
class FTPFileProvider private constructor() : AbstractOriginatingFileProvider() {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { FTPFileProvider() }
|
||||
val capabilities = listOf(
|
||||
Capability.CREATE,
|
||||
Capability.DELETE,
|
||||
Capability.RENAME,
|
||||
Capability.GET_TYPE,
|
||||
Capability.LIST_CHILDREN,
|
||||
Capability.READ_CONTENT,
|
||||
Capability.URI,
|
||||
Capability.WRITE_CONTENT,
|
||||
Capability.GET_LAST_MODIFIED,
|
||||
Capability.SET_LAST_MODIFIED_FILE,
|
||||
Capability.RANDOM_ACCESS_READ,
|
||||
Capability.APPEND_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCapabilities(): Collection<Capability> {
|
||||
return FTPFileProvider.capabilities
|
||||
}
|
||||
|
||||
override fun doCreateFileSystem(
|
||||
rootFileName: FileName,
|
||||
fileSystemOptions: FileSystemOptions
|
||||
): FileSystem? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.transfer.s3.S3FileSystem
|
||||
import app.termora.transfer.s3.S3Path
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.net.ftp.FTPClient
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||
|
||||
class FTPFileSystem(private val pool: GenericObjectPool<FTPClient>) : S3FileSystem(FTPSystemProvider(pool)) {
|
||||
|
||||
override fun create(root: String?, names: List<String>): S3Path {
|
||||
val path = FTPPath(this, root, names)
|
||||
if (names.isEmpty()) {
|
||||
path.attributes = path.attributes.copy(directory = true)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(pool)
|
||||
super.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.keymgr.KeyManager
|
||||
import app.termora.plugin.internal.BasicProxyOption
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.nio.charset.Charset
|
||||
import javax.swing.*
|
||||
|
||||
class FTPHostOptionsPane : OptionsPane() {
|
||||
private val generalOption = GeneralOption()
|
||||
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
|
||||
private val sftpOption = SFTPOption()
|
||||
|
||||
init {
|
||||
addOption(generalOption)
|
||||
addOption(proxyOption)
|
||||
addOption(sftpOption)
|
||||
|
||||
}
|
||||
|
||||
fun getHost(): Host {
|
||||
val name = generalOption.nameTextField.text
|
||||
val protocol = FTPProtocolProvider.PROTOCOL
|
||||
val port = generalOption.portTextField.value as Int
|
||||
var authentication = Authentication.Companion.No
|
||||
var proxy = Proxy.Companion.No
|
||||
val authenticationType = AuthenticationType.Password
|
||||
|
||||
authentication = authentication.copy(
|
||||
type = authenticationType,
|
||||
password = String(generalOption.passwordTextField.password)
|
||||
)
|
||||
|
||||
|
||||
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||
proxy = proxy.copy(
|
||||
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
|
||||
host = proxyOption.proxyHostTextField.text,
|
||||
username = proxyOption.proxyUsernameTextField.text,
|
||||
password = String(proxyOption.proxyPasswordTextField.password),
|
||||
port = proxyOption.proxyPortTextField.value as Int,
|
||||
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val options = Options.Default.copy(
|
||||
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||
encoding = sftpOption.charsetComboBox.selectedItem as String,
|
||||
extras = mutableMapOf("passive" to (sftpOption.passiveComboBox.selectedItem as PassiveMode).name)
|
||||
)
|
||||
|
||||
return Host(
|
||||
name = name,
|
||||
protocol = protocol,
|
||||
port = port,
|
||||
host = generalOption.hostTextField.text,
|
||||
username = generalOption.usernameTextField.text,
|
||||
authentication = authentication,
|
||||
proxy = proxy,
|
||||
sort = System.currentTimeMillis(),
|
||||
remark = generalOption.remarkTextArea.text,
|
||||
options = options,
|
||||
)
|
||||
}
|
||||
|
||||
fun setHost(host: Host) {
|
||||
generalOption.nameTextField.text = host.name
|
||||
generalOption.usernameTextField.text = host.username
|
||||
generalOption.remarkTextArea.text = host.remark
|
||||
generalOption.passwordTextField.text = host.authentication.password
|
||||
generalOption.hostTextField.text = host.host
|
||||
generalOption.portTextField.value = host.port
|
||||
|
||||
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||
proxyOption.proxyUsernameTextField.text = host.proxy.username
|
||||
proxyOption.proxyPortTextField.value = host.proxy.port
|
||||
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
|
||||
|
||||
|
||||
val passive = host.options.extras["passive"] ?: PassiveMode.Local.name
|
||||
sftpOption.charsetComboBox.selectedItem = host.options.encoding
|
||||
sftpOption.passiveComboBox.selectedItem = runCatching { PassiveMode.valueOf(passive) }
|
||||
.getOrNull() ?: PassiveMode.Local
|
||||
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||
}
|
||||
|
||||
fun validateFields(): Boolean {
|
||||
val host = getHost()
|
||||
|
||||
// general
|
||||
if (validateField(generalOption.nameTextField)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if (validateField(generalOption.hostTextField)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(generalOption.usernameTextField.text) || generalOption.passwordTextField.password.isNotEmpty()) {
|
||||
if (validateField(generalOption.usernameTextField)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (validateField(generalOption.passwordTextField)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// proxy
|
||||
if (host.proxy.type != ProxyType.No) {
|
||||
if (validateField(proxyOption.proxyHostTextField)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (host.proxy.authenticationType != AuthenticationType.No) {
|
||||
if (validateField(proxyOption.proxyUsernameTextField)
|
||||
|| validateField(proxyOption.proxyPasswordTextField)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 true 表示有错误
|
||||
*/
|
||||
private fun validateField(textField: JTextField): Boolean {
|
||||
if (textField.isEnabled && (if (textField is JPasswordField) textField.password.isEmpty() else textField.text.isBlank())) {
|
||||
setOutlineError(textField)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun setOutlineError(c: JComponent) {
|
||||
selectOptionJComponent(c)
|
||||
c.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||
c.requestFocusInWindow()
|
||||
}
|
||||
|
||||
|
||||
inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||
val portTextField = PortSpinner(21)
|
||||
val nameTextField = OutlineTextField(128)
|
||||
val usernameTextField = OutlineTextField(128)
|
||||
val hostTextField = OutlineTextField(255)
|
||||
val passwordTextField = OutlinePasswordField(255)
|
||||
val publicKeyComboBox = OutlineComboBox<String>()
|
||||
val remarkTextArea = FixedLengthTextArea(512)
|
||||
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
|
||||
publicKeyComboBox.isEditable = false
|
||||
|
||||
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = StringUtils.EMPTY
|
||||
if (value is String) {
|
||||
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
|
||||
}
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
text,
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = value?.toString() ?: ""
|
||||
when (value) {
|
||||
AuthenticationType.Password -> {
|
||||
text = "Password"
|
||||
}
|
||||
|
||||
AuthenticationType.PublicKey -> {
|
||||
text = "Public Key"
|
||||
}
|
||||
|
||||
AuthenticationType.KeyboardInteractive -> {
|
||||
text = "Keyboard Interactive"
|
||||
}
|
||||
}
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
text,
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||
|
||||
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
|
||||
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentResized(e: ComponentEvent) {
|
||||
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
|
||||
removeComponentListener(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.settings
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.new-host.general")
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
|
||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
remarkTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
remarkTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
|
||||
remarkTextArea.rows = 8
|
||||
remarkTextArea.lineWrap = true
|
||||
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
||||
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||
.add(hostTextField).xy(3, rows)
|
||||
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||
.add(portTextField).xy(7, rows).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
|
||||
.add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||
.xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.build()
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||
val defaultDirectoryField = OutlineTextField(255)
|
||||
val charsetComboBox = JComboBox<String>()
|
||||
val passiveComboBox = JComboBox<PassiveMode>()
|
||||
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
|
||||
for (e in Charset.availableCharsets()) {
|
||||
charsetComboBox.addItem(e.key)
|
||||
}
|
||||
|
||||
charsetComboBox.selectedItem = "UTF-8"
|
||||
|
||||
passiveComboBox.addItem(PassiveMode.Local)
|
||||
passiveComboBox.addItem(PassiveMode.Remote)
|
||||
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.folder
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.transport.sftp")
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow",
|
||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
||||
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${FTPI18n.getString("termora.plugins.ftp.passive")}:").xy(1, rows)
|
||||
.add(passiveComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
|
||||
.build()
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
enum class PassiveMode {
|
||||
Local,
|
||||
Remote,
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.I18n
|
||||
import app.termora.NamedI18n
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
|
||||
object FTPI18n : NamedI18n("i18n/messages") {
|
||||
private val log = LoggerFactory.getLogger(FTPI18n::class.java)
|
||||
|
||||
override fun getLogger(): Logger {
|
||||
return log
|
||||
}
|
||||
|
||||
override fun getString(key: String): String {
|
||||
return try {
|
||||
substitutor.replace(getBundle().getString(key))
|
||||
} catch (_: MissingResourceException) {
|
||||
I18n.getString(key)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.transfer.s3.S3Path
|
||||
|
||||
class FTPPath(fileSystem: FTPFileSystem, root: String?, names: List<String>) : S3Path(fileSystem, root, names) {
|
||||
override val isBucket: Boolean
|
||||
get() = false
|
||||
|
||||
override val bucketName: String
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override val objectName: String
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override fun getCustomType(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.PaidPlugin
|
||||
@@ -27,6 +24,7 @@ class FTPPlugin : PaidPlugin {
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.Disposer
|
||||
import app.termora.Host
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.BorderLayout
|
||||
|
||||
class FTPProtocolHostPanel : ProtocolHostPanel() {
|
||||
|
||||
private val pane = FTPHostOptionsPane()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
private fun initView() {
|
||||
add(pane, BorderLayout.CENTER)
|
||||
Disposer.register(this, pane)
|
||||
}
|
||||
|
||||
private fun initEvents() {}
|
||||
|
||||
override fun getHost(): Host {
|
||||
return Host(
|
||||
name = StringUtils.EMPTY,
|
||||
protocol = FTPProtocolProvider.PROTOCOL
|
||||
)
|
||||
return pane.getHost()
|
||||
}
|
||||
|
||||
override fun setHost(host: Host) {
|
||||
|
||||
pane.setHost(host)
|
||||
}
|
||||
|
||||
override fun validateFields(): Boolean {
|
||||
return true
|
||||
return pane.validateFields()
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
|
||||
class FTPProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolHostPanelExtension() }
|
||||
val instance = FTPProtocolHostPanelExtension()
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return FTPProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return FTPProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,33 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.AuthenticationType
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.Icons
|
||||
import app.termora.protocol.FileObjectHandler
|
||||
import app.termora.protocol.FileObjectRequest
|
||||
import app.termora.ProxyType
|
||||
import app.termora.protocol.PathHandler
|
||||
import app.termora.protocol.PathHandlerRequest
|
||||
import app.termora.protocol.TransferProtocolProvider
|
||||
import org.apache.commons.vfs2.provider.FileProvider
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.net.ftp.FTPClient
|
||||
import org.apache.commons.pool2.BasePooledObjectFactory
|
||||
import org.apache.commons.pool2.PooledObject
|
||||
import org.apache.commons.pool2.impl.DefaultPooledObject
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.nio.charset.Charset
|
||||
import java.time.Duration
|
||||
|
||||
|
||||
class FTPProtocolProvider private constructor() : TransferProtocolProvider {
|
||||
|
||||
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolProvider() }
|
||||
private val log = LoggerFactory.getLogger(FTPProtocolProvider::class.java)
|
||||
|
||||
val instance = FTPProtocolProvider()
|
||||
const val PROTOCOL = "FTP"
|
||||
}
|
||||
|
||||
@@ -22,12 +39,82 @@ class FTPProtocolProvider private constructor() : TransferProtocolProvider {
|
||||
return Icons.ftp
|
||||
}
|
||||
|
||||
override fun getFileProvider(): FileProvider {
|
||||
return FTPFileProvider.instance
|
||||
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
|
||||
val host = requester.host
|
||||
|
||||
val config = GenericObjectPoolConfig<FTPClient>().apply {
|
||||
maxTotal = 12
|
||||
// 与 transfer 最大传输量匹配
|
||||
maxIdle = 6
|
||||
minIdle = 1
|
||||
testOnBorrow = false
|
||||
testWhileIdle = true
|
||||
// 检测空闲对象线程每次运行时检测的空闲对象的数量
|
||||
timeBetweenEvictionRuns = Duration.ofSeconds(30)
|
||||
// 连接空闲的最小时间,达到此值后空闲链接将会被移除,且保留 minIdle 个空闲连接数
|
||||
softMinEvictableIdleDuration = Duration.ofSeconds(30)
|
||||
// 连接的最小空闲时间,达到此值后该空闲连接可能会被移除(还需看是否已达最大空闲连接数)
|
||||
minEvictableIdleDuration = Duration.ofMinutes(3)
|
||||
}
|
||||
|
||||
val ftpClientPool = GenericObjectPool(object : BasePooledObjectFactory<FTPClient>() {
|
||||
override fun create(): FTPClient {
|
||||
val client = FTPClient()
|
||||
client.charset = Charset.forName(host.options.encoding)
|
||||
client.controlEncoding = client.charset.name()
|
||||
client.connect(host.host, host.port)
|
||||
if (client.isConnected.not()) {
|
||||
throw IllegalStateException("FTP client is not connected")
|
||||
}
|
||||
|
||||
if (host.proxy.type == ProxyType.HTTP) {
|
||||
client.proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress(host.proxy.host, host.proxy.port))
|
||||
} else if (host.proxy.type == ProxyType.SOCKS5) {
|
||||
client.proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(host.proxy.host, host.proxy.port))
|
||||
}
|
||||
|
||||
val password = if (host.authentication.type == AuthenticationType.Password)
|
||||
host.authentication.password else StringUtils.EMPTY
|
||||
if (client.login(host.username, password).not()) {
|
||||
throw IllegalStateException("Incorrect account or password")
|
||||
}
|
||||
|
||||
if (host.options.extras["passive"] == FTPHostOptionsPane.PassiveMode.Remote.name) {
|
||||
client.enterRemotePassiveMode()
|
||||
} else {
|
||||
client.enterLocalPassiveMode()
|
||||
}
|
||||
|
||||
client.listHiddenFiles = true
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
override fun wrap(obj: FTPClient): PooledObject<FTPClient> {
|
||||
return DefaultPooledObject(obj)
|
||||
}
|
||||
|
||||
override fun validateObject(p: PooledObject<FTPClient>): Boolean {
|
||||
val ftp = p.`object`
|
||||
return ftp.isConnected.not() && ftp.sendNoOp()
|
||||
}
|
||||
|
||||
override fun destroyObject(p: PooledObject<FTPClient>) {
|
||||
try {
|
||||
p.`object`.disconnect()
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}, config)
|
||||
|
||||
val defaultPath = host.options.sftpDefaultDirectory
|
||||
val fs = FTPFileSystem(ftpClientPool)
|
||||
return PathHandler(fs, fs.getPath(defaultPath))
|
||||
}
|
||||
|
||||
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolProviderExtension() }
|
||||
val instance = FTPProtocolProviderExtension()
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return FTPProtocolProvider.Companion.instance
|
||||
return FTPProtocolProvider.instance
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.transfer.s3.S3FileSystemProvider
|
||||
import app.termora.transfer.s3.S3Path
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.net.ftp.FTPClient
|
||||
import org.apache.commons.net.ftp.FTPFile
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.AccessMode
|
||||
import java.nio.file.CopyOption
|
||||
import java.nio.file.NoSuchFileException
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.attribute.FileAttribute
|
||||
import java.nio.file.attribute.PosixFilePermission
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.exists
|
||||
|
||||
class FTPSystemProvider(private val pool: GenericObjectPool<FTPClient>) : S3FileSystemProvider() {
|
||||
|
||||
|
||||
override fun getScheme(): String? {
|
||||
return "ftp"
|
||||
}
|
||||
|
||||
override fun getOutputStream(path: S3Path): OutputStream {
|
||||
return createStreamer(path)
|
||||
}
|
||||
|
||||
override fun getInputStream(path: S3Path): InputStream {
|
||||
val ftp = pool.borrowObject()
|
||||
val fs = ftp.retrieveFileStream(path.absolutePathString())
|
||||
return object : InputStream() {
|
||||
override fun read(): Int {
|
||||
return fs.read()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(fs)
|
||||
ftp.completePendingCommand()
|
||||
pool.returnObject(ftp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createStreamer(path: S3Path): OutputStream {
|
||||
val ftp = pool.borrowObject()
|
||||
val os = ftp.storeFileStream(path.absolutePathString())
|
||||
return object : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
os.write(b)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(os)
|
||||
ftp.completePendingCommand()
|
||||
pool.returnObject(ftp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
|
||||
val paths = mutableListOf<S3Path>()
|
||||
if (path.exists().not()) {
|
||||
throw NoSuchFileException(path.absolutePathString())
|
||||
}
|
||||
|
||||
withFtpClient {
|
||||
val files = it.listFiles(path.absolutePathString())
|
||||
for (file in files) {
|
||||
val p = path.resolve(file.name)
|
||||
p.attributes = p.attributes.copy(
|
||||
directory = file.isDirectory,
|
||||
regularFile = file.isFile,
|
||||
size = file.size,
|
||||
lastModifiedTime = file.timestamp.timeInMillis,
|
||||
)
|
||||
p.attributes.permissions = ftpPermissionsToPosix(file)
|
||||
paths.add(p)
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun ftpPermissionsToPosix(file: FTPFile): Set<PosixFilePermission> {
|
||||
val perms = mutableSetOf<PosixFilePermission>()
|
||||
|
||||
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION))
|
||||
perms.add(PosixFilePermission.OWNER_READ)
|
||||
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OWNER_WRITE)
|
||||
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OWNER_EXECUTE)
|
||||
|
||||
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.READ_PERMISSION))
|
||||
perms.add(PosixFilePermission.GROUP_READ)
|
||||
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||
perms.add(PosixFilePermission.GROUP_WRITE)
|
||||
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||
perms.add(PosixFilePermission.GROUP_EXECUTE)
|
||||
|
||||
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.READ_PERMISSION))
|
||||
perms.add(PosixFilePermission.OTHERS_READ)
|
||||
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OTHERS_WRITE)
|
||||
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OTHERS_EXECUTE)
|
||||
|
||||
return perms
|
||||
}
|
||||
|
||||
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
|
||||
withFtpClient { it.mkd(dir.absolutePathString()) }
|
||||
}
|
||||
|
||||
override fun move(source: Path?, target: Path?, vararg options: CopyOption?) {
|
||||
if (source != null && target != null) {
|
||||
withFtpClient {
|
||||
it.rename(source.absolutePathString(), target.absolutePathString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(path: S3Path, isDirectory: Boolean) {
|
||||
withFtpClient {
|
||||
if (isDirectory) {
|
||||
it.rmd(path.absolutePathString())
|
||||
} else {
|
||||
it.deleteFile(path.absolutePathString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
|
||||
withFtpClient {
|
||||
if (it.cwd(path.absolutePathString()) == 250) {
|
||||
return
|
||||
}
|
||||
if (it.listFiles(path.absolutePathString()).isNotEmpty()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
throw NoSuchFileException(path.absolutePathString())
|
||||
}
|
||||
|
||||
private inline fun <T> withFtpClient(block: (FTPClient) -> T): T {
|
||||
val client = pool.borrowObject()
|
||||
return try {
|
||||
block(client)
|
||||
} finally {
|
||||
pool.returnObject(client)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<descriptions>
|
||||
<description>Connecting to FTP</description>
|
||||
<description language="zh_CN">支持连接到到 FTP</description>
|
||||
<description language="zh_CN">支持连接到 FTP</description>
|
||||
<description language="zh_TW">支援連接到 FTP</description>
|
||||
</descriptions>
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#6C707E"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#6C707E"></path></svg>
|
||||
<svg t="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#6C707E"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#6C707E"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1 +1 @@
|
||||
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#CED0D6"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#CED0D6"></path></svg>
|
||||
<svg t="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#CED0D6"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#CED0D6"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
1
plugins/ftp/src/main/resources/i18n/messages.properties
Normal file
1
plugins/ftp/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1 @@
|
||||
termora.plugins.ftp.passive=Passive Mode
|
||||
@@ -0,0 +1 @@
|
||||
termora.plugins.ftp.passive=被动模式
|
||||
@@ -0,0 +1 @@
|
||||
termora.plugins.ftp.passive=被動模式
|
||||
@@ -2,14 +2,14 @@ plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.6"
|
||||
project.version = "0.0.7"
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
implementation("com.maxmind.geoip2:geoip2:4.3.1")
|
||||
// https://github.com/hstyi/geolite2
|
||||
implementation("com.github.hstyi:geolite2:v1.0-202506280327")
|
||||
implementation("com.github.hstyi:geolite2:v1.0-202507040118")
|
||||
}
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.2"
|
||||
project.version = "0.0.3"
|
||||
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -46,7 +46,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
controlsVisible = false
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
title = I18n.getString("termora.doorman.safe")
|
||||
title = MigrationI18n.getString("termora.doorman.safe")
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||
}
|
||||
|
||||
@@ -65,8 +65,8 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
label.text = I18n.getString("termora.doorman.safe")
|
||||
tip.text = I18n.getString("termora.doorman.unlock-data")
|
||||
label.text = MigrationI18n.getString("termora.doorman.safe")
|
||||
tip.text = MigrationI18n.getString("termora.doorman.unlock-data")
|
||||
icon.icon = FlatSVGIcon(Icons.role.name, 80, 80)
|
||||
safeBtn.icon = Icons.unlocked
|
||||
|
||||
@@ -95,24 +95,24 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
.add(passwordTextField).xy(2, rows)
|
||||
.add(safeBtn).xy(4, rows).apply { rows += step }
|
||||
.add(tip).xyw(2, rows, 4, "center, fill").apply { rows += step }
|
||||
.add(JXHyperlink(object : AnAction(I18n.getString("termora.doorman.forget-password")) {
|
||||
.add(JXHyperlink(object : AnAction(MigrationI18n.getString("termora.doorman.forget-password")) {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val option = OptionPane.showConfirmDialog(
|
||||
this@DoormanDialog, I18n.getString("termora.doorman.forget-password-message"),
|
||||
this@DoormanDialog, MigrationI18n.getString("termora.doorman.forget-password-message"),
|
||||
options = arrayOf(
|
||||
I18n.getString("termora.doorman.have-a-mnemonic"),
|
||||
I18n.getString("termora.doorman.dont-have-a-mnemonic"),
|
||||
MigrationI18n.getString("termora.doorman.have-a-mnemonic"),
|
||||
MigrationI18n.getString("termora.doorman.dont-have-a-mnemonic"),
|
||||
),
|
||||
optionType = JOptionPane.YES_NO_OPTION,
|
||||
messageType = JOptionPane.INFORMATION_MESSAGE,
|
||||
initialValue = I18n.getString("termora.doorman.have-a-mnemonic")
|
||||
initialValue = MigrationI18n.getString("termora.doorman.have-a-mnemonic")
|
||||
)
|
||||
if (option == JOptionPane.YES_OPTION) {
|
||||
showMnemonicsDialog()
|
||||
} else if (option == JOptionPane.NO_OPTION) {
|
||||
OptionPane.showMessageDialog(
|
||||
this@DoormanDialog,
|
||||
I18n.getString("termora.doorman.delete-data"),
|
||||
MigrationI18n.getString("termora.doorman.delete-data"),
|
||||
messageType = JOptionPane.WARNING_MESSAGE
|
||||
)
|
||||
Application.browse(MigrationApplicationRunnerExtension.instance.getDatabaseFile().toURI())
|
||||
@@ -141,7 +141,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
OptionPane.showMessageDialog(
|
||||
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
||||
this, MigrationI18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
passwordTextField.outline = "error"
|
||||
@@ -166,7 +166,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
} catch (e: Exception) {
|
||||
if (e is PasswordWrongException) {
|
||||
OptionPane.showMessageDialog(
|
||||
this, I18n.getString("termora.doorman.password-wrong"),
|
||||
this, MigrationI18n.getString("termora.doorman.password-wrong"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
@@ -197,7 +197,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
isModal = true
|
||||
isResizable = true
|
||||
controlsVisible = false
|
||||
title = I18n.getString("termora.doorman.mnemonic.title")
|
||||
title = MigrationI18n.getString("termora.doorman.mnemonic.title")
|
||||
init()
|
||||
pack()
|
||||
size = Dimension(max(size.width, UIManager.getInt("Dialog.width") - 250), size.height)
|
||||
@@ -251,7 +251,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
} catch (e: Exception) {
|
||||
OptionPane.showMessageDialog(
|
||||
this,
|
||||
I18n.getString("termora.doorman.mnemonic.incorrect"),
|
||||
MigrationI18n.getString("termora.doorman.mnemonic.incorrect"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
return
|
||||
|
||||
@@ -7,3 +7,18 @@ termora.plugins.migration.message=<html> \
|
||||
<h3 align="center">📎 For more information, please see: <a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||
</html>
|
||||
termora.plugins.migration.migrate=Migrate
|
||||
|
||||
# Doorman
|
||||
termora.doorman.safe=Data is encrypted
|
||||
termora.doorman.unlock-data=Enter password to unlock data
|
||||
termora.doorman.password-wrong=Wrong password
|
||||
termora.doorman.forget-password=Forgot password?
|
||||
termora.doorman.delete-data=Delete the data catalog and restart, This will lose all data
|
||||
termora.doorman.forget-password-message=Unlock data with a mnemonic. Without it, data cannot be accessed
|
||||
termora.doorman.have-a-mnemonic=I have a mnemonic
|
||||
termora.doorman.dont-have-a-mnemonic=I don't have a mnemonic
|
||||
termora.doorman.mnemonic-data-corrupted=Unable to decrypt data with the mnemonic, the data maybe corrupted
|
||||
|
||||
termora.doorman.mnemonic.title=Enter 12 mnemonic words
|
||||
termora.doorman.mnemonic.incorrect=Incorrect mnemonic
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
# Doorman
|
||||
termora.doorman.safe=Данные защифрованы
|
||||
termora.doorman.unlock-data=Введите пароль для разблокировки данных
|
||||
termora.doorman.verify-password=Введите пароль для проверки
|
||||
termora.doorman.password-wrong=Неверный пароль
|
||||
termora.doorman.password-correct=Пароль верный
|
||||
termora.doorman.unsafe=Данные не зашифрованы
|
||||
termora.doorman.lock-data=Спрашивать пароль при запуске
|
||||
termora.doorman.forget-password=Забыли пароль?
|
||||
termora.doorman.delete-data=Удалить данные и перезапустить, это приведет к потере всех данных
|
||||
termora.doorman.forget-password-message=Разблокировать данные с помощью мнемоники. Без него доступ к данным невозможен.
|
||||
termora.doorman.have-a-mnemonic=У меня есть мнемоники
|
||||
termora.doorman.dont-have-a-mnemonic=У меня нет мнемоники
|
||||
termora.doorman.mnemonic-data-corrupted=Невозможно расшифровать данные с помощью мнемоники, возможно, данные повреждены.
|
||||
|
||||
termora.doorman.mnemonic.title=Введите 12 слов мнемоники
|
||||
termora.doorman.mnemonic.incorrect=Неверные мнемоники
|
||||
@@ -7,3 +7,17 @@ termora.plugins.migration.message=<html> \
|
||||
<h3 align="center">📎 更多信息请查看:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||
</html>
|
||||
termora.plugins.migration.migrate=迁移
|
||||
|
||||
# Doorman
|
||||
termora.doorman.safe=数据已加密
|
||||
termora.doorman.unlock-data=输入密码解锁数据
|
||||
termora.doorman.password-wrong=密码错误
|
||||
termora.doorman.forget-password=忘记密码?
|
||||
termora.doorman.delete-data=删除数据目录后重新启动程序,这样会丢失所有数据
|
||||
termora.doorman.forget-password-message=通过助记词解锁数据,没有助记词则无法解锁
|
||||
termora.doorman.have-a-mnemonic=我有助记词
|
||||
termora.doorman.dont-have-a-mnemonic=我没有助记词
|
||||
termora.doorman.mnemonic-data-corrupted=无法从助记词解密数据,数据可能已经损坏
|
||||
|
||||
termora.doorman.mnemonic.title=输入 12 个助记词
|
||||
termora.doorman.mnemonic.incorrect=助记词错误
|
||||
|
||||
@@ -7,3 +7,18 @@ termora.plugins.migration.message=<html> \
|
||||
<h3 align="center">📎 更多資訊請參見:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||
</html>
|
||||
termora.plugins.migration.migrate=遷移
|
||||
|
||||
|
||||
# Doorman
|
||||
termora.doorman.safe=資料已加密
|
||||
termora.doorman.unlock-data=輸入密碼解鎖資料
|
||||
termora.doorman.password-wrong=密碼錯誤
|
||||
termora.doorman.forget-password=忘記密碼?
|
||||
termora.doorman.delete-data=刪除資料目錄後重新啟動程序,這樣會遺失所有數據
|
||||
termora.doorman.forget-password-message=透過助記詞解鎖數據,沒有助記詞則無法解鎖
|
||||
termora.doorman.have-a-mnemonic=我有助記詞
|
||||
termora.doorman.dont-have-a-mnemonic=我沒有助記詞
|
||||
termora.doorman.mnemonic-data-corrupted=無法從助記詞解密數據,資料可能已損壞
|
||||
termora.doorman.mnemonic.title=輸入 12 個助記詞
|
||||
termora.doorman.mnemonic.incorrect=助記詞錯誤
|
||||
|
||||
|
||||
17
plugins/serial/build.gradle.kts
Normal file
17
plugins/serial/build.gradle.kts
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
|
||||
|
||||
project.version = "0.0.1"
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
implementation("com.fazecast:jSerialComm:2.11.2")
|
||||
}
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.plugin.internal.BasicGeneralOption
|
||||
@@ -1,11 +1,18 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.InternalPlugin
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.Plugin
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
internal class SerialInternalPlugin : InternalPlugin() {
|
||||
internal class SerialPlugin : Plugin {
|
||||
private val support = ExtensionSupport()
|
||||
|
||||
override fun getAuthor(): String {
|
||||
return "TermoraDev"
|
||||
}
|
||||
|
||||
init {
|
||||
support.addExtension(ProtocolProviderExtension::class.java) { SerialProtocolProviderExtension.instance }
|
||||
support.addExtension(ProtocolHostPanelExtension::class.java) { SerialProtocolHostPanelExtension.instance }
|
||||
@@ -13,7 +20,7 @@ internal class SerialInternalPlugin : InternalPlugin() {
|
||||
|
||||
|
||||
override fun getName(): String {
|
||||
return "Serial Protocol"
|
||||
return "Serial Comm"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.termora
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.terminal.PtyConnector
|
||||
import com.fazecast.jSerialComm.SerialPort
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.Disposer
|
||||
import app.termora.Host
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.actions.DataProvider
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
@@ -1,6 +1,9 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.Host
|
||||
import app.termora.Icons
|
||||
import app.termora.PtyHostTerminalTab
|
||||
import app.termora.WindowScope
|
||||
import app.termora.terminal.PtyConnector
|
||||
import org.apache.commons.io.Charsets
|
||||
import java.nio.charset.StandardCharsets
|
||||
@@ -1,5 +1,8 @@
|
||||
package app.termora
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.Host
|
||||
import app.termora.SerialCommFlowControl
|
||||
import app.termora.SerialCommParity
|
||||
import com.fazecast.jSerialComm.SerialPort
|
||||
|
||||
object Serials {
|
||||
22
plugins/serial/src/main/resources/META-INF/plugin.xml
Normal file
22
plugins/serial/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<termora-plugin>
|
||||
|
||||
<id>serial</id>
|
||||
|
||||
<name>Serial Comm</name>
|
||||
|
||||
<version>${projectVersion}</version>
|
||||
|
||||
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||
|
||||
<entry>app.termora.plugins.serial.SerialPlugin</entry>
|
||||
|
||||
<descriptions>
|
||||
<description>Supports access to serial ports</description>
|
||||
<description language="zh_CN">支持访问串口</description>
|
||||
<description language="zh_TW">支援訪問串口</description>
|
||||
</descriptions>
|
||||
|
||||
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||
|
||||
|
||||
</termora-plugin>
|
||||
@@ -0,0 +1 @@
|
||||
<svg t="1747210120200" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1169" width="16" height="16"><path d="M806.11718723 531.44140652l-78.2578125 78.50390571L410.22265625 291.21874973l82.08984402-78.53906223a231.46874973 231.46874973 0 0 1 162.49218723-68.66015625 206.54296902 206.54296902 0 0 1 131.02734402 44.296875L856.98828125 117.125a35.15625027 35.15625027 0 0 1 49.67578125-0.03515652h0.03515652a35.15625027 35.15625027 0 0 1 0 49.74609429l-74.35546875 74.35546875a225.21093777 225.21093777 0 0 1-26.22656304 290.25z m-191.63671875 27.07031223a24.57421875 24.57421875 0 0 1-1.51171821 33.08203098l-57.72656277 57.76171929 63.98437473 63.73828071-83.42578125 83.46093777a230.62499973 230.62499973 0 0 1-161.71874946 68.66015625 204.92578125 204.92578125 0 0 1-136.75781304-49.21874973l-95.83593723 95.80078071a24.18750027 24.18750027 0 0 1-34.24218723-0.07031223 24.890625 24.890625 0 0 1-0.35156277-34.76953098l95.94140598-100.12500027a225.98437473 225.98437473 0 0 1 21.09375-299.28515598L305.98437473 394.0859375l68.66015625 68.66015625L427.13281277 411.10156277a24.39843777 24.39843777 0 0 1 34.34765598 34.59375L409.94140652 497.234375l117.9140625 117.63281277 56.53125-57.5859375a20.28515652 20.28515652 0 0 1 30.09374946 1.23046848z m-112.32421875 199.19531223l44.05078179-44.05078098-240.22265679-240.46875-44.296875 44.57812473a169.91015625 169.91015625 0 0 0-4.67578071 240.18750027l4.640625 4.92187473a151.875 151.875 0 0 0 117.9140625 48.97265652 175.35937473 175.35937473 0 0 0 122.58984321-54.70312473v0.56249946zM933.875 512c0 232.98046875-188.89453125 421.875-421.875 421.875-7.87499973 0-15.71484375-0.2109375-23.48437473-0.6328125l73.12499946-73.16015598a351.77343777 351.77343777 0 0 0 298.44140679-298.40625027l73.12499946-73.16015598c0.45703152 7.76953098 0.66796902 15.609375 0.66796902 23.48437473zM535.48437473 90.7578125L462.35937527 163.953125a351.77343777 351.77343777 0 0 0-298.44140679 298.40625027l-73.12499946 73.16015598A428.625 428.625 0 0 1 90.125 512C90.125 279.01953125 279.01953125 90.125 512 90.125c7.87499973 0 15.71484375 0.2109375 23.48437473 0.6328125z" fill="#6C707E" p-id="1170"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg t="1747210120200" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1169"
|
||||
width="16" height="16">
|
||||
<path d="M806.11718723 531.44140652l-78.2578125 78.50390571L410.22265625 291.21874973l82.08984402-78.53906223a231.46874973 231.46874973 0 0 1 162.49218723-68.66015625 206.54296902 206.54296902 0 0 1 131.02734402 44.296875L856.98828125 117.125a35.15625027 35.15625027 0 0 1 49.67578125-0.03515652h0.03515652a35.15625027 35.15625027 0 0 1 0 49.74609429l-74.35546875 74.35546875a225.21093777 225.21093777 0 0 1-26.22656304 290.25z m-191.63671875 27.07031223a24.57421875 24.57421875 0 0 1-1.51171821 33.08203098l-57.72656277 57.76171929 63.98437473 63.73828071-83.42578125 83.46093777a230.62499973 230.62499973 0 0 1-161.71874946 68.66015625 204.92578125 204.92578125 0 0 1-136.75781304-49.21874973l-95.83593723 95.80078071a24.18750027 24.18750027 0 0 1-34.24218723-0.07031223 24.890625 24.890625 0 0 1-0.35156277-34.76953098l95.94140598-100.12500027a225.98437473 225.98437473 0 0 1 21.09375-299.28515598L305.98437473 394.0859375l68.66015625 68.66015625L427.13281277 411.10156277a24.39843777 24.39843777 0 0 1 34.34765598 34.59375L409.94140652 497.234375l117.9140625 117.63281277 56.53125-57.5859375a20.28515652 20.28515652 0 0 1 30.09374946 1.23046848z m-112.32421875 199.19531223l44.05078179-44.05078098-240.22265679-240.46875-44.296875 44.57812473a169.91015625 169.91015625 0 0 0-4.67578071 240.18750027l4.640625 4.92187473a151.875 151.875 0 0 0 117.9140625 48.97265652 175.35937473 175.35937473 0 0 0 122.58984321-54.70312473v0.56249946zM933.875 512c0 232.98046875-188.89453125 421.875-421.875 421.875-7.87499973 0-15.71484375-0.2109375-23.48437473-0.6328125l73.12499946-73.16015598a351.77343777 351.77343777 0 0 0 298.44140679-298.40625027l73.12499946-73.16015598c0.45703152 7.76953098 0.66796902 15.609375 0.66796902 23.48437473zM535.48437473 90.7578125L462.35937527 163.953125a351.77343777 351.77343777 0 0 0-298.44140679 298.40625027l-73.12499946 73.16015598A428.625 428.625 0 0 1 90.125 512C90.125 279.01953125 279.01953125 90.125 512 90.125c7.87499973 0 15.71484375 0.2109375 23.48437473 0.6328125z"
|
||||
fill="#CED0D6" p-id="1170"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -2,7 +2,7 @@ plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.2"
|
||||
project.version = "0.0.3"
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.transfer.s3.S3FileSystem
|
||||
import app.termora.transfer.s3.S3Path
|
||||
import com.hierynomus.smbj.session.Session
|
||||
import com.hierynomus.smbj.share.DiskShare
|
||||
|
||||
class SMBFileSystem(private val share: DiskShare, session: Session) :
|
||||
S3FileSystem(SMBFileSystemProvider(share, session)) {
|
||||
|
||||
override fun create(root: String?, names: List<String>): S3Path {
|
||||
val path = SMBPath(this, root, names)
|
||||
if (names.isEmpty()) {
|
||||
path.attributes = path.attributes.copy(directory = true)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
|
||||
override fun close() {
|
||||
share.close()
|
||||
super.close()
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.transfer.s3.S3Path
|
||||
|
||||
class SMBPath(fileSystem: SMBFileSystem, root: String?, names: List<String>) : S3Path(fileSystem, root, names) {
|
||||
override val isBucket: Boolean
|
||||
get() = false
|
||||
|
||||
override val bucketName: String
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override val objectName: String
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override fun getCustomType(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -7,7 +7,7 @@ include("plugins:s3")
|
||||
include("plugins:oss")
|
||||
include("plugins:cos")
|
||||
include("plugins:obs")
|
||||
//include("plugins:ftp")
|
||||
include("plugins:ftp")
|
||||
include("plugins:bg")
|
||||
include("plugins:sync")
|
||||
include("plugins:migration")
|
||||
@@ -15,3 +15,4 @@ include("plugins:editor")
|
||||
include("plugins:geo")
|
||||
include("plugins:webdav")
|
||||
include("plugins:smb")
|
||||
include("plugins:serial")
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.actions.ActionManager
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.keymap.KeymapManager
|
||||
import app.termora.plugin.ExtensionManager
|
||||
import app.termora.plugin.PluginManager
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
@@ -54,12 +52,6 @@ class ApplicationRunner {
|
||||
// 统计
|
||||
enableAnalytics()
|
||||
|
||||
// init ActionManager、KeymapManager、VFS
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
ActionManager.getInstance()
|
||||
KeymapManager.getInstance()
|
||||
}
|
||||
|
||||
// 设置 LAF
|
||||
setupLaf()
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.actions.MultipleAction
|
||||
import app.termora.database.DatabaseManager
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
@@ -18,10 +15,10 @@ import javax.swing.event.ListDataListener
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class CustomizeToolBarDialog(
|
||||
internal class CustomizeToolBarDialog(
|
||||
owner: Window,
|
||||
private val windowScope: WindowScope,
|
||||
private val toolbar: TermoraToolBar
|
||||
private val model: TermoraToolbarModel,
|
||||
) : DialogWrapper(owner) {
|
||||
|
||||
private val moveTopBtn = JButton(Icons.moveUp)
|
||||
@@ -40,6 +37,7 @@ class CustomizeToolBarDialog(
|
||||
private val actionManager get() = ActionManager.getInstance()
|
||||
|
||||
private var isOk = false
|
||||
private val actions = mutableListOf<ToolBarAction>()
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 150, UIManager.getInt("Dialog.height") - 100)
|
||||
@@ -147,7 +145,7 @@ class CustomizeToolBarDialog(
|
||||
resetBtn.addActionListener {
|
||||
leftList.model.removeAllElements()
|
||||
rightList.model.removeAllElements()
|
||||
for (action in toolbar.getAllActions()) {
|
||||
for (action in model.getAllActions()) {
|
||||
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
|
||||
}
|
||||
}
|
||||
@@ -258,7 +256,7 @@ class CustomizeToolBarDialog(
|
||||
override fun windowOpened(e: WindowEvent) {
|
||||
removeWindowListener(this)
|
||||
|
||||
for (action in toolbar.getActions()) {
|
||||
for (action in model.getActions()) {
|
||||
if (action.visible) {
|
||||
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
|
||||
} else {
|
||||
@@ -271,12 +269,7 @@ class CustomizeToolBarDialog(
|
||||
}
|
||||
|
||||
private fun getActionHolder(actionId: String): ActionHolder? {
|
||||
var action = actionManager.getAction(actionId)
|
||||
if (action == null) {
|
||||
if (actionId == MultipleAction.MULTIPLE) {
|
||||
action = MultipleAction.getInstance(windowScope)
|
||||
}
|
||||
}
|
||||
val action = actionManager.getAction(actionId)
|
||||
if (action == null) return null
|
||||
return ActionHolder(actionId, action)
|
||||
}
|
||||
@@ -365,12 +358,14 @@ class CustomizeToolBarDialog(
|
||||
actions.add(ToolBarAction(leftList.model.getElementAt(i).id, false))
|
||||
}
|
||||
|
||||
DatabaseManager.getInstance()
|
||||
.properties.putString("Termora.ToolBar.Actions", ohMyJson.encodeToString(actions))
|
||||
this.actions.clear()
|
||||
this.actions.addAll(actions)
|
||||
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
fun getActions()=actions
|
||||
|
||||
fun open(): Boolean {
|
||||
isModal = true
|
||||
isVisible = true
|
||||
|
||||
@@ -344,6 +344,11 @@ data class Host(
|
||||
|
||||
val isFolder get() = StringUtils.equalsIgnoreCase(protocol, "Folder")
|
||||
|
||||
/**
|
||||
* 临时的 SSH 不可以保存
|
||||
*/
|
||||
val isTemporary get() = options.extras["Temporary"] != null
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
@@ -21,9 +21,13 @@ class HostManager private constructor() : Disposable {
|
||||
*/
|
||||
fun addHost(host: Host, source: DatabaseChangedExtension.Source = DatabaseChangedExtension.Source.User) {
|
||||
assertEventDispatchThread()
|
||||
if (host.ownerType.isBlank()) {
|
||||
|
||||
if (host.isTemporary)
|
||||
throw IllegalArgumentException("Temporary host")
|
||||
|
||||
if (host.ownerType.isBlank())
|
||||
throw IllegalArgumentException("Owner type cannot be null")
|
||||
}
|
||||
|
||||
databaseManager.saveAndIncrementVersion(
|
||||
Data(
|
||||
id = host.id,
|
||||
|
||||
@@ -20,6 +20,7 @@ object I18n : AbstractI18n() {
|
||||
"en_US" to "English",
|
||||
"zh_CN" to "简体中文",
|
||||
"zh_TW" to "繁體中文",
|
||||
"ru_RU" to "Русский",
|
||||
)
|
||||
|
||||
fun containsLanguage(locale: Locale): String? {
|
||||
|
||||
@@ -27,7 +27,7 @@ class MultipleTerminalListener : TerminalPaintListener {
|
||||
) {
|
||||
val windowScope = AnActionEvent(terminalPanel, StringUtils.EMPTY, EventObject(terminalPanel))
|
||||
.getData(DataProviders.WindowScope) ?: return
|
||||
if (!MultipleAction.getInstance(windowScope).isSelected) return
|
||||
if (MultipleAction.getInstance().isSelected(windowScope).not()) return
|
||||
|
||||
val oldFont = g.font
|
||||
val colorPalette = terminal.getTerminalModel().getColorPalette()
|
||||
|
||||
165
src/main/kotlin/app/termora/MyTermoraToolbar.kt
Normal file
165
src/main/kotlin/app/termora/MyTermoraToolbar.kt
Normal file
@@ -0,0 +1,165 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.actions.StateAction
|
||||
import app.termora.findeverywhere.FindEverywhereAction
|
||||
import app.termora.plugin.internal.badge.Badge
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
import java.awt.AWTEvent
|
||||
import java.awt.Rectangle
|
||||
import java.awt.event.AWTEventListener
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.MouseEvent
|
||||
import java.beans.PropertyChangeEvent
|
||||
import java.beans.PropertyChangeListener
|
||||
import javax.swing.*
|
||||
|
||||
internal class MyTermoraToolbar(private val windowScope: WindowScope) : FlatToolBar() {
|
||||
|
||||
|
||||
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
|
||||
private val model get() = TermoraToolbarModel.getInstance()
|
||||
private val actionManager get() = model.getActionManager()
|
||||
private val toolbar get() = this
|
||||
|
||||
/**
|
||||
* 一次性生命周期 每次刷新都会重置
|
||||
*/
|
||||
private var disposable = Disposer.newDisposable()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
refreshActions()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
isFloatable = false
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
Disposer.register(windowScope, object : Disposable {
|
||||
override fun dispose() {
|
||||
Disposer.dispose(disposable)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// 监听全局事件
|
||||
toolkit.addAWTEventListener(customizeToolBarAWTEventListener, AWTEvent.MOUSE_EVENT_MASK)
|
||||
Disposer.register(windowScope, customizeToolBarAWTEventListener)
|
||||
|
||||
// 监听变化
|
||||
model.addTermoraToolbarModelListener(object : TermoraToolbarModel.TermoraToolbarModelListener {
|
||||
override fun onChanged() {
|
||||
refreshActions()
|
||||
}
|
||||
}).let { Disposer.register(windowScope, it) }
|
||||
|
||||
}
|
||||
|
||||
private fun refreshActions() {
|
||||
Disposer.dispose(disposable)
|
||||
disposable = Disposer.newDisposable()
|
||||
|
||||
removeAll()
|
||||
|
||||
add(JButton(object : AbstractAction() {
|
||||
init {
|
||||
putValue(SMALL_ICON, Icons.add)
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: ActionEvent) {
|
||||
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
|
||||
}
|
||||
}))
|
||||
|
||||
add(Box.createHorizontalGlue())
|
||||
|
||||
for (action in model.getActions()) {
|
||||
if (action.visible.not()) continue
|
||||
val action = actionManager.getAction(action.id) ?: continue
|
||||
add(redirectAction(action, disposable))
|
||||
}
|
||||
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun redirectAction(action: Action, disposable: Disposable): AbstractButton {
|
||||
val button = if (action is StateAction) JToggleButton() else JButton()
|
||||
button.toolTipText = action.getValue(Action.SHORT_DESCRIPTION) as? String
|
||||
button.icon = action.getValue(Action.SMALL_ICON) as? Icon
|
||||
button.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
action.actionPerformed(e)
|
||||
if (action is StateAction) {
|
||||
button.isSelected = action.isSelected(windowScope)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val listener = object : PropertyChangeListener, Disposable {
|
||||
private val badge get() = Badge.getInstance(windowScope)
|
||||
override fun propertyChange(evt: PropertyChangeEvent) {
|
||||
if (evt.propertyName == "Badge") {
|
||||
if (action.getValue("Badge") == true) {
|
||||
badge.addBadge(button)
|
||||
} else {
|
||||
badge.removeBadge(button)
|
||||
}
|
||||
}
|
||||
|
||||
if (action is StateAction) {
|
||||
button.isSelected = action.isSelected(windowScope)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
action.removePropertyChangeListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
action.addPropertyChangeListener(listener)
|
||||
Disposer.register(disposable, listener)
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
/**
|
||||
* 对着 ToolBar 右键
|
||||
*/
|
||||
private inner class CustomizeToolBarAWTEventListener : AWTEventListener, Disposable {
|
||||
override fun eventDispatched(event: AWTEvent) {
|
||||
if (event !is MouseEvent || event.id != MouseEvent.MOUSE_CLICKED
|
||||
|| SwingUtilities.isRightMouseButton(event).not()
|
||||
) return
|
||||
|
||||
// 如果 ToolBar 没有显示
|
||||
if (toolbar.isShowing.not()) return
|
||||
|
||||
// 如果不是作用于在 ToolBar 上面
|
||||
if (Rectangle(toolbar.locationOnScreen, toolbar.size).contains(event.locationOnScreen).not()) return
|
||||
|
||||
// 显示右键菜单
|
||||
showContextMenu(event)
|
||||
}
|
||||
|
||||
private fun showContextMenu(event: MouseEvent) {
|
||||
val popupMenu = FlatPopupMenu()
|
||||
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
|
||||
val owner = windowScope.window
|
||||
val dialog = CustomizeToolBarDialog(owner, windowScope, model)
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
if (dialog.open()) {
|
||||
model.setActions(dialog.getActions())
|
||||
}
|
||||
}
|
||||
popupMenu.show(event.component, event.x, event.y)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
toolkit.removeAWTEventListener(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,8 @@ class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(own
|
||||
preferredSize = size
|
||||
minimumSize = size
|
||||
|
||||
rememberCheckBox.isVisible = host.isTemporary.not()
|
||||
|
||||
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
@@ -84,7 +86,7 @@ class RequestAuthenticationDialog(owner: Window, host: Host) : DialogWrapper(own
|
||||
|
||||
switchPasswordComponent()
|
||||
|
||||
return FormBuilder.create().padding("$formMargin, $formMargin, $formMargin, $formMargin")
|
||||
return FormBuilder.create().padding("1dlu, $formMargin, $formMargin, $formMargin")
|
||||
.layout(layout)
|
||||
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, 1)
|
||||
.add(authenticationTypeComboBox).xy(3, 1)
|
||||
|
||||
@@ -133,8 +133,8 @@ class TerminalPanelFactory : Disposable {
|
||||
return
|
||||
}
|
||||
|
||||
val multipleAction = MultipleAction.getInstance(windowScope)
|
||||
if (!multipleAction.isSelected) {
|
||||
val multipleAction = MultipleAction.getInstance()
|
||||
if (multipleAction.isSelected(windowScope).not()) {
|
||||
ptyConnector.write(request.buffer)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.*
|
||||
import java.awt.event.AWTEventListener
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
@@ -28,13 +27,10 @@ import kotlin.math.min
|
||||
|
||||
class TerminalTabbed(
|
||||
private val windowScope: WindowScope,
|
||||
private val termoraToolBar: TermoraToolBar,
|
||||
private val tabbedPane: FlatTabbedPane,
|
||||
private val layout: TermoraLayout,
|
||||
) : JPanel(BorderLayout()), Disposable, TerminalTabbedManager, DataProvider {
|
||||
private val tabs = mutableListOf<TerminalTab>()
|
||||
private val customizeToolBarAWTEventListener = CustomizeToolBarAWTEventListener()
|
||||
private val toolbar = termoraToolBar.getJToolBar()
|
||||
private val actionManager = ActionManager.getInstance()
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private val appearance get() = DatabaseManager.getInstance().appearance
|
||||
@@ -60,8 +56,6 @@ class TerminalTabbed(
|
||||
tabbedPane.isTabsClosable = true
|
||||
tabbedPane.tabType = FlatTabbedPane.TabType.card
|
||||
|
||||
tabbedPane.trailingComponent = toolbar
|
||||
|
||||
add(tabbedPane, BorderLayout.CENTER)
|
||||
|
||||
windowScope.getOrCreate(TerminalTabbedManager::class) { this }
|
||||
@@ -72,7 +66,6 @@ class TerminalTabbed(
|
||||
|
||||
|
||||
private fun initEvents() {
|
||||
Disposer.register(this, customizeToolBarAWTEventListener)
|
||||
|
||||
// 关闭 tab
|
||||
tabbedPane.setTabCloseCallback { _, i -> removeTabAt(i, true) }
|
||||
@@ -146,9 +139,6 @@ class TerminalTabbed(
|
||||
}
|
||||
}).let { Disposer.register(this, it) }
|
||||
|
||||
// 监听全局事件
|
||||
toolkit.addAWTEventListener(customizeToolBarAWTEventListener, AWTEvent.MOUSE_EVENT_MASK)
|
||||
|
||||
}
|
||||
|
||||
private fun removeTabAt(index: Int, disposable: Boolean = true) {
|
||||
@@ -301,9 +291,7 @@ class TerminalTabbed(
|
||||
|
||||
// 关闭
|
||||
val close = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close"))
|
||||
close.addActionListener {
|
||||
tabbedPane.tabCloseCallback?.accept(tabbedPane, tabIndex)
|
||||
}
|
||||
close.addActionListener { tabbedPane.tabCloseCallback?.accept(tabbedPane, tabIndex) }
|
||||
|
||||
// 关闭其他标签页
|
||||
popupMenu.add(I18n.getString("termora.tabbed.contextmenu.close-other-tabs")).addActionListener {
|
||||
@@ -326,7 +314,7 @@ class TerminalTabbed(
|
||||
close.isEnabled = tab.canClose()
|
||||
rename.isEnabled = close.isEnabled
|
||||
clone.isEnabled = close.isEnabled
|
||||
edit.isEnabled = tab is HostTerminalTab && tab.host.id != "local"
|
||||
edit.isEnabled = tab is HostTerminalTab && tab.host.id != "local" && tab.host.isTemporary.not()
|
||||
openInNewWindow.isEnabled = close.isEnabled
|
||||
|
||||
// 如果不允许克隆
|
||||
@@ -337,12 +325,7 @@ class TerminalTabbed(
|
||||
if (close.isEnabled) {
|
||||
popupMenu.addSeparator()
|
||||
val reconnect = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
||||
reconnect.addActionListener {
|
||||
if (tabIndex > 0) {
|
||||
tabs[tabIndex].reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
reconnect.addActionListener { tabs[tabIndex].reconnect() }
|
||||
reconnect.isEnabled = tabs[tabIndex].canReconnect()
|
||||
}
|
||||
|
||||
@@ -384,60 +367,6 @@ class TerminalTabbed(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 对着 ToolBar 右键
|
||||
*/
|
||||
private inner class CustomizeToolBarAWTEventListener : AWTEventListener, Disposable {
|
||||
override fun eventDispatched(event: AWTEvent) {
|
||||
if (event !is MouseEvent || event.id != MouseEvent.MOUSE_CLICKED || !SwingUtilities.isRightMouseButton(event)) return
|
||||
// 如果 ToolBar 没有显示
|
||||
if (!toolbar.isShowing) return
|
||||
// 如果不是作用于在 ToolBar 上面
|
||||
if (!Rectangle(toolbar.locationOnScreen, toolbar.size).contains(event.locationOnScreen)) return
|
||||
|
||||
// 显示右键菜单
|
||||
showContextMenu(event)
|
||||
}
|
||||
|
||||
private fun showContextMenu(event: MouseEvent) {
|
||||
val popupMenu = FlatPopupMenu()
|
||||
popupMenu.add(I18n.getString("termora.toolbar.customize-toolbar")).addActionListener {
|
||||
val owner = SwingUtilities.getWindowAncestor(this@TerminalTabbed)
|
||||
val dialog = CustomizeToolBarDialog(owner, windowScope, termoraToolBar)
|
||||
dialog.setLocationRelativeTo(owner)
|
||||
if (dialog.open()) {
|
||||
TermoraToolBar.rebuild()
|
||||
}
|
||||
}
|
||||
popupMenu.show(event.component, event.x, event.y)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
toolkit.removeAWTEventListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
/*private inner class CustomizeToolBarDialog(owner: Window) : DialogWrapper(owner) {
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height"))
|
||||
isModal = true
|
||||
title = I18n.getString("termora.setting")
|
||||
setLocationRelativeTo(null)
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val model = DefaultListModel<String>()
|
||||
val checkBoxList = CheckBoxList(model)
|
||||
checkBoxList.fixedCellHeight = UIManager.getInt("Tree.rowHeight")
|
||||
model.addElement("Test")
|
||||
return checkBoxList
|
||||
}
|
||||
|
||||
}*/
|
||||
|
||||
private inner class SwitchFindEverywhereResult(
|
||||
private val title: String,
|
||||
private val icon: Icon?,
|
||||
|
||||
@@ -107,7 +107,7 @@ class TermoraFencePanel(
|
||||
label.foreground = UIManager.getColor("textInactiveText")
|
||||
label.font = label.font.deriveFont(Font.BOLD)
|
||||
// 与最后一个按钮对冲,使其宽度和谐
|
||||
box.add(JButton(Icons.empty))
|
||||
box.add(Box.createHorizontalStrut(24))
|
||||
box.add(Box.createHorizontalGlue())
|
||||
if (SystemInfo.isMacOS.not()) {
|
||||
box.add(label)
|
||||
@@ -153,7 +153,8 @@ class TermoraFencePanel(
|
||||
|
||||
|
||||
override fun dispose() {
|
||||
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
|
||||
if (leftTreePanel.isVisible)
|
||||
enableManager.setFlag("Termora.Fence.dividerLocation", max(splitPane.dividerLocation, 10))
|
||||
}
|
||||
|
||||
fun getHostTree(): NewHostTree {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package app.termora
|
||||
|
||||
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviderSupport
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.actions.OpenHostAction
|
||||
import app.termora.actions.*
|
||||
import app.termora.database.DatabaseChangedExtension
|
||||
import app.termora.database.DatabasePropertiesChangedExtension
|
||||
import app.termora.findeverywhere.FindEverywhereProvider
|
||||
import app.termora.findeverywhere.FindEverywhereProviderExtension
|
||||
import app.termora.findeverywhere.FindEverywhereResult
|
||||
import app.termora.keymap.KeyShortcut
|
||||
import app.termora.keymap.KeymapManager
|
||||
import app.termora.plugin.ExtensionManager
|
||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||
import app.termora.plugin.internal.ssh.SSHProtocolProvider
|
||||
@@ -21,16 +22,12 @@ import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jetbrains.JBR
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import java.awt.*
|
||||
import java.awt.event.*
|
||||
import java.util.*
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JFrame
|
||||
import javax.swing.*
|
||||
import javax.swing.SwingUtilities.isEventDispatchThread
|
||||
import javax.swing.UIManager
|
||||
|
||||
|
||||
fun assertEventDispatchThread() {
|
||||
@@ -45,22 +42,25 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
private val id = UUID.randomUUID().toString()
|
||||
private val windowScope = ApplicationScope.forWindowScope(this)
|
||||
private val tabbedPane = MyTabbedPane().apply { tabHeight = titleBarHeight }
|
||||
private val toolbar = TermoraToolBar(windowScope, this)
|
||||
private val terminalTabbed = TerminalTabbed(windowScope, toolbar, tabbedPane, layout)
|
||||
private val toolbar = MyTermoraToolbar(windowScope)
|
||||
private val terminalTabbed = TerminalTabbed(windowScope, tabbedPane, layout)
|
||||
private val dataProviderSupport = DataProviderSupport()
|
||||
private var notifyListeners = emptyArray<NotifyListener>()
|
||||
private val moveMouseAdapter = createMoveMouseAdaptor()
|
||||
|
||||
private val keymapManager get() = KeymapManager.getInstance()
|
||||
private val actionManager get() = ActionManager.getInstance()
|
||||
private val dynamicExtensionHandler get() = DynamicExtensionHandler.getInstance()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
initKeymap()
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
if (SystemInfo.isLinux) {
|
||||
toolbar.getJToolBar().addMouseListener(moveMouseAdapter)
|
||||
toolbar.getJToolBar().addMouseMotionListener(moveMouseAdapter)
|
||||
toolbar.addMouseListener(moveMouseAdapter)
|
||||
toolbar.addMouseMotionListener(moveMouseAdapter)
|
||||
} else if (SystemInfo.isMacOS) {
|
||||
terminalTabbed.addMouseListener(moveMouseAdapter)
|
||||
terminalTabbed.addMouseMotionListener(moveMouseAdapter)
|
||||
@@ -68,12 +68,20 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
tabbedPane.addMouseListener(moveMouseAdapter)
|
||||
tabbedPane.addMouseMotionListener(moveMouseAdapter)
|
||||
|
||||
toolbar.getJToolBar().addMouseListener(moveMouseAdapter)
|
||||
toolbar.getJToolBar().addMouseMotionListener(moveMouseAdapter)
|
||||
toolbar.addMouseListener(moveMouseAdapter)
|
||||
toolbar.addMouseMotionListener(moveMouseAdapter)
|
||||
}
|
||||
|
||||
// 快捷键变动时重新监听
|
||||
val refresher = KeymapRefresher()
|
||||
dynamicExtensionHandler.register(DatabasePropertiesChangedExtension::class.java, refresher)
|
||||
.let { Disposer.register(windowScope, it) }
|
||||
dynamicExtensionHandler.register(DatabaseChangedExtension::class.java, refresher)
|
||||
.let { Disposer.register(windowScope, it) }
|
||||
|
||||
|
||||
// FindEverywhere
|
||||
DynamicExtensionHandler.getInstance()
|
||||
dynamicExtensionHandler
|
||||
.register(FindEverywhereProviderExtension::class.java, object : FindEverywhereProviderExtension {
|
||||
private val hostTreeModel get() = NewHostTreeModel.getInstance()
|
||||
|
||||
@@ -115,8 +123,7 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
private val showMoreInfo get() = EnableManager.getInstance().isShowMoreInfo()
|
||||
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
ActionManager.getInstance()
|
||||
.getAction(OpenHostAction.OPEN_HOST)
|
||||
actionManager.getAction(OpenHostAction.OPEN_HOST)
|
||||
?.actionPerformed(OpenHostActionEvent(e.source, host, e))
|
||||
}
|
||||
|
||||
@@ -149,7 +156,6 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun initView() {
|
||||
|
||||
// macOS 要避开左边的控制栏
|
||||
@@ -162,6 +168,8 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
tabbedPane.tabAreaInsets = Insets(1, 2, 0, 0)
|
||||
}
|
||||
|
||||
tabbedPane.trailingComponent = toolbar
|
||||
|
||||
val height = UIManager.getInt("TabbedPane.tabHeight") + tabbedPane.tabAreaInsets.top
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
@@ -209,6 +217,61 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
dataProviderSupport.addData(DataProviders.WindowScope, windowScope)
|
||||
}
|
||||
|
||||
private fun initKeymap() {
|
||||
assertEventDispatchThread()
|
||||
|
||||
val keymap = keymapManager.getActiveKeymap()
|
||||
val inputMap = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
|
||||
val actionMap = rootPane.actionMap
|
||||
|
||||
// 移除之前所有的快捷键
|
||||
inputMap.clear()
|
||||
actionMap.clear()
|
||||
|
||||
for ((shortcut, actionIds) in keymap.getShortcuts()) {
|
||||
if (shortcut !is KeyShortcut) continue
|
||||
if (actionIds.contains(SwitchTabAction.SWITCH_TAB)) continue
|
||||
registerKeyStroke(actionMap, inputMap, shortcut.keyStroke, actionIds)
|
||||
}
|
||||
|
||||
for (shortcut in keymap.getShortcut(SwitchTabAction.SWITCH_TAB)) {
|
||||
if (shortcut !is KeyShortcut) continue
|
||||
registerKeyStroke(actionMap, inputMap, shortcut.keyStroke, listOf(SwitchTabAction.SWITCH_TAB))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun registerKeyStroke(
|
||||
actionMap: ActionMap,
|
||||
inputMap: InputMap,
|
||||
keyStroke: KeyStroke,
|
||||
actionIds: List<String>
|
||||
) {
|
||||
val keyShortcutActionId = "KeyShortcutAction_${randomUUID()}"
|
||||
actionMap.put(keyShortcutActionId, redirectAction(actionIds))
|
||||
inputMap.put(keyStroke, keyShortcutActionId)
|
||||
}
|
||||
|
||||
private fun redirectAction(actionIds: List<String>): Action {
|
||||
return object : AbstractAction() {
|
||||
private val keyboardFocusManager get() = KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
var source = e.source
|
||||
if (source == rootPane) {
|
||||
val focusOwner = keyboardFocusManager.focusOwner
|
||||
if (focusOwner is JComponent) {
|
||||
source = focusOwner
|
||||
}
|
||||
}
|
||||
|
||||
for (actionId in actionIds) {
|
||||
val action = actionManager.getAction(actionId) ?: continue
|
||||
action.actionPerformed(RedirectAnActionEvent(source, e.actionCommand, EventQueue.getCurrentEvent()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||
return dataProviderSupport.getData(dataKey) ?: terminalTabbed.getData(dataKey)
|
||||
}
|
||||
@@ -355,6 +418,35 @@ class TermoraFrame : JFrame(), DataProvider {
|
||||
return object : MouseAdapter() {}
|
||||
}
|
||||
|
||||
private inner class KeymapRefresher : DatabasePropertiesChangedExtension, DatabaseChangedExtension {
|
||||
|
||||
override fun onDataChanged(
|
||||
id: String,
|
||||
type: String,
|
||||
action: DatabaseChangedExtension.Action,
|
||||
source: DatabaseChangedExtension.Source
|
||||
) {
|
||||
if (type != "Keymap") return
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun onPropertyChanged(name: String, key: String, value: String) {
|
||||
if (name != "Setting.Properties") return
|
||||
if (key != "Keymap.Active") return
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
initKeymap()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private inner class RedirectAnActionEvent(
|
||||
source: Any,
|
||||
command: String,
|
||||
event: EventObject
|
||||
) : AnActionEvent(source, command, event)
|
||||
|
||||
private inner class GlassPane : JComponent() {
|
||||
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.actions.*
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.findeverywhere.FindEverywhereAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionContainerFactory
|
||||
import java.awt.Rectangle
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import javax.swing.Box
|
||||
import javax.swing.JToolBar
|
||||
|
||||
|
||||
@Serializable
|
||||
data class ToolBarAction(
|
||||
val id: String,
|
||||
val visible: Boolean,
|
||||
)
|
||||
|
||||
class TermoraToolBar(
|
||||
private val windowScope: WindowScope,
|
||||
private val frame: TermoraFrame,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun rebuild() {
|
||||
for (frame in TermoraFrameManager.getInstance().getWindows()) {
|
||||
val toolbars = SwingUtils.getDescendantsOfClass(MyToolBar::class.java, frame)
|
||||
for (toolbar in toolbars) {
|
||||
toolbar.rebuild()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val properties get() = DatabaseManager.getInstance().properties
|
||||
private val toolbar by lazy { MyToolBar().apply { rebuild() } }
|
||||
|
||||
|
||||
fun getJToolBar(): JToolBar {
|
||||
return toolbar
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取到所有的 Action
|
||||
*/
|
||||
fun getAllActions(): List<ToolBarAction> {
|
||||
return listOf(
|
||||
ToolBarAction(SnippetAction.SNIPPET, true),
|
||||
ToolBarAction(Actions.SFTP, true),
|
||||
ToolBarAction(Actions.TERMINAL_LOGGER, true),
|
||||
ToolBarAction(Actions.MACRO, true),
|
||||
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
|
||||
ToolBarAction(Actions.KEY_MANAGER, true),
|
||||
ToolBarAction(MultipleAction.MULTIPLE, true),
|
||||
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
|
||||
ToolBarAction(SettingsAction.SETTING, true),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取到所有 Action,会根据用户个性化排序/显示
|
||||
*/
|
||||
fun getActions(): List<ToolBarAction> {
|
||||
val text = properties.getString(
|
||||
"Termora.ToolBar.Actions",
|
||||
StringUtils.EMPTY
|
||||
)
|
||||
|
||||
val actions = getAllActions()
|
||||
|
||||
if (text.isBlank()) {
|
||||
return actions
|
||||
}
|
||||
|
||||
// 存储的 action
|
||||
val storageActions = (ohMyJson.runCatching {
|
||||
ohMyJson.decodeFromString<List<ToolBarAction>>(text)
|
||||
}.getOrNull() ?: return actions).toMutableList()
|
||||
|
||||
for (action in actions) {
|
||||
// 如果存储的 action 不包含这个,那么这个可能是新增的,新增的默认显示出来
|
||||
if (storageActions.none { it.id == action.id }) {
|
||||
storageActions.addFirst(ToolBarAction(action.id, true))
|
||||
}
|
||||
}
|
||||
|
||||
// 如果存储的 Action 在所有 Action 里没有,那么移除
|
||||
storageActions.removeIf { e -> actions.none { e.id == it.id } }
|
||||
|
||||
return storageActions
|
||||
}
|
||||
|
||||
private inner class MyToolBar : JToolBar() {
|
||||
init {
|
||||
// 监听窗口大小变动,然后修改边距避开控制按钮
|
||||
addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentResized(e: ComponentEvent) {
|
||||
adjust()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun adjust() {
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
val rectangle =
|
||||
frame.rootPane.getClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT_BUTTONS_BOUNDS)
|
||||
as? Rectangle ?: return
|
||||
val right = rectangle.width
|
||||
val toolbar = this@MyToolBar
|
||||
for (i in 0 until toolbar.componentCount) {
|
||||
val c = toolbar.getComponent(i)
|
||||
if (c.name == "spacing") {
|
||||
if (c.width == right) {
|
||||
return
|
||||
}
|
||||
toolbar.remove(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (right > 0) {
|
||||
val spacing = Box.createHorizontalStrut(right)
|
||||
spacing.name = "spacing"
|
||||
toolbar.add(spacing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun rebuild() {
|
||||
val toolbar: JToolBar = this
|
||||
val actionManager = ActionManager.getInstance()
|
||||
val actionContainerFactory = ActionContainerFactory(actionManager)
|
||||
|
||||
toolbar.removeAll()
|
||||
|
||||
toolbar.add(actionContainerFactory.createButton(object : AnAction(StringUtils.EMPTY, Icons.add) {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.actionPerformed(evt)
|
||||
}
|
||||
|
||||
override fun isEnabled(): Boolean {
|
||||
return actionManager.getAction(FindEverywhereAction.FIND_EVERYWHERE)?.isEnabled ?: false
|
||||
}
|
||||
}))
|
||||
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
|
||||
if (SystemInfo.isLinux || SystemInfo.isWindows) {
|
||||
toolbar.add(Box.createHorizontalStrut(16))
|
||||
}
|
||||
|
||||
|
||||
// update btn
|
||||
val updateBtn = actionContainerFactory.createButton(actionManager.getAction(Actions.APP_UPDATE))
|
||||
updateBtn.isVisible = updateBtn.isEnabled
|
||||
updateBtn.addChangeListener { updateBtn.isVisible = updateBtn.isEnabled }
|
||||
toolbar.add(updateBtn)
|
||||
|
||||
|
||||
// 获取显示的Action,如果不是 false 那么就是显示出来
|
||||
for (action in getActions()) {
|
||||
if (action.visible) {
|
||||
val ac = actionManager.getAction(action.id)
|
||||
if (ac == null) {
|
||||
if (action.id == MultipleAction.MULTIPLE) {
|
||||
toolbar.add(actionContainerFactory.createButton(MultipleAction.getInstance(windowScope)))
|
||||
}
|
||||
} else {
|
||||
toolbar.add(actionContainerFactory.createButton(ac))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (toolbar is MyToolBar) {
|
||||
toolbar.adjust()
|
||||
}
|
||||
|
||||
toolbar.revalidate()
|
||||
toolbar.repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
103
src/main/kotlin/app/termora/TermoraToolbarModel.kt
Normal file
103
src/main/kotlin/app/termora/TermoraToolbarModel.kt
Normal file
@@ -0,0 +1,103 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.actions.ActionManager
|
||||
import app.termora.actions.MultipleAction
|
||||
import app.termora.actions.SettingsAction
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.findeverywhere.FindEverywhereAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.util.*
|
||||
import javax.swing.event.EventListenerList
|
||||
|
||||
internal class TermoraToolbarModel private constructor() {
|
||||
companion object {
|
||||
fun getInstance(): TermoraToolbarModel {
|
||||
return ApplicationScope.forApplicationScope()
|
||||
.getOrCreate(TermoraToolbarModel::class) { TermoraToolbarModel() }
|
||||
}
|
||||
}
|
||||
|
||||
private val properties get() = DatabaseManager.getInstance().properties
|
||||
private val eventListener = EventListenerList()
|
||||
|
||||
|
||||
fun getActionManager() = ActionManager.getInstance()
|
||||
|
||||
/**
|
||||
* 获取到所有的 Action
|
||||
*/
|
||||
fun getAllActions(): List<ToolBarAction> {
|
||||
return listOf(
|
||||
ToolBarAction(SnippetAction.SNIPPET, true),
|
||||
ToolBarAction(Actions.SFTP, true),
|
||||
ToolBarAction(Actions.TERMINAL_LOGGER, true),
|
||||
ToolBarAction(Actions.MACRO, true),
|
||||
ToolBarAction(Actions.KEYWORD_HIGHLIGHT, true),
|
||||
ToolBarAction(Actions.KEY_MANAGER, true),
|
||||
ToolBarAction(MultipleAction.MULTIPLE, true),
|
||||
ToolBarAction(FindEverywhereAction.FIND_EVERYWHERE, true),
|
||||
ToolBarAction(SettingsAction.SETTING, true),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取到所有 Action,会根据用户个性化排序/显示
|
||||
*/
|
||||
fun getActions(): List<ToolBarAction> {
|
||||
val text = properties.getString(
|
||||
"Termora.ToolBar.Actions",
|
||||
StringUtils.EMPTY
|
||||
)
|
||||
|
||||
val actions = getAllActions()
|
||||
|
||||
if (text.isBlank()) {
|
||||
return actions
|
||||
}
|
||||
|
||||
// 存储的 action
|
||||
val storageActions = (ohMyJson.runCatching {
|
||||
ohMyJson.decodeFromString<List<ToolBarAction>>(text)
|
||||
}.getOrNull() ?: return actions).toMutableList()
|
||||
|
||||
for (action in actions) {
|
||||
// 如果存储的 action 不包含这个,那么这个可能是新增的,新增的默认显示出来
|
||||
if (storageActions.none { it.id == action.id }) {
|
||||
storageActions.addFirst(ToolBarAction(action.id, true))
|
||||
}
|
||||
}
|
||||
|
||||
// 如果存储的 Action 在所有 Action 里没有,那么移除
|
||||
storageActions.removeIf { e -> actions.none { e.id == it.id } }
|
||||
|
||||
return storageActions
|
||||
}
|
||||
|
||||
fun setActions(actions: List<ToolBarAction>) {
|
||||
assertEventDispatchThread()
|
||||
properties.putString("Termora.ToolBar.Actions", ohMyJson.encodeToString(actions))
|
||||
for (listener in eventListener.getListeners(TermoraToolbarModelListener::class.java)) {
|
||||
listener.onChanged()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun addTermoraToolbarModelListener(listener: TermoraToolbarModelListener): Disposable {
|
||||
eventListener.add(TermoraToolbarModelListener::class.java, listener)
|
||||
return object : Disposable {
|
||||
override fun dispose() {
|
||||
removeTermoraToolbarModelListener(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeTermoraToolbarModelListener(listener: TermoraToolbarModelListener) {
|
||||
eventListener.remove(TermoraToolbarModelListener::class.java, listener)
|
||||
}
|
||||
|
||||
interface TermoraToolbarModelListener : EventListener {
|
||||
fun onChanged()
|
||||
}
|
||||
}
|
||||
9
src/main/kotlin/app/termora/ToolBarAction.kt
Normal file
9
src/main/kotlin/app/termora/ToolBarAction.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package app.termora
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ToolBarAction(
|
||||
val id: String,
|
||||
val visible: Boolean,
|
||||
)
|
||||
@@ -3,7 +3,6 @@ package app.termora.account
|
||||
import app.termora.AES
|
||||
import app.termora.database.*
|
||||
import app.termora.database.Data.Companion.toData
|
||||
import okhttp3.internal.EMPTY_BYTE_ARRAY
|
||||
import org.apache.commons.codec.binary.Base64
|
||||
import org.apache.commons.codec.digest.DigestUtils
|
||||
import org.apache.commons.lang3.ObjectUtils
|
||||
@@ -88,7 +87,7 @@ abstract class SyncService {
|
||||
return accountManager.getSecretKey()
|
||||
}
|
||||
val team = accountManager.getTeams().firstOrNull { it.id == ownerId }
|
||||
return team?.secretKey ?: EMPTY_BYTE_ARRAY
|
||||
return team?.secretKey ?: byteArrayOf()
|
||||
}
|
||||
|
||||
protected fun decryptData(id: String, data: String, ownerId: String): String {
|
||||
|
||||
@@ -28,12 +28,14 @@ class ActionManager : org.jdesktop.swingx.action.ActionManager() {
|
||||
private fun registerActions() {
|
||||
addAction(NewWindowAction.NEW_WINDOW, NewWindowAction())
|
||||
addAction(FindEverywhereAction.FIND_EVERYWHERE, FindEverywhereAction())
|
||||
addAction(QuickConnectAction.QUICK_CONNECT, QuickConnectAction.instance)
|
||||
|
||||
addAction(Actions.APP_UPDATE, AppUpdateAction.getInstance())
|
||||
addAction(Actions.KEYWORD_HIGHLIGHT, KeywordHighlightAction())
|
||||
addAction(Actions.TERMINAL_LOGGER, TerminalLoggerAction())
|
||||
addAction(Actions.SFTP, TransferAnAction())
|
||||
addAction(SFTPCommandAction.SFTP_COMMAND, SFTPCommandAction())
|
||||
addAction(MultipleAction.MULTIPLE, MultipleAction.getInstance())
|
||||
addAction(SnippetAction.SNIPPET, SnippetAction.getInstance())
|
||||
addAction(Actions.MACRO, MacroAction())
|
||||
addAction(Actions.KEY_MANAGER, KeyManagerAction())
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.TerminalPanelFactory
|
||||
import app.termora.WindowScope
|
||||
import app.termora.*
|
||||
|
||||
class MultipleAction private constructor() : AnAction(
|
||||
I18n.getString("termora.tools.multiple"),
|
||||
Icons.vcs
|
||||
) {
|
||||
), StateAction {
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -17,8 +14,8 @@ class MultipleAction private constructor() : AnAction(
|
||||
*/
|
||||
const val MULTIPLE = "MultipleAction"
|
||||
|
||||
fun getInstance(windowScope: WindowScope): MultipleAction {
|
||||
return windowScope.getOrCreate(MultipleAction::class) { MultipleAction() }
|
||||
fun getInstance(): MultipleAction {
|
||||
return ApplicationScope.forApplicationScope().getOrCreate(MultipleAction::class) { MultipleAction() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +24,24 @@ class MultipleAction private constructor() : AnAction(
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
super.setSelected(false)
|
||||
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
|
||||
setSelected(windowScope, isSelected(windowScope).not())
|
||||
TerminalPanelFactory.getInstance().repaintAll()
|
||||
}
|
||||
|
||||
override fun isSelected(): Boolean {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun isSelected(windowScope: WindowScope): Boolean {
|
||||
return windowScope.getBoolean("MultipleAction.isSelected", false)
|
||||
}
|
||||
|
||||
override fun setSelected(windowScope: WindowScope, selected: Boolean) {
|
||||
windowScope.putBoolean("MultipleAction.isSelected", selected)
|
||||
putValue("MultipleAction.isSelected", selected)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -38,7 +38,7 @@ class OpenHostAction : AnAction() {
|
||||
if (providers.first { StringUtils.equalsIgnoreCase(it.getProtocol(), host.protocol) }
|
||||
.isTransfer()) {
|
||||
ActionManager.getInstance().getAction(Actions.SFTP)
|
||||
.actionPerformed(TransferActionEvent(evt.source, evt.host.id, evt.event))
|
||||
.actionPerformed(TransferActionEvent(evt.source, host, evt.event))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
178
src/main/kotlin/app/termora/actions/QuickConnectAction.kt
Normal file
178
src/main/kotlin/app/termora/actions/QuickConnectAction.kt
Normal file
@@ -0,0 +1,178 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.OptionsPane.Companion.FORM_MARGIN
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import javax.swing.*
|
||||
|
||||
class QuickConnectAction private constructor() : AnAction(I18n.getString("termora.actions.quick-connect"), Icons.find) {
|
||||
companion object {
|
||||
const val QUICK_CONNECT = "QuickConnectAction"
|
||||
val instance = QuickConnectAction()
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
putValue(SHORT_DESCRIPTION, I18n.getString("termora.actions.quick-connect"))
|
||||
}
|
||||
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val scope = evt.getData(DataProviders.WindowScope) ?: return
|
||||
val dialog = QuickConnectDialog(scope.window)
|
||||
dialog.isVisible = true
|
||||
}
|
||||
|
||||
private class QuickConnectDialog(owner: Window) : DialogWrapper(owner) {
|
||||
private val properties get() = DatabaseManager.getInstance().properties
|
||||
private val hostComboBox = OutlineComboBox<String>()
|
||||
private val usernameTextField = OutlineTextField(256)
|
||||
private val passwordTextField = OutlinePasswordField(256)
|
||||
|
||||
init {
|
||||
isModal = true
|
||||
title = I18n.getString("termora.actions.quick-connect")
|
||||
isResizable = false
|
||||
init()
|
||||
pack()
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 250, preferredSize.height)
|
||||
setLocationRelativeTo(owner)
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
hostComboBox.isEditable = true
|
||||
hostComboBox.placeholderText = "ssh://127.0.0.1:22"
|
||||
|
||||
val histories = getHistories()
|
||||
for (history in histories) {
|
||||
if (histories.first() == history) {
|
||||
usernameTextField.text = history.host.username
|
||||
passwordTextField.text = history.host.authentication.password
|
||||
}
|
||||
hostComboBox.addItem(history.url)
|
||||
}
|
||||
|
||||
usernameTextField.placeholderText = I18n.getString("termora.new-host.general.username")
|
||||
passwordTextField.placeholderText = I18n.getString("termora.new-host.general.password")
|
||||
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow",
|
||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
|
||||
return FormBuilder.create().layout(layout)
|
||||
.border(BorderFactory.createEmptyBorder(0, 8, 8, 8))
|
||||
.add("${I18n.getString("termora.new-host.general.protocol")}:").xy(1, 1)
|
||||
.add(hostComboBox).xy(3, 1)
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, 3)
|
||||
.add(usernameTextField).xy(3, 3)
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, 5)
|
||||
.add(passwordTextField).xy(3, 5)
|
||||
.build()
|
||||
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
val host = hostComboBox.selectedItem as? String
|
||||
if (host.isNullOrBlank()) {
|
||||
hostComboBox.requestFocusInWindow()
|
||||
return
|
||||
}
|
||||
|
||||
val historyHost: HistoryHost
|
||||
try {
|
||||
historyHost = getHistoryHost(host.trim())
|
||||
} catch (e: Exception) {
|
||||
hostComboBox.requestFocusInWindow()
|
||||
OptionPane.showMessageDialog(
|
||||
this,
|
||||
e.message ?: ExceptionUtils.getRootCauseMessage(e),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val action = ActionManager.getInstance().getAction(OpenHostAction.OPEN_HOST)
|
||||
if (action is OpenHostAction) {
|
||||
SwingUtilities.invokeLater {
|
||||
action.actionPerformed(OpenHostActionEvent(this, historyHost.host, EventObject(this)))
|
||||
}
|
||||
}
|
||||
super.doOKAction()
|
||||
|
||||
|
||||
}
|
||||
|
||||
override fun createOkAction(): AbstractAction {
|
||||
return OkAction(I18n.getString("termora.welcome.contextmenu.connect"))
|
||||
}
|
||||
|
||||
private fun getHistoryHost(host: String): HistoryHost {
|
||||
|
||||
|
||||
val uri = URI.create(host)
|
||||
val protocolProvider = ProtocolProvider.valueOf(uri.scheme)
|
||||
if (protocolProvider == null) {
|
||||
throw UnsupportedOperationException(I18n.getString("termora.protocol.not-supported", uri.scheme))
|
||||
}
|
||||
|
||||
val historyHost = HistoryHost(
|
||||
host, Host(
|
||||
name = uri.host,
|
||||
protocol = uri.scheme,
|
||||
host = uri.host,
|
||||
port = uri.port,
|
||||
username = usernameTextField.text.trim(),
|
||||
authentication = Authentication.No.copy(
|
||||
type = AuthenticationType.Password,
|
||||
password = String(passwordTextField.password)
|
||||
),
|
||||
options = Options.Default.copy(
|
||||
extras = mutableMapOf("Temporary" to "true")
|
||||
)
|
||||
)
|
||||
)
|
||||
val histories = getHistories().toMutableList()
|
||||
histories.removeIf { it.url == host }
|
||||
histories.addFirst(historyHost)
|
||||
|
||||
if (histories.size > 20) {
|
||||
histories.removeLast()
|
||||
}
|
||||
|
||||
properties.putString("QuickConnect.historyHosts", ohMyJson.encodeToString(histories))
|
||||
|
||||
return historyHost
|
||||
}
|
||||
|
||||
private fun getHistories(): List<HistoryHost> {
|
||||
val text = properties.getString("QuickConnect.historyHosts", "[]")
|
||||
return ohMyJson.runCatching { ohMyJson.decodeFromString<List<HistoryHost>>(text) }
|
||||
.getOrNull() ?: emptyList()
|
||||
}
|
||||
|
||||
override fun addNotify() {
|
||||
super.addNotify()
|
||||
controlsVisible = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class HistoryHost(
|
||||
val url: String,
|
||||
val host: Host,
|
||||
)
|
||||
|
||||
}
|
||||
8
src/main/kotlin/app/termora/actions/StateAction.kt
Normal file
8
src/main/kotlin/app/termora/actions/StateAction.kt
Normal file
@@ -0,0 +1,8 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.WindowScope
|
||||
|
||||
interface StateAction {
|
||||
fun isSelected(windowScope: WindowScope): Boolean
|
||||
fun setSelected(windowScope: WindowScope, selected: Boolean)
|
||||
}
|
||||
@@ -504,6 +504,8 @@ class DatabaseManager private constructor() : Disposable {
|
||||
|
||||
protected open fun putString(key: String, value: String) {
|
||||
databaseManager.setSetting("${name}.$key", value)
|
||||
// 触发变动
|
||||
DatabasePropertiesChangedExtension.onPropertyChanged(name, key, value)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package app.termora.database
|
||||
|
||||
import app.termora.database.DatabaseManager.Companion.log
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionManager
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
internal interface DatabasePropertiesChangedExtension : Extension {
|
||||
|
||||
companion object {
|
||||
fun onPropertyChanged(name: String, key: String, value: String) {
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
for (extension in ExtensionManager.getInstance()
|
||||
.getExtensions(DatabasePropertiesChangedExtension::class.java)) {
|
||||
try {
|
||||
extension.onPropertyChanged(name, key, value)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SwingUtilities.invokeLater { onPropertyChanged(name, key, value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 属性数据变动
|
||||
*
|
||||
* @param name 属性名
|
||||
* @param key key
|
||||
*/
|
||||
fun onPropertyChanged(name: String, key: String, value: String)
|
||||
|
||||
}
|
||||
@@ -22,9 +22,7 @@ class QuickActionsFindEverywhereProvider(private val windowScope: WindowScope) :
|
||||
for (action in actions) {
|
||||
val ac = actionManager.getAction(action)
|
||||
if (ac == null) {
|
||||
if (action == MultipleAction.MULTIPLE) {
|
||||
results.add(ActionFindEverywhereResult(MultipleAction.getInstance(windowScope)))
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
results.add(ActionFindEverywhereResult(ac))
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import app.termora.Icons
|
||||
import app.termora.Scope
|
||||
import app.termora.actions.NewHostAction
|
||||
import app.termora.actions.OpenLocalTerminalAction
|
||||
import app.termora.actions.QuickConnectAction
|
||||
import app.termora.snippet.SnippetAction
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
@@ -19,19 +20,13 @@ class QuickCommandFindEverywhereProvider : FindEverywhereProvider {
|
||||
actionManager.let { list.add(CreateHostFindEverywhereResult()) }
|
||||
|
||||
// Local terminal
|
||||
actionManager.getAction(OpenLocalTerminalAction.LOCAL_TERMINAL)?.let {
|
||||
list.add(ActionFindEverywhereResult(it))
|
||||
}
|
||||
|
||||
actionManager.getAction(OpenLocalTerminalAction.LOCAL_TERMINAL)?.let { list.add(ActionFindEverywhereResult(it)) }
|
||||
// Snippet
|
||||
actionManager.getAction(SnippetAction.SNIPPET)?.let {
|
||||
list.add(ActionFindEverywhereResult(it))
|
||||
}
|
||||
|
||||
actionManager.getAction(SnippetAction.SNIPPET)?.let { list.add(ActionFindEverywhereResult(it)) }
|
||||
// SFTP
|
||||
actionManager.getAction(Actions.SFTP)?.let {
|
||||
list.add(ActionFindEverywhereResult(it))
|
||||
}
|
||||
actionManager.getAction(Actions.SFTP)?.let { list.add(ActionFindEverywhereResult(it)) }
|
||||
// quick connect
|
||||
actionManager.getAction(QuickConnectAction.QUICK_CONNECT)?.let { list.add(ActionFindEverywhereResult(it)) }
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
package app.termora.keymap
|
||||
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.DialogWrapper
|
||||
import app.termora.Disposable
|
||||
import app.termora.SwingUtils
|
||||
import app.termora.account.AccountManager
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.database.Data
|
||||
import app.termora.database.DataType
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.database.OwnerType
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.action.ActionManager
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Container
|
||||
import java.awt.KeyEventDispatcher
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.KeyStroke
|
||||
|
||||
class KeymapManager private constructor() : Disposable {
|
||||
|
||||
@@ -34,17 +21,13 @@ class KeymapManager private constructor() : Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
private val keymapKeyEventDispatcher = KeymapKeyEventDispatcher()
|
||||
private val database get() = DatabaseManager.getInstance()
|
||||
private val properties get() = DatabaseManager.getInstance().properties
|
||||
private val keymaps = linkedMapOf<String, Keymap>()
|
||||
private val accountManager get() = AccountManager.getInstance()
|
||||
private val activeKeymap get() = properties.getString("Keymap.Active")
|
||||
private val keyboardFocusManager by lazy { KeyboardFocusManager.getCurrentKeyboardFocusManager() }
|
||||
|
||||
init {
|
||||
keyboardFocusManager.addKeyEventDispatcher(keymapKeyEventDispatcher)
|
||||
|
||||
try {
|
||||
for (data in database.rawData(DataType.Keymap)) {
|
||||
try {
|
||||
@@ -63,13 +46,8 @@ class KeymapManager private constructor() : Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
MacOSKeymap.getInstance().let {
|
||||
keymaps[it.name] = it
|
||||
}
|
||||
|
||||
WindowsKeymap.getInstance().let {
|
||||
keymaps[it.name] = it
|
||||
}
|
||||
MacOSKeymap.getInstance().let { keymaps[it.name] = it }
|
||||
WindowsKeymap.getInstance().let { keymaps[it.name] = it }
|
||||
|
||||
}
|
||||
|
||||
@@ -102,7 +80,7 @@ class KeymapManager private constructor() : Disposable {
|
||||
keymaps.putFirst(keymap.name, keymap)
|
||||
val accountId = accountManager.getAccountId()
|
||||
|
||||
database.save(
|
||||
database.saveAndIncrementVersion(
|
||||
Data(
|
||||
id = keymap.id,
|
||||
ownerId = accountId,
|
||||
@@ -122,84 +100,4 @@ class KeymapManager private constructor() : Disposable {
|
||||
database.delete(id, DataType.Keymap.name)
|
||||
}
|
||||
|
||||
private inner class KeymapKeyEventDispatcher : KeyEventDispatcher {
|
||||
|
||||
override fun dispatchKeyEvent(e: KeyEvent): Boolean {
|
||||
if (e.isConsumed || e.id != KeyEvent.KEY_PRESSED || e.modifiersEx == 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
val keyStroke = KeyStroke.getKeyStrokeForEvent(e)
|
||||
val component = e.source
|
||||
|
||||
if (component is JComponent) {
|
||||
// 如果这个键已经被组件注册了,那么忽略
|
||||
if (getConditionForKeyStroke(component, keyStroke) != JComponent.UNDEFINED_CONDITION) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val shortcuts = getActiveKeymap()
|
||||
val actionIds = shortcuts.getActionIds(KeyShortcut(keyStroke))
|
||||
if (actionIds.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val focusedWindow = keyboardFocusManager.focusedWindow
|
||||
if (focusedWindow is DialogWrapper) {
|
||||
if (!focusedWindow.processGlobalKeymap) {
|
||||
return false
|
||||
}
|
||||
} else if (focusedWindow is JDialog) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果当前有 Popup ,那么不派发事件
|
||||
val c = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
|
||||
if (c is Container) {
|
||||
val popups: List<JPopupMenu> = SwingUtils.getDescendantsOfType(
|
||||
JPopupMenu::class.java,
|
||||
c, true
|
||||
)
|
||||
if (popups.isNotEmpty()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val evt = AnActionEvent(e.source, StringUtils.EMPTY, e)
|
||||
for (actionId in actionIds) {
|
||||
val action = ActionManager.getInstance().getAction(actionId) ?: continue
|
||||
if (!action.isEnabled) {
|
||||
continue
|
||||
}
|
||||
action.actionPerformed(evt)
|
||||
if (evt.isConsumed) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun getConditionForKeyStroke(c: JComponent, keyStroke: KeyStroke): Int {
|
||||
val condition = c.getConditionForKeyStroke(keyStroke)
|
||||
|
||||
// 如果这个键已经被组件注册了,那么忽略
|
||||
if (condition != JComponent.UNDEFINED_CONDITION) {
|
||||
return condition
|
||||
}
|
||||
|
||||
if (c.parent is JComponent) {
|
||||
return getConditionForKeyStroke(c.parent as JComponent, keyStroke)
|
||||
}
|
||||
|
||||
return JComponent.UNDEFINED_CONDITION
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
keyboardFocusManager.removeKeyEventDispatcher(keymapKeyEventDispatcher)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import app.termora.plugin.internal.extension.DynamicExtensionPlugin
|
||||
import app.termora.plugin.internal.local.LocalInternalPlugin
|
||||
import app.termora.plugin.internal.plugin.PluginInternalPlugin
|
||||
import app.termora.plugin.internal.rdp.RDPInternalPlugin
|
||||
import app.termora.plugin.internal.serial.SerialInternalPlugin
|
||||
import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin
|
||||
import app.termora.plugin.internal.ssh.SSHInternalPlugin
|
||||
import app.termora.plugin.internal.telnet.TelnetInternalPlugin
|
||||
@@ -118,8 +117,6 @@ internal class PluginManager private constructor() {
|
||||
plugins.add(PluginDescriptor(RDPInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||
// telnet plugin
|
||||
plugins.add(PluginDescriptor(TelnetInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||
// serial plugin
|
||||
plugins.add(PluginDescriptor(SerialInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||
// wsl plugin
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
plugins.add(PluginDescriptor(WSLInternalPlugin(), origin = PluginOrigin.Internal, version = version))
|
||||
|
||||
@@ -10,7 +10,7 @@ import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import javax.swing.*
|
||||
|
||||
internal class BasicGeneralOption : JPanel(BorderLayout()), OptionsPane.Option {
|
||||
class BasicGeneralOption : JPanel(BorderLayout()), OptionsPane.Option {
|
||||
val nameTextField = OutlineTextField(128)
|
||||
val remarkTextArea = FixedLengthTextArea(512)
|
||||
private val formMargin = "7dlu"
|
||||
|
||||
@@ -78,8 +78,6 @@ class BasicProxyOption(
|
||||
proxyAuthenticationTypeComboBox.addItem(type)
|
||||
}
|
||||
|
||||
proxyUsernameTextField.text = "root"
|
||||
|
||||
refreshStates()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
package app.termora.plugin.internal.badge
|
||||
|
||||
import app.termora.WindowScope
|
||||
import app.termora.*
|
||||
import java.awt.Color
|
||||
import java.util.*
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.UIManager
|
||||
|
||||
class Badge private constructor() {
|
||||
class Badge private constructor(scope: Scope) {
|
||||
companion object {
|
||||
fun getInstance(scope: WindowScope): Badge {
|
||||
return scope.getOrCreate(Badge::class) { Badge() }
|
||||
return scope.getOrCreate(Badge::class) { Badge(scope) }
|
||||
}
|
||||
|
||||
fun getInstance(): Badge {
|
||||
val scope = ApplicationScope.forApplicationScope()
|
||||
return scope.getOrCreate(Badge::class) { Badge(scope) }
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
Disposer.register(scope, object : Disposable {
|
||||
override fun dispose() {
|
||||
map.clear()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private val map = WeakHashMap<JComponent, BadgePresentation>()
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.formdev.flatlaf.FlatLaf
|
||||
import com.formdev.flatlaf.util.UIScale
|
||||
import com.github.weisj.jsvg.SVGDocument
|
||||
import com.github.weisj.jsvg.parser.SVGLoader
|
||||
import com.github.weisj.jsvg.parser.impl.MutableLoaderContext
|
||||
import java.awt.Component
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
@@ -21,8 +22,8 @@ class PluginSVGIcon(input: InputStream, dark: InputStream? = null) : Icon {
|
||||
|
||||
}
|
||||
|
||||
private val document = svgLoader.load(input)
|
||||
private val darkDocument = dark?.let { svgLoader.load(it) }
|
||||
private val document = svgLoader.load(input, null, MutableLoaderContext.createDefault())
|
||||
private val darkDocument = dark?.let { svgLoader.load(it, null, MutableLoaderContext.createDefault()) }
|
||||
|
||||
override fun getIconHeight(): Int {
|
||||
return 32
|
||||
|
||||
@@ -40,5 +40,8 @@ class CloneSessionTerminalTabbedContextMenuExtension private constructor() : Ter
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun ordered(): Long {
|
||||
return 0
|
||||
}
|
||||
|
||||
}
|
||||
@@ -250,7 +250,8 @@ object SshClients {
|
||||
|
||||
val session = client.connect(entry).verify(timeout).session
|
||||
if (host.authentication.type == AuthenticationType.Password) {
|
||||
session.addPasswordIdentity(host.authentication.password)
|
||||
if (StringUtils.isNotBlank(host.authentication.password))
|
||||
session.addPasswordIdentity(host.authentication.password)
|
||||
} else if (host.authentication.type == AuthenticationType.PublicKey) {
|
||||
session.keyIdentityProvider = OhKeyPairKeyPairProvider(host.authentication.password)
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@ class CommandTransfer(
|
||||
isDirectory: Boolean,
|
||||
private val size: Long,
|
||||
val command: String,
|
||||
) : AbstractTransfer(parentId, path, path, isDirectory) {
|
||||
) : AbstractTransfer(parentId, path, path, isDirectory), TransferIndeterminate {
|
||||
|
||||
private var executed = false
|
||||
|
||||
override suspend fun transfer(bufferSize: Int): Long {
|
||||
if (executed) return 0
|
||||
val fs = source().fileSystem as SftpFileSystem
|
||||
fs.session.executeRemoteCommand(command)
|
||||
fs.clientSession.executeRemoteCommand(command)
|
||||
executed = true
|
||||
return this.size()
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import kotlin.io.path.name
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.math.max
|
||||
|
||||
class DefaultInternalTransferManager(
|
||||
internal class DefaultInternalTransferManager(
|
||||
private val owner: Supplier<Window>,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val transferManager: TransferManager,
|
||||
|
||||
@@ -4,7 +4,7 @@ import app.termora.Disposable
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
interface InternalTransferManager {
|
||||
internal interface InternalTransferManager {
|
||||
enum class TransferMode {
|
||||
Delete,
|
||||
Transfer,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package app.termora.transfer
|
||||
|
||||
import app.termora.Host
|
||||
import app.termora.actions.AnActionEvent
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.util.*
|
||||
|
||||
class TransferActionEvent(
|
||||
source: Any,
|
||||
val hostId: String,
|
||||
val host: Host? = null,
|
||||
event: EventObject
|
||||
) : AnActionEvent(source, StringUtils.EMPTY, event)
|
||||
@@ -1,6 +1,5 @@
|
||||
package app.termora.transfer
|
||||
|
||||
import app.termora.HostManager
|
||||
import app.termora.HostTerminalTab
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
@@ -8,10 +7,8 @@ import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.actions.DataProviders
|
||||
import app.termora.protocol.TransferProtocolProvider
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
class TransferAnAction : AnAction(I18n.getString("termora.transport.sftp"), Icons.folder) {
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||
|
||||
@@ -29,35 +26,32 @@ class TransferAnAction : AnAction(I18n.getString("termora.transport.sftp"), Icon
|
||||
terminalTabbedManager.addTerminalTab(sftpTab, false)
|
||||
}
|
||||
|
||||
var hostId = if (evt is TransferActionEvent) evt.hostId else StringUtils.EMPTY
|
||||
var host = if (evt is TransferActionEvent) evt.host else null
|
||||
|
||||
// 如果不是特定事件,那么尝试获取选中的Tab,如果是一个 Host 并且是 SSH 协议那么直接打开
|
||||
if (hostId.isBlank()) {
|
||||
if (host == null) {
|
||||
val tab = terminalTabbedManager.getSelectedTerminalTab()
|
||||
// 如果当前选中的是 Host 主机
|
||||
if (tab is HostTerminalTab) {
|
||||
if (TransferProtocolProvider.valueOf(tab.host.protocol) != null) {
|
||||
hostId = tab.host.id
|
||||
host = tab.host
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
terminalTabbedManager.setSelectedTerminalTab(sftpTab)
|
||||
|
||||
if (hostId.isBlank()) return
|
||||
|
||||
val tabbed = sftpTab.rightTabbed
|
||||
|
||||
// 如果已经打开了 那么直接选中
|
||||
for (i in 0 until tabbed.tabCount) {
|
||||
val panel = tabbed.getTransportPanel(i) ?: continue
|
||||
if (panel.host.id == hostId) {
|
||||
tabbed.selectedIndex = i
|
||||
return
|
||||
if (host != null) {
|
||||
for (i in 0 until tabbed.tabCount) {
|
||||
val panel = tabbed.getTransportPanel(i) ?: continue
|
||||
if (panel.host.id == host.id) {
|
||||
tabbed.selectedIndex = i
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val host = hostManager.getHost(hostId) ?: return
|
||||
var selectionPane: TransportSelectionPanel? = null
|
||||
|
||||
for (i in 0 until tabbed.tabCount) {
|
||||
val c = tabbed.getComponentAt(i)
|
||||
if (c is TransportSelectionPanel) {
|
||||
@@ -72,8 +66,10 @@ class TransferAnAction : AnAction(I18n.getString("termora.transport.sftp"), Icon
|
||||
selectionPane = tabbed.addSelectionTab()
|
||||
}
|
||||
|
||||
selectionPane.connect(host)
|
||||
|
||||
if (host != null) {
|
||||
selectionPane.connect(host)
|
||||
}
|
||||
|
||||
terminalTabbedManager.setSelectedTerminalTab(sftpTab)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package app.termora.transfer
|
||||
|
||||
interface TransferIndeterminate
|
||||
@@ -30,4 +30,8 @@ interface TransferManager {
|
||||
*/
|
||||
fun addTransferListener(listener: TransferListener): Disposable
|
||||
|
||||
/**
|
||||
* 移除传输监听器
|
||||
*/
|
||||
fun removeTransferListener(listener: TransferListener)
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package app.termora.transfer
|
||||
|
||||
import app.termora.Disposable
|
||||
import app.termora.I18n
|
||||
import app.termora.NativeIcons
|
||||
import app.termora.OptionPane
|
||||
import app.termora.*
|
||||
import app.termora.transfer.TransferTreeTableNode.State
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.util.SoftCache
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
@@ -15,6 +14,7 @@ import java.awt.Component
|
||||
import java.awt.Graphics
|
||||
import java.awt.Insets
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.ActionListener
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@@ -23,6 +23,7 @@ import javax.swing.table.DefaultTableCellRenderer
|
||||
import javax.swing.tree.DefaultTreeCellRenderer
|
||||
import kotlin.io.path.name
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.min
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
@@ -85,6 +86,7 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
|
||||
columnModel.getColumn(TransferTableModel.COLUMN_SPEED).cellRenderer = centerTableCellRenderer
|
||||
columnModel.getColumn(TransferTableModel.COLUMN_ESTIMATED_TIME).cellRenderer = centerTableCellRenderer
|
||||
columnModel.getColumn(TransferTableModel.COLUMN_PROGRESS).cellRenderer = ProgressTableCellRenderer()
|
||||
.apply { Disposer.register(table, this) }
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
@@ -169,10 +171,16 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
|
||||
disposed.set(true)
|
||||
}
|
||||
|
||||
private inner class ProgressTableCellRenderer : DefaultTableCellRenderer() {
|
||||
data class Indeterminate(val progress: Int = 0)
|
||||
|
||||
private inner class ProgressTableCellRenderer : DefaultTableCellRenderer(), Disposable, ActionListener {
|
||||
private var progress = 0.0
|
||||
private var progressInt = 0
|
||||
private val padding = 4
|
||||
private val map = SoftCache<Any, Indeterminate>()
|
||||
private val timer = Timer(1000 / 40, this).apply { start() }
|
||||
private var value: Any? = null
|
||||
private val block = 36
|
||||
|
||||
init {
|
||||
horizontalAlignment = CENTER
|
||||
@@ -189,9 +197,19 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
|
||||
|
||||
this.progress = 0.0
|
||||
this.progressInt = 0
|
||||
this.value = value
|
||||
|
||||
if (value is TransferTreeTableNode) {
|
||||
if (value.state() == TransferTreeTableNode.State.Processing || value.waitingChildrenCompleted() || value.transfer is DeleteTransfer) {
|
||||
|
||||
if (value.transfer is TransferIndeterminate) {
|
||||
if (map.containsKey(value).not()) {
|
||||
map[value] = Indeterminate()
|
||||
}
|
||||
} else if (map.containsKey(value)) {
|
||||
map.remove(value)
|
||||
}
|
||||
|
||||
if (value.state() == State.Processing || value.waitingChildrenCompleted() || value.transfer is DeleteTransfer) {
|
||||
this.progress = value.transferred.get() * 1.0 / value.filesize.get()
|
||||
this.progressInt = floor(progress * 100.0).toInt()
|
||||
// 因为有一些 0B 大小的文件,所以如果在进行中,那么最大就是99
|
||||
@@ -200,6 +218,7 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
|
||||
this.progressInt = floor(progress * 100.0).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return super.getTableCellRendererComponent(
|
||||
@@ -213,6 +232,9 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
val width = width
|
||||
val height = height
|
||||
|
||||
// 原始背景
|
||||
g.color = background
|
||||
g.fillRect(0, 0, width, height)
|
||||
@@ -221,6 +243,25 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
|
||||
g.color = UIManager.getColor("Table.selectionInactiveBackground")
|
||||
g.fillRect(0, padding, width, height - padding * 2)
|
||||
|
||||
if (map.containsKey(value)) {
|
||||
val state = getState(value)
|
||||
if (state == State.Processing || state == State.Failed) {
|
||||
val indeterminate = map.getValue(value)
|
||||
|
||||
g.color = if (state == State.Processing) UIManager.getColor("ProgressBar.foreground")
|
||||
else UIManager.getColor("Component.error.focusedBorderColor")
|
||||
|
||||
g.fillRect(indeterminate.progress, padding, block, height - padding * 2)
|
||||
if (indeterminate.progress + block > width) {
|
||||
val c = width - indeterminate.progress - block
|
||||
val x = -block - c
|
||||
g.fillRect(x, padding, block, height - padding * 2)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 进度条颜色
|
||||
g.color = UIManager.getColor("ProgressBar.foreground")
|
||||
g.fillRect(0, padding, (width * progress).toInt(), height - padding * 2)
|
||||
@@ -233,6 +274,31 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
|
||||
// 绘制文字
|
||||
ui.paint(g, this)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
timer.stop()
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
for (i in 0 until table.rowCount) {
|
||||
val row = table.getPathForRow(i).lastPathComponent ?: continue
|
||||
val node = tableModel.getValueAt(row, TransferTableModel.COLUMN_PROGRESS)
|
||||
if (node !is TransferTreeTableNode) continue
|
||||
if (node.state() != State.Processing) continue
|
||||
val c = map[node] ?: continue
|
||||
val rect = table.getCellRect(i, TransferTableModel.COLUMN_PROGRESS, false)
|
||||
val indeterminate = c.copy(progress = min(c.progress + block / 10, rect.width))
|
||||
map[node] = if (indeterminate.progress == rect.width) Indeterminate() else indeterminate
|
||||
table.repaint(rect)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getState(value: Any?): State? {
|
||||
if (value == null) return null
|
||||
val c = tableModel.getValueAt(value, TransferTableModel.COLUMN_PROGRESS)
|
||||
if (c !is TransferTreeTableNode) return null
|
||||
return c.state()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import okio.withLock
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode
|
||||
import org.jdesktop.swingx.treetable.DefaultTreeTableModel
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -87,11 +88,15 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) :
|
||||
eventListener.add(TransferListener::class.java, listener)
|
||||
return object : Disposable {
|
||||
override fun dispose() {
|
||||
eventListener.remove(TransferListener::class.java, listener)
|
||||
removeTransferListener(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeTransferListener(listener: TransferListener) {
|
||||
eventListener.remove(TransferListener::class.java, listener)
|
||||
}
|
||||
|
||||
override fun addTransfer(transfer: Transfer): Boolean {
|
||||
val node = TransferTreeTableNode(transfer)
|
||||
val parent = if (transfer.parentId().isBlank()) getRoot() else map[transfer.parentId()] ?: return false
|
||||
@@ -496,6 +501,10 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) :
|
||||
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
removeTransfer(StringUtils.EMPTY)
|
||||
}
|
||||
|
||||
private class UserCanceledException : RuntimeException()
|
||||
|
||||
|
||||
|
||||
@@ -61,15 +61,18 @@ class TransferTreeTableNode(transfer: Transfer) : DefaultMutableTreeTableNode(tr
|
||||
(transfer is DeleteTransfer && transfer.isDirectory() && (state() == State.Processing || state() == State.Ready))
|
||||
val speed = counter.getLastSecondBytes()
|
||||
val estimatedTime = max(if (isProcessing && speed > 0) (filesize - totalBytesTransferred) / speed else 0, 0)
|
||||
val indeterminate = transfer is TransferIndeterminate
|
||||
val formatSize = "${formatBytes(totalBytesTransferred)} / ${formatBytes(filesize)}"
|
||||
val formatEstimatedTime = if (indeterminate) "-" else if (isProcessing) formatSeconds(estimatedTime) else "-"
|
||||
|
||||
return when (column) {
|
||||
TransferTableModel.COLUMN_NAME -> transfer.source().name
|
||||
TransferTableModel.COLUMN_STATUS -> formatStatus(state)
|
||||
TransferTableModel.COLUMN_SOURCE_PATH -> formatPath(transfer.source(), false)
|
||||
TransferTableModel.COLUMN_TARGET_PATH -> formatPath(transfer.target(), true)
|
||||
TransferTableModel.COLUMN_SIZE -> "${formatBytes(totalBytesTransferred)} / ${formatBytes(filesize)}"
|
||||
TransferTableModel.COLUMN_SPEED -> if (isProcessing) "${formatBytes(speed)}/s" else "-"
|
||||
TransferTableModel.COLUMN_ESTIMATED_TIME -> if (isProcessing) formatSeconds(estimatedTime) else "-"
|
||||
TransferTableModel.COLUMN_SIZE -> if (indeterminate) "-" else formatSize
|
||||
TransferTableModel.COLUMN_SPEED -> if (indeterminate) "-" else if (isProcessing) "${formatBytes(speed)}/s" else "-"
|
||||
TransferTableModel.COLUMN_ESTIMATED_TIME -> formatEstimatedTime
|
||||
TransferTableModel.COLUMN_PROGRESS -> this
|
||||
else -> StringUtils.EMPTY
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package app.termora.transfer
|
||||
|
||||
import app.termora.WindowScope
|
||||
import app.termora.plugin.Extension
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Path
|
||||
import javax.swing.JMenuItem
|
||||
|
||||
internal interface TransportContextMenuExtension : Extension {
|
||||
|
||||
/**
|
||||
* 抛出 [UnsupportedOperationException] 表示不支持
|
||||
*
|
||||
* @param fileSystem 为 null 表示可能已经断线,处于不可用状态
|
||||
*/
|
||||
fun createJMenuItem(
|
||||
windowScope: WindowScope,
|
||||
fileSystem: FileSystem?,
|
||||
popupMenu: TransportPopupMenu,
|
||||
files: List<Pair<Path, TransportTableModel.Attributes>>
|
||||
): JMenuItem
|
||||
}
|
||||
@@ -3,10 +3,13 @@ package app.termora.transfer
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviderSupport
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.plugin.ExtensionManager
|
||||
import app.termora.plugin.internal.wsl.WSLHostTerminalTab
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.transfer.TransportTableModel.Attributes
|
||||
import app.termora.transfer.s3.S3FileAttributes
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
import com.formdev.flatlaf.icons.FlatTreeClosedIcon
|
||||
@@ -46,6 +49,7 @@ import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.stream.Stream
|
||||
import javax.swing.*
|
||||
import javax.swing.TransferHandler
|
||||
@@ -60,11 +64,13 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
internal class TransportPanel(
|
||||
private val transferManager: InternalTransferManager,
|
||||
private val internalTransferManager: InternalTransferManager,
|
||||
val host: Host,
|
||||
val loader: TransportSupportLoader,
|
||||
) : JPanel(BorderLayout()), DataProvider, Disposable, TransportNavigator {
|
||||
companion object {
|
||||
val MyTransportPanel = DataKey(TransportPanel::class)
|
||||
|
||||
private val log = LoggerFactory.getLogger(TransportPanel::class.java)
|
||||
private val folderIcon = FlatTreeClosedIcon()
|
||||
private val fileIcon = FlatTreeLeafIcon()
|
||||
@@ -79,6 +85,7 @@ internal class TransportPanel(
|
||||
|
||||
}
|
||||
|
||||
private val mod = AtomicLong(0)
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||
private val lru = object : LinkedHashMap<String, Icon?>() {
|
||||
override fun removeEldestEntry(eldest: Map.Entry<String?, Icon?>?): Boolean {
|
||||
@@ -109,7 +116,7 @@ internal class TransportPanel(
|
||||
get() = enableManager.getFlag(showHiddenFilesKey, true)
|
||||
set(value) = enableManager.setFlag(showHiddenFilesKey, value)
|
||||
private val navigator get() = this
|
||||
private val nextReloadCallbacks = mutableListOf<() -> Unit>()
|
||||
private val nextReloadCallbacks = Collections.synchronizedMap(mutableMapOf<Long, MutableList<() -> Unit>>())
|
||||
private val history = linkedSetOf<Path>()
|
||||
private val undoManager = MyUndoManager()
|
||||
private val editTransferListener = EditTransferListener()
|
||||
@@ -117,7 +124,7 @@ internal class TransportPanel(
|
||||
|
||||
private val disposed = AtomicBoolean(false)
|
||||
private val futures = Collections.synchronizedSet(mutableSetOf<Future<*>>())
|
||||
|
||||
private val support = DataProviderSupport()
|
||||
|
||||
/**
|
||||
* 工作目录
|
||||
@@ -212,6 +219,8 @@ internal class TransportPanel(
|
||||
|
||||
add(toolbar, BorderLayout.NORTH)
|
||||
add(layeredPane, BorderLayout.CENTER)
|
||||
|
||||
support.addData(MyTransportPanel, this)
|
||||
}
|
||||
|
||||
private fun compare(o1: Attributes, o2: Attributes): Int? {
|
||||
@@ -286,7 +295,7 @@ internal class TransportPanel(
|
||||
})
|
||||
|
||||
// 传输完成之后刷新
|
||||
transferManager.addTransferListener(object : TransferListener {
|
||||
internalTransferManager.addTransferListener(object : TransferListener {
|
||||
override fun onTransferChanged(transfer: Transfer, state: TransferTreeTableNode.State) {
|
||||
if (state != TransferTreeTableNode.State.Done && state != TransferTreeTableNode.State.Failed) return
|
||||
val target = transfer.target()
|
||||
@@ -294,13 +303,17 @@ internal class TransportPanel(
|
||||
if (target.fileSystem != loader.getSyncTransportSupport().getFileSystem()) return
|
||||
}
|
||||
if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) {
|
||||
reload(requestFocus = false)
|
||||
if (loading) {
|
||||
registerNextReloadCallback { reload(requestFocus = false) }
|
||||
} else {
|
||||
reload(requestFocus = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}).let { Disposer.register(this, it) }
|
||||
|
||||
// High 专门用于编辑目的,下载完成之后立即去编辑
|
||||
transferManager.addTransferListener(editTransferListener).let { Disposer.register(this, it) }
|
||||
internalTransferManager.addTransferListener(editTransferListener).let { Disposer.register(this, it) }
|
||||
|
||||
// parent button
|
||||
addPropertyChangeListener("loading") { evt ->
|
||||
@@ -427,8 +440,8 @@ internal class TransportPanel(
|
||||
enterSelectionFolder()
|
||||
} else {
|
||||
val paths = listOf(model.getPath(row) to attributes)
|
||||
if (loader.isOpened() && transferManager.canTransfer(paths.map { it.first })) {
|
||||
transferManager.addTransfer(paths, InternalTransferManager.TransferMode.Transfer)
|
||||
if (loader.isOpened() && internalTransferManager.canTransfer(paths.map { it.first })) {
|
||||
internalTransferManager.addTransfer(paths, InternalTransferManager.TransferMode.Transfer)
|
||||
}
|
||||
}
|
||||
} else if (SwingUtilities.isRightMouseButton(e)) {
|
||||
@@ -517,7 +530,7 @@ internal class TransportPanel(
|
||||
override fun importData(support: TransferSupport): Boolean {
|
||||
val data = getTransferData(support, true) ?: return false
|
||||
|
||||
val future = transferManager
|
||||
val future = internalTransferManager
|
||||
.addTransfer(data.files, data.workdir, InternalTransferManager.TransferMode.Transfer)
|
||||
|
||||
mountFuture(future)
|
||||
@@ -609,8 +622,8 @@ internal class TransportPanel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerSelectRow(name: String) {
|
||||
nextReloadCallbacks.add {
|
||||
fun registerSelectRow(name: String) {
|
||||
registerNextReloadCallback {
|
||||
for (i in 0 until model.rowCount) {
|
||||
if (model.getAttributes(i).name == name) {
|
||||
val c = sorter.convertRowIndexToView(i)
|
||||
@@ -623,6 +636,11 @@ internal class TransportPanel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerNextReloadCallback(block: () -> Unit) {
|
||||
nextReloadCallbacks.computeIfAbsent(mod.get()) { mutableListOf() }
|
||||
.add(block)
|
||||
}
|
||||
|
||||
fun reload(
|
||||
oldPath: String? = workdir?.absolutePathString(),
|
||||
newPath: String? = workdir?.absolutePathString(),
|
||||
@@ -633,6 +651,8 @@ internal class TransportPanel(
|
||||
if (loading) return false
|
||||
loading = true
|
||||
|
||||
val mod = mod.getAndAdd(1)
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
|
||||
@@ -640,7 +660,7 @@ internal class TransportPanel(
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
setNewWorkdir(workdir)
|
||||
nextReloadCallbacks.forEach { runCatching { it.invoke() } }
|
||||
nextReloadCallbacks[mod]?.forEach { runCatching { it.invoke() } }
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -655,7 +675,7 @@ internal class TransportPanel(
|
||||
} finally {
|
||||
withContext(Dispatchers.Swing) {
|
||||
loading = false
|
||||
nextReloadCallbacks.clear()
|
||||
nextReloadCallbacks.entries.removeIf { it.key <= mod }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -720,6 +740,13 @@ internal class TransportPanel(
|
||||
if (files.isNotEmpty())
|
||||
consume.invoke()
|
||||
|
||||
if (first.compareAndSet(false, true)) {
|
||||
withContext(Dispatchers.Swing) {
|
||||
model.clear()
|
||||
table.scrollRectToVisible(Rectangle())
|
||||
}
|
||||
}
|
||||
|
||||
if (requestFocus)
|
||||
coroutineScope.launch(Dispatchers.Swing) { table.requestFocusInWindow() }
|
||||
|
||||
@@ -766,7 +793,9 @@ internal class TransportPanel(
|
||||
.getOrNull()
|
||||
|
||||
val fileSize = basicAttributes?.size() ?: 0
|
||||
val permissions = posixFileAttribute?.permissions() ?: emptySet()
|
||||
val permissions = posixFileAttribute?.permissions()
|
||||
?: if (basicAttributes is S3FileAttributes) basicAttributes.permissions
|
||||
else emptySet()
|
||||
val owner = fileOwnerAttribute?.name ?: StringUtils.EMPTY
|
||||
val lastModifiedTime = basicAttributes?.lastModifiedTime()?.toMillis() ?: 0
|
||||
val isDirectory = basicAttributes?.isDirectory ?: false
|
||||
@@ -796,11 +825,14 @@ internal class TransportPanel(
|
||||
|
||||
private fun showContextmenu(rows: Array<Int>, e: MouseEvent) {
|
||||
val files = rows.map { model.getPath(it) to model.getAttributes(it) }
|
||||
val popupMenu = TransportPopupMenu(owner, model, transferManager, loader, files)
|
||||
val popupMenu = TransportPopupMenu(owner, model, internalTransferManager, loader, files)
|
||||
popupMenu.addActionListener(PopupMenuActionListener(files))
|
||||
popupMenu.show(table, e.x, e.y)
|
||||
}
|
||||
|
||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||
return support.getData(dataKey)
|
||||
}
|
||||
|
||||
override fun navigateTo(destination: String): Boolean {
|
||||
assertEventDispatchThread()
|
||||
@@ -808,7 +840,7 @@ internal class TransportPanel(
|
||||
if (loading) return false
|
||||
|
||||
if (loader.isOpened()) {
|
||||
if (workdir?.absolutePathString() == destination) return false
|
||||
if (workdir?.pathString == destination) return false
|
||||
}
|
||||
|
||||
return reload(newPath = destination)
|
||||
@@ -927,7 +959,7 @@ internal class TransportPanel(
|
||||
if (fs.isOpen.not()) continue
|
||||
|
||||
// 发送到服务器
|
||||
transferManager.addHighTransfer(localPath, fs.getPath(target.absolutePathString()))
|
||||
internalTransferManager.addHighTransfer(localPath, fs.getPath(target.absolutePathString()))
|
||||
oldMillis = millis
|
||||
}
|
||||
}
|
||||
@@ -1036,7 +1068,7 @@ internal class TransportPanel(
|
||||
val target = source.parent.resolve(e.source.toString())
|
||||
processPath(e.source.toString()) { source.moveTo(target) }
|
||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.Rmrf) {
|
||||
transferManager.addTransfer(files, InternalTransferManager.TransferMode.Rmrf)
|
||||
internalTransferManager.addTransfer(files, InternalTransferManager.TransferMode.Rmrf)
|
||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.Reconnect) {
|
||||
// reload now
|
||||
reload()
|
||||
@@ -1046,7 +1078,7 @@ internal class TransportPanel(
|
||||
processPath(path.name) {
|
||||
if (c.includeSubFolder) {
|
||||
val future = withContext(Dispatchers.Swing) {
|
||||
transferManager.addTransfer(
|
||||
internalTransferManager.addTransfer(
|
||||
listOf(path to files.first().second.copy(permissions = c.permissions)),
|
||||
InternalTransferManager.TransferMode.ChangePermission
|
||||
)
|
||||
@@ -1061,14 +1093,14 @@ internal class TransportPanel(
|
||||
}
|
||||
|
||||
private fun transfer(mode: InternalTransferManager.TransferMode) {
|
||||
val future = transferManager.addTransfer(files, mode)
|
||||
val future = internalTransferManager.addTransfer(files, mode)
|
||||
mountFuture(future)
|
||||
}
|
||||
|
||||
private fun edit() {
|
||||
for (path in files.map { it.first }) {
|
||||
val target = Application.createSubTemporaryDir().resolve(path.name)
|
||||
val transferId = transferManager.addHighTransfer(path, target)
|
||||
val transferId = internalTransferManager.addHighTransfer(path, target)
|
||||
editTransferListener.addListenTransfer(transferId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package app.termora.transfer
|
||||
|
||||
import app.termora.Application
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.OptionPane
|
||||
import app.termora.plugin.ExtensionManager
|
||||
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import org.apache.commons.io.IOUtils
|
||||
@@ -20,7 +21,6 @@ import java.util.*
|
||||
import javax.swing.JMenu
|
||||
import javax.swing.JMenuItem
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.event.EventListenerList
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.name
|
||||
@@ -42,7 +42,6 @@ internal class TransportPopupMenu(
|
||||
private val openInFinderMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.open-in-folder"))
|
||||
private val renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename"))
|
||||
private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete"))
|
||||
private val rmrfMenu = JMenuItem("rm -rf", Icons.warningIntroduction)
|
||||
|
||||
// @formatter:off
|
||||
private val changePermissionsMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.change-permissions"))
|
||||
@@ -52,6 +51,7 @@ internal class TransportPopupMenu(
|
||||
private val newFolderMenu = newMenu.add(I18n.getString("termora.transport.table.contextmenu.new.folder"))
|
||||
private val newFileMenu = newMenu.add(I18n.getString("termora.transport.table.contextmenu.new.file"))
|
||||
|
||||
private val extensionManager get() = ExtensionManager.getInstance()
|
||||
private val eventListeners = EventListenerList()
|
||||
private val mnemonics = mapOf(
|
||||
refreshMenu to KeyEvent.VK_R,
|
||||
@@ -89,13 +89,32 @@ internal class TransportPopupMenu(
|
||||
addSeparator()
|
||||
add(renameMenu)
|
||||
add(deleteMenu)
|
||||
if (fileSystem is SftpFileSystem) {
|
||||
add(rmrfMenu)
|
||||
}
|
||||
add(changePermissionsMenu)
|
||||
|
||||
val menus = mutableListOf<JMenuItem>()
|
||||
for (extension in extensionManager.getExtensions(TransportContextMenuExtension::class.java)) {
|
||||
try {
|
||||
val menu = extension.createJMenuItem(
|
||||
ApplicationScope.forWindowScope(owner),
|
||||
fileSystem,
|
||||
this,
|
||||
files
|
||||
)
|
||||
menus.add(menu)
|
||||
} catch (_: UnsupportedOperationException) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (menus.isNotEmpty()) {
|
||||
addSeparator()
|
||||
menus.forEach { add(it) }
|
||||
}
|
||||
|
||||
addSeparator()
|
||||
add(refreshMenu)
|
||||
addSeparator()
|
||||
|
||||
add(newMenu)
|
||||
|
||||
// 开发环境提供断线
|
||||
@@ -113,7 +132,6 @@ internal class TransportPopupMenu(
|
||||
&& files.all { it.second.isFile && it.second.isSymbolicLink.not() }
|
||||
renameMenu.isEnabled = hasParent.not() && files.size == 1
|
||||
deleteMenu.isEnabled = hasParent.not() && files.isNotEmpty()
|
||||
rmrfMenu.isEnabled = hasParent.not() && files.isNotEmpty()
|
||||
changePermissionsMenu.isVisible = hasParent.not() && fileSystem is SftpFileSystem && files.size == 1
|
||||
|
||||
for ((item, mnemonic) in mnemonics) {
|
||||
@@ -134,16 +152,7 @@ internal class TransportPopupMenu(
|
||||
fireActionPerformed(it, ActionCommand.Delete)
|
||||
}
|
||||
}
|
||||
rmrfMenu.addActionListener {
|
||||
if (OptionPane.showConfirmDialog(
|
||||
SwingUtilities.getWindowAncestor(this),
|
||||
I18n.getString("termora.transport.table.contextmenu.rm-warning"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
) == JOptionPane.YES_OPTION
|
||||
) {
|
||||
fireActionPerformed(it, ActionCommand.Rmrf)
|
||||
}
|
||||
}
|
||||
|
||||
renameMenu.addActionListener { newFolderOrNewFile(it, ActionCommand.Rename) }
|
||||
editMenu.addActionListener { fireActionPerformed(it, ActionCommand.Edit) }
|
||||
newFolderMenu.addActionListener { newFolderOrNewFile(it, ActionCommand.NewFolder) }
|
||||
@@ -159,7 +168,7 @@ internal class TransportPopupMenu(
|
||||
}
|
||||
}
|
||||
|
||||
private fun fireActionPerformed(evt: ActionEvent, command: ActionCommand) {
|
||||
fun fireActionPerformed(evt: ActionEvent, command: ActionCommand) {
|
||||
for (listener in eventListeners.getListeners(ActionListener::class.java)) {
|
||||
listener.actionPerformed(ActionEvent(evt.source, evt.id, command.name))
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ internal class TransportTabbed(
|
||||
}
|
||||
})
|
||||
|
||||
edit.isEnabled = clone.isEnabled
|
||||
edit.isEnabled = clone.isEnabled && panel.host.isTemporary.not()
|
||||
|
||||
popupMenu.show(this, e.x, e.y)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import java.nio.file.Path
|
||||
import java.nio.file.attribute.PosixFilePermission
|
||||
import javax.swing.table.DefaultTableModel
|
||||
|
||||
class TransportTableModel() : DefaultTableModel() {
|
||||
internal class TransportTableModel() : DefaultTableModel() {
|
||||
companion object {
|
||||
const val COLUMN_NAME = 0
|
||||
const val COLUMN_TYPE = 1
|
||||
@@ -60,7 +60,7 @@ class TransportTableModel() : DefaultTableModel() {
|
||||
}
|
||||
|
||||
|
||||
data class Attributes(
|
||||
internal data class Attributes(
|
||||
val name: String,
|
||||
val type: String,
|
||||
val isDirectory: Boolean,
|
||||
|
||||
@@ -4,6 +4,8 @@ import app.termora.Disposable
|
||||
import app.termora.Disposer
|
||||
import app.termora.DynamicColor
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviderSupport
|
||||
import app.termora.terminal.DataKey
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import java.awt.*
|
||||
@@ -15,6 +17,9 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
||||
companion object {
|
||||
val MyTransferManager = DataKey(TransferManager::class)
|
||||
}
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val splitPane = JSplitPane()
|
||||
@@ -26,6 +31,7 @@ internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposabl
|
||||
private val rightTransferManager = createInternalTransferManager(rightTabbed, leftTabbed)
|
||||
private val rootSplitPane = JSplitPane(JSplitPane.VERTICAL_SPLIT)
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||
private val support = DataProviderSupport()
|
||||
|
||||
init {
|
||||
initView()
|
||||
@@ -56,10 +62,13 @@ internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposabl
|
||||
rootSplitPane.topComponent = splitPane
|
||||
rootSplitPane.bottomComponent = scrollPane
|
||||
|
||||
support.addData(MyTransferManager, transferManager)
|
||||
|
||||
add(rootSplitPane, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
splitPane.addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentResized(e: ComponentEvent) {
|
||||
removeComponentListener(this)
|
||||
@@ -83,6 +92,8 @@ internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposabl
|
||||
}
|
||||
}
|
||||
|
||||
Disposer.register(this, transferManager)
|
||||
Disposer.register(this, transferTable)
|
||||
Disposer.register(this, leftTabbed)
|
||||
Disposer.register(this, rightTabbed)
|
||||
}
|
||||
@@ -147,6 +158,10 @@ internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposabl
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||
return support.getData(dataKey)
|
||||
}
|
||||
|
||||
internal class MyIcon(private val color: Color) : Icon {
|
||||
private val size = 10
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package app.termora.transfer.internal.sftp
|
||||
|
||||
internal enum class CompressMode(val extension: String) {
|
||||
TarGz("tar.gz"),
|
||||
Tar("tar"),
|
||||
Zip("zip"),
|
||||
SevenZ("7z"),
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package app.termora.transfer.internal.sftp
|
||||
|
||||
import app.termora.I18n
|
||||
import app.termora.WindowScope
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
import app.termora.randomUUID
|
||||
import app.termora.transfer.*
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.sshd.common.file.util.MockPath
|
||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Path
|
||||
import javax.swing.JMenu
|
||||
import javax.swing.JMenuItem
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.name
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
internal class CompressTransportContextMenuExtension private constructor() : TransportContextMenuExtension {
|
||||
companion object {
|
||||
val instance = CompressTransportContextMenuExtension()
|
||||
}
|
||||
|
||||
override fun createJMenuItem(
|
||||
windowScope: WindowScope,
|
||||
fileSystem: FileSystem?,
|
||||
popupMenu: TransportPopupMenu,
|
||||
files: List<Pair<Path, TransportTableModel.Attributes>>
|
||||
): JMenuItem {
|
||||
if (files.isEmpty() || fileSystem !is SftpFileSystem) throw UnsupportedOperationException()
|
||||
val hasParent = files.any { it.second.isParent }
|
||||
if (hasParent) throw UnsupportedOperationException()
|
||||
|
||||
val compressMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.compress"))
|
||||
for (mode in CompressMode.entries) {
|
||||
compressMenu.add(mode.extension).addActionListener(object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
compress(evt, fileSystem, mode, files)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return compressMenu
|
||||
}
|
||||
|
||||
|
||||
private fun compress(
|
||||
event: AnActionEvent,
|
||||
fileSystem: SftpFileSystem,
|
||||
mode: CompressMode,
|
||||
files: List<Pair<Path, TransportTableModel.Attributes>>,
|
||||
) {
|
||||
val transferManager = event.getData(TransportViewer.MyTransferManager) ?: return
|
||||
val file = files.first().first
|
||||
val workdir = file.parent ?: file.fileSystem.getPath(file.fileSystem.separator)
|
||||
val name = StringUtils.defaultIfBlank(if (files.size > 1) workdir.name else file.name, "compress")
|
||||
val target = workdir.resolve(name + ".${mode.extension}")
|
||||
val myTransfer = CompressTransfer(fileSystem, mode, files, workdir, target)
|
||||
if (transferManager.addTransfer(myTransfer).not()) return
|
||||
|
||||
val panel = event.getData(TransportPanel.MyTransportPanel) ?: return
|
||||
transferManager.addTransferListener(object : TransferListener {
|
||||
override fun onTransferChanged(transfer: Transfer, state: TransferTreeTableNode.State) {
|
||||
if (transfer.id() != myTransfer.id()) return
|
||||
if (state == TransferTreeTableNode.State.Done || state == TransferTreeTableNode.State.Failed) {
|
||||
transferManager.removeTransferListener(this)
|
||||
if (state == TransferTreeTableNode.State.Done) {
|
||||
panel.registerSelectRow(target.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private class CompressTransfer(
|
||||
private val fileSystem: SftpFileSystem,
|
||||
private val mode: CompressMode,
|
||||
private val files: List<Pair<Path, TransportTableModel.Attributes>>,
|
||||
private val workdir: Path,
|
||||
private val target: Path,
|
||||
) : Transfer, TransferIndeterminate {
|
||||
private val myID = randomUUID()
|
||||
private var end = false
|
||||
private val mySource = if (files.size == 1) files.first().first
|
||||
else MockPath(files.joinToString(",") { it.first.pathString })
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
override suspend fun transfer(bufferSize: Int): Long {
|
||||
if (end) return 0
|
||||
|
||||
val paths = files.joinToString(StringUtils.SPACE) { "'${it.second.name}'" }
|
||||
val command = StringBuilder()
|
||||
command.append("cd '${workdir.absolutePathString()}'")
|
||||
command.append(" && ")
|
||||
if (mode == CompressMode.TarGz) {
|
||||
command.append("tar -czf")
|
||||
} else if (mode == CompressMode.Tar) {
|
||||
command.append("tar -cf")
|
||||
} else if (mode == CompressMode.Zip) {
|
||||
command.append("zip -r")
|
||||
} else if (mode == CompressMode.SevenZ) {
|
||||
command.append("7z a")
|
||||
}
|
||||
command.append(" '${target.name}' ")
|
||||
command.append(paths)
|
||||
|
||||
fileSystem.clientSession.executeRemoteCommand(command.toString(), System.out, Charsets.UTF_8)
|
||||
|
||||
end = true
|
||||
|
||||
return size()
|
||||
}
|
||||
|
||||
override fun source(): Path {
|
||||
return mySource
|
||||
}
|
||||
|
||||
override fun target(): Path {
|
||||
return target
|
||||
}
|
||||
|
||||
override fun size(): Long {
|
||||
return files.size.toLong()
|
||||
}
|
||||
|
||||
override fun isDirectory(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun priority(): Transfer.Priority {
|
||||
return Transfer.Priority.High
|
||||
}
|
||||
|
||||
override fun scanning(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun id(): String {
|
||||
return myID
|
||||
}
|
||||
|
||||
override fun parentId(): String {
|
||||
return StringUtils.EMPTY
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun ordered(): Long {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user