diff --git a/THIRDPARTY b/THIRDPARTY index ebfb779..4bf1918 100644 --- a/THIRDPARTY +++ b/THIRDPARTY @@ -38,6 +38,10 @@ commons-text 1.13.0 Apache License 2.0 https://github.com/apache/commons-text/blob/master/LICENSE.txt +commons-csv 1.13.0 +Apache License 2.0 +https://github.com/apache/commons-csv/blob/master/LICENSE.txt + eddsa 0.3.0 Creative Commons Zero v1.0 Universal https://github.com/str4d/ed25519-java/blob/master/LICENSE.txt diff --git a/build.gradle.kts b/build.gradle.kts index 388e82f..8d272e3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -59,6 +59,7 @@ dependencies { implementation(libs.commons.codec) implementation(libs.commons.io) implementation(libs.commons.lang3) + implementation(libs.commons.csv) implementation(libs.commons.net) implementation(libs.commons.text) implementation(libs.commons.compress) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3db756f..11bb96a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ trove4j = "1.0.20200330" kotlinx-serialization-json = "1.8.0" commons-codec = "1.18.0" commons-lang3 = "3.17.0" +commons-csv = "1.13.0" commons-net = "3.11.1" commons-text = "1.13.0" commons-compress = "1.27.1" @@ -41,7 +42,7 @@ rhino = "1.7.15" delight-rhino-sandbox = "0.0.17" testcontainers = "1.20.4" mixpanel = "1.5.3" -jSerialComm="2.11.0" +jSerialComm = "2.11.0" [libraries] kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } @@ -53,6 +54,7 @@ tinylog-impl = { group = "org.tinylog", name = "tinylog-impl", version.ref = "ti commons-codec = { group = "commons-codec", name = "commons-codec", version.ref = "commons-codec" } commons-net = { group = "commons-net", name = "commons-net", version.ref = "commons-net" } commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commons-lang3" } +commons-csv = { group = "org.apache.commons", name = "commons-csv", version.ref = "commons-csv" } commons-text = { group = "org.apache.commons", name = "commons-text", version.ref = "commons-text" } commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "commons-compress" } pty4j = { group = "org.jetbrains.pty4j", name = "pty4j", version.ref = "pty4j" } diff --git a/src/main/kotlin/app/termora/HostManager.kt b/src/main/kotlin/app/termora/HostManager.kt index cdb249f..8fb2ab1 100644 --- a/src/main/kotlin/app/termora/HostManager.kt +++ b/src/main/kotlin/app/termora/HostManager.kt @@ -17,7 +17,11 @@ class HostManager private constructor() { fun addHost(host: Host) { assertEventDispatchThread() database.addHost(host) - setHost(host) + if (host.deleted) { + hosts.entries.removeIf { it.value.id == host.id || it.value.parentId == host.id } + } else { + hosts[host.id] = host + } } /** @@ -39,12 +43,4 @@ class HostManager private constructor() { return hosts[id] } - - /** - * 仅修改缓存中的 - */ - fun setHost(host: Host) { - assertEventDispatchThread() - hosts[host.id] = host - } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/HostTreeNode.kt b/src/main/kotlin/app/termora/HostTreeNode.kt index 115d856..d8207b3 100644 --- a/src/main/kotlin/app/termora/HostTreeNode.kt +++ b/src/main/kotlin/app/termora/HostTreeNode.kt @@ -1,18 +1,26 @@ package app.termora import javax.swing.tree.DefaultMutableTreeNode +import javax.swing.tree.TreeNode class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) { companion object { private val hostManager get() = HostManager.getInstance() } + /** + * 如果要重新赋值,记得修改 [Host.updateDate] 否则下次取出时可能时缓存的 + */ var host: Host - get() = hostManager.getHost((userObject as Host).id) ?: userObject as Host - set(value) { - setUserObject(value) - hostManager.setHost(value) + get() { + val cacheHost = hostManager.getHost((userObject as Host).id) + val myHost = userObject as Host + if (cacheHost == null) { + return myHost + } + return if (cacheHost.updateDate > myHost.updateDate) cacheHost else myHost } + set(value) = setUserObject(value) val folderCount get() = children().toList().count { if (it is HostTreeNode) it.host.protocol == Protocol.Folder else false } @@ -32,6 +40,29 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) { return children } + fun childrenNode(): List { + return children?.map { it as HostTreeNode } ?: emptyList() + } + + + /** + * 深度克隆 + * @param scopes 克隆的范围 + */ + fun clone(scopes: Set = emptySet()): HostTreeNode { + val newNode = clone() as HostTreeNode + deepClone(newNode, this, scopes) + return newNode + } + + private fun deepClone(newNode: HostTreeNode, oldNode: HostTreeNode, scopes: Set = emptySet()) { + for (child in oldNode.childrenNode()) { + if (scopes.isNotEmpty() && !scopes.contains(child.host.protocol)) continue + val newChildNode = child.clone() as HostTreeNode + deepClone(newChildNode, child, scopes) + newNode.add(newChildNode) + } + } override fun clone(): Any { val newNode = HostTreeNode(host) @@ -40,6 +71,17 @@ class HostTreeNode(host: Host) : DefaultMutableTreeNode(host) { return newNode } + override fun isNodeChild(aNode: TreeNode?): Boolean { + if (aNode is HostTreeNode) { + for (node in childrenNode()) { + if (node.host == aNode.host) { + return true + } + } + } + return super.isNodeChild(aNode) + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/src/main/kotlin/app/termora/NewHostTree.kt b/src/main/kotlin/app/termora/NewHostTree.kt index 92e73a8..d4be85a 100644 --- a/src/main/kotlin/app/termora/NewHostTree.kt +++ b/src/main/kotlin/app/termora/NewHostTree.kt @@ -11,12 +11,16 @@ import kotlinx.serialization.json.intOrNull import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive +import org.apache.commons.csv.CSVFormat +import org.apache.commons.csv.CSVParser +import org.apache.commons.csv.CSVPrinter import org.apache.commons.io.FileUtils import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.exception.ExceptionUtils import org.jdesktop.swingx.JXTree import org.jdesktop.swingx.action.ActionManager import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer +import org.slf4j.LoggerFactory import java.awt.Component import java.awt.Dimension import java.awt.datatransfer.DataFlavor @@ -26,7 +30,7 @@ import java.awt.event.ActionEvent import java.awt.event.ActionListener import java.awt.event.MouseAdapter import java.awt.event.MouseEvent -import java.io.File +import java.io.* import java.util.* import java.util.function.Function import javax.swing.* @@ -41,6 +45,11 @@ import kotlin.math.min class NewHostTree : JXTree() { + companion object { + private val log = LoggerFactory.getLogger(NewHostTree::class.java) + private val CSV_HEADERS = arrayOf("Folders", "Label", "Hostname", "Port", "Username", "Protocol") + } + private val tree = this private val editor = OutlineTextField(64) private val hostManager get() = HostManager.getInstance() @@ -207,7 +216,7 @@ class NewHostTree : JXTree() { if (lastHost !is HostTreeNode || editor.text.isBlank() || editor.text == lastHost.host.name) { return } - lastHost.host = lastHost.host.copy(name = editor.text) + lastHost.host = lastHost.host.copy(name = editor.text, updateDate = System.currentTimeMillis()) hostManager.addHost(lastHost.host) } @@ -298,7 +307,7 @@ class NewHostTree : JXTree() { // 转移 for (e in nodes) { model.removeNodeFromParent(e) - e.host = e.host.copy(parentId = node.host.id) + e.host = e.host.copy(parentId = node.host.id, updateDate = System.currentTimeMillis()) hostManager.addHost(e.host) if (dropLocation.childIndex == -1) { @@ -347,6 +356,7 @@ class NewHostTree : JXTree() { val newFolder = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.folder")) val newHost = newMenu.add(I18n.getString("termora.welcome.contextmenu.new.host")) val importMenu = JMenu(I18n.getString("termora.welcome.contextmenu.import")) + val csvMenu = importMenu.add("CSV") val windTermMenu = importMenu.add("WindTerm") val open = popupMenu.add(I18n.getString("termora.welcome.contextmenu.connect")) @@ -375,6 +385,9 @@ class NewHostTree : JXTree() { popupMenu.add(showMoreInfo) val property = popupMenu.add(I18n.getString("termora.welcome.contextmenu.property")) + csvMenu.addActionListener { + importHosts(lastNode, ImportType.CSV) + } windTermMenu.addActionListener { importHosts(lastNode, ImportType.WindTerm) } open.addActionListener { openHosts(it, false) } openInNewWindow.addActionListener { openHosts(it, true) } @@ -604,6 +617,17 @@ class NewHostTree : JXTree() { } private fun importHosts(folder: HostTreeNode, type: ImportType) { + try { + doImportHosts(folder, type) + } catch (e: Exception) { + if (log.isErrorEnabled) { + log.error(e.message, e) + } + OptionPane.showMessageDialog(owner, ExceptionUtils.getMessage(e), messageType = JOptionPane.ERROR_MESSAGE) + } + } + + private fun doImportHosts(folder: HostTreeNode, type: ImportType) { val chooser = JFileChooser() chooser.fileSelectionMode = JFileChooser.FILES_ONLY chooser.isAcceptAllFileFilterUsed = false @@ -611,6 +635,8 @@ class NewHostTree : JXTree() { if (type == ImportType.WindTerm) { chooser.fileFilter = FileNameExtensionFilter("WindTerm(*.sessions)", "sessions") + } else if (type == ImportType.CSV) { + chooser.fileFilter = FileNameExtensionFilter("CSV(*.csv)", "csv") } val dir = properties.getString("NewHostTree.ImportHosts.defaultDir", StringUtils.EMPTY) @@ -621,7 +647,47 @@ class NewHostTree : JXTree() { } } + // csv template + if (type == ImportType.CSV) { + val code = OptionPane.showConfirmDialog( + owner, + I18n.getString("termora.welcome.contextmenu.import.csv.download-template"), + optionType = JOptionPane.YES_NO_OPTION, + messageType = JOptionPane.QUESTION_MESSAGE, + options = arrayOf( + I18n.getString("termora.welcome.contextmenu.import"), + I18n.getString("termora.welcome.contextmenu.download") + ), + initialValue = I18n.getString("termora.welcome.contextmenu.import") + ) + if (code == JOptionPane.DEFAULT_OPTION) { + return + } else if (code != JOptionPane.YES_OPTION) { + chooser.setSelectedFile(File("termora_import.csv")) + if (chooser.showSaveDialog(owner) == JFileChooser.APPROVE_OPTION) { + CSVPrinter( + FileWriter(chooser.selectedFile, Charsets.UTF_8), + CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get() + ).use { printer -> + printer.printRecord("Projects/Dev", "Web Server", "192.168.1.1", "22", "root", "SSH") + printer.printRecord("Projects/Prod", "Web Server", "serverhost.com", "2222", "root", "SSH") + printer.printRecord(StringUtils.EMPTY, "Web Server", "serverhost.com", "2222", "user", "SSH") + } + OptionPane.openFileInFolder( + owner, + chooser.selectedFile, + I18n.getString("termora.welcome.contextmenu.import.csv.download-template-done-open-folder"), + I18n.getString("termora.welcome.contextmenu.import.csv.download-template-done") + ) + } + return + } + } + + // 选择文件 val code = chooser.showOpenDialog(owner) + + // 记住目录 properties.putString("NewHostTree.ImportHosts.defaultDir", chooser.currentDirectory.absolutePath) if (code != JFileChooser.APPROVE_OPTION) { @@ -630,15 +696,16 @@ class NewHostTree : JXTree() { val file = chooser.selectedFile - val nodes = if (type == ImportType.WindTerm) { - parseFromWindTerm(file) - } else { - emptyList() + val nodes = when (type) { + ImportType.WindTerm -> parseFromWindTerm(folder, file) + ImportType.CSV -> file.bufferedReader().use { parseFromCSV(folder, it) } } - for (node in nodes) { - node.host = node.host.copy(parentId = folder.host.id) + node.host = node.host.copy(parentId = folder.host.id, updateDate = System.currentTimeMillis()) + if (folder.getIndex(node) != -1) { + continue + } model.insertNodeInto( node, folder, @@ -650,36 +717,73 @@ class NewHostTree : JXTree() { hostManager.addHost(node.host) node.getAllChildren().forEach { hostManager.addHost(it.host) } } + + // 重新加载 + model.reload(folder) } - private fun parseFromWindTerm(file: File): List { + private fun parseFromWindTerm(folder: HostTreeNode, file: File): List { val sessions = ohMyJson.runCatching { ohMyJson.parseToJsonElement(file.readText()).jsonArray } .onFailure { OptionPane.showMessageDialog(owner, ExceptionUtils.getMessage(it)) } .getOrNull() ?: return emptyList() - val nodes = mutableListOf() - for (i in 0 until sessions.size) { - val json = sessions[i].jsonObject - val protocol = json["session.protocol"]?.jsonPrimitive?.content ?: StringUtils.EMPTY - if (protocol != "SSH") continue - val label = json["session.label"]?.jsonPrimitive?.content ?: StringUtils.EMPTY - val target = json["session.target"]?.jsonPrimitive?.content ?: StringUtils.EMPTY - val port = json["session.port"]?.jsonPrimitive?.intOrNull ?: 22 - val group = json["session.group"]?.jsonPrimitive?.content ?: StringUtils.EMPTY - val groups = group.split(">") + val sw = StringWriter() + CSVPrinter(sw, CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).get()).use { printer -> + for (i in 0 until sessions.size) { + val json = sessions[i].jsonObject + val protocol = json["session.protocol"]?.jsonPrimitive?.content ?: "SSH" + if (!StringUtils.equalsIgnoreCase("SSH", protocol)) continue + val label = json["session.label"]?.jsonPrimitive?.content ?: StringUtils.EMPTY + val target = json["session.target"]?.jsonPrimitive?.content ?: StringUtils.EMPTY + val port = json["session.port"]?.jsonPrimitive?.intOrNull ?: 22 + val group = json["session.group"]?.jsonPrimitive?.content ?: StringUtils.EMPTY + val groups = group.split(">") + printer.printRecord(groups.joinToString("/"), label, target, port, StringUtils.EMPTY, "SSH") + } + } + + return parseFromCSV(folder, StringReader(sw.toString())) + } + + private fun parseFromCSV(folderNode: HostTreeNode, sr: Reader): List { + val records = CSVParser.builder() + .setFormat(CSVFormat.EXCEL.builder().setHeader(*CSV_HEADERS).setSkipHeaderRecord(true).get()) + .setCharset(Charsets.UTF_8) + .setReader(sr) + .get() + .use { it.records } + // 把现有目录提取出来,避免重复创建 + val nodes = folderNode.clone(setOf(Protocol.Folder)) + .childrenNode().filter { it.host.protocol == Protocol.Folder } + .toMutableList() + + for (record in records) { + val map = mutableMapOf() + for (e in record.parser.headerMap.keys) { + map[e] = record.get(e) + } + + val folder = map["Folders"] ?: StringUtils.EMPTY + val label = map["Label"] ?: StringUtils.EMPTY + val hostname = map["Hostname"] ?: StringUtils.EMPTY + val port = map["Port"]?.toIntOrNull() ?: 22 + val username = map["Username"] ?: StringUtils.EMPTY + val protocol = map["Protocol"] ?: "SSH" + if (!StringUtils.equalsIgnoreCase(protocol, "SSH")) continue + if (StringUtils.isAllBlank(hostname, label)) continue var p: HostTreeNode? = null - if (group.isNotBlank()) { - for (j in groups.indices) { + if (folder.isNotBlank()) { + for ((j, name) in folder.split("/").withIndex()) { val folders = if (j == 0 || p == null) nodes else p.children().toList().filterIsInstance() val n = HostTreeNode( Host( - name = groups[j], protocol = Protocol.Folder, + name = name, protocol = Protocol.Folder, parentId = p?.host?.id ?: StringUtils.EMPTY ) ) - val cp = folders.find { it.host.protocol == Protocol.Folder && it.host.name == groups[j] } + val cp = folders.find { it.host.protocol == Protocol.Folder && it.host.name == name } if (cp != null) { p = cp continue @@ -696,9 +800,10 @@ class NewHostTree : JXTree() { val n = HostTreeNode( Host( - name = StringUtils.defaultIfBlank(label, target), - host = target, + name = StringUtils.defaultIfBlank(label, hostname), + host = hostname, port = port, + username = username, protocol = Protocol.SSH, parentId = p?.host?.id ?: StringUtils.EMPTY, ) @@ -716,7 +821,8 @@ class NewHostTree : JXTree() { private enum class ImportType { - WindTerm + WindTerm, + CSV, } private class MoveHostTransferable(val nodes: List) : Transferable { diff --git a/src/main/kotlin/app/termora/NewHostTreeModel.kt b/src/main/kotlin/app/termora/NewHostTreeModel.kt index 389fd9e..e5c3d65 100644 --- a/src/main/kotlin/app/termora/NewHostTreeModel.kt +++ b/src/main/kotlin/app/termora/NewHostTreeModel.kt @@ -73,7 +73,7 @@ class NewHostTreeModel : DefaultTreeModel( for ((i, c) in parent.children().toList().filterIsInstance().withIndex()) { val sort = i.toLong() if (c.host.sort == sort) continue - c.host = c.host.copy(sort = sort) + c.host = c.host.copy(sort = sort, updateDate = System.currentTimeMillis()) hostManager.addHost(c.host) } } diff --git a/src/main/kotlin/app/termora/SFTPPtyTerminalTab.kt b/src/main/kotlin/app/termora/SFTPPtyTerminalTab.kt index 7b2ca40..7c9289e 100644 --- a/src/main/kotlin/app/termora/SFTPPtyTerminalTab.kt +++ b/src/main/kotlin/app/termora/SFTPPtyTerminalTab.kt @@ -50,6 +50,7 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal // 如果配置了跳板机或者代理,那么通过 SSH 的端口转发到本地 if (useJumpHosts) { host = host.copy( + updateDate = System.currentTimeMillis(), tunnelings = listOf( Tunneling( type = TunnelingType.Local, @@ -66,7 +67,11 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal // 打开通道 for (tunneling in host.tunnelings) { val address = SshClients.openTunneling(sshSession, host, tunneling) - host = host.copy(host = address.hostName, port = address.port) + host = host.copy( + host = address.hostName, + port = address.port, + updateDate = System.currentTimeMillis(), + ) } } @@ -128,9 +133,9 @@ class SFTPPtyTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminal private fun setAuthentication(commands: MutableList, host: Host) { // 如果通过公钥连接 if (host.authentication.type == AuthenticationType.PublicKey) { - val keyPair = keyManager.getOhKeyPair(host.authentication.password) - if (keyPair != null) { - val keyPair = OhKeyPairKeyPairProvider.generateKeyPair(keyPair) + val ohKeyPair = keyManager.getOhKeyPair(host.authentication.password) + if (ohKeyPair != null) { + val keyPair = OhKeyPairKeyPairProvider.generateKeyPair(ohKeyPair) val privateKeyPath = Application.createSubTemporaryDir() val privateKeyFile = Files.createTempFile(privateKeyPath, Application.getName(), StringUtils.EMPTY) Files.newOutputStream(privateKeyFile) diff --git a/src/main/kotlin/app/termora/SSHTerminalTab.kt b/src/main/kotlin/app/termora/SSHTerminalTab.kt index 40bce7a..e3c2383 100644 --- a/src/main/kotlin/app/termora/SSHTerminalTab.kt +++ b/src/main/kotlin/app/termora/SSHTerminalTab.kt @@ -89,7 +89,8 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) : terminal.write("SSH client is opening...\r\n") } - var host = this.host.copy(authentication = this.host.authentication.copy()) + var host = + this.host.copy(authentication = this.host.authentication.copy(), updateDate = System.currentTimeMillis()) val owner = SwingUtilities.getWindowAncestor(terminalPanel) val client = SshClients.openClient(host).also { sshClient = it } client.serverKeyVerifier = DialogServerKeyVerifier(owner) @@ -103,13 +104,14 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) : host = host.copy( authentication = authentication, username = dialog.getUsername(), + updateDate = System.currentTimeMillis(), ) // save if (dialog.isRemembered()) { HostManager.getInstance().addHost( tab.host.copy( authentication = authentication, - username = dialog.getUsername(), + username = dialog.getUsername(), updateDate = System.currentTimeMillis(), ) ) } diff --git a/src/main/kotlin/app/termora/SshClients.kt b/src/main/kotlin/app/termora/SshClients.kt index 53502ff..5413ac6 100644 --- a/src/main/kotlin/app/termora/SshClients.kt +++ b/src/main/kotlin/app/termora/SshClients.kt @@ -151,7 +151,7 @@ object SshClients { log.info("jump host: ${currentHost.host}:${currentHost.port} , next host: ${nextHost.host}:${nextHost.port} , local address: ${address.hostName}:${address.port}") } // 映射完毕之后修改Host和端口 - jumpHosts[i + 1] = nextHost.copy(host = address.hostName, port = address.port) + jumpHosts[i + 1] = nextHost.copy(host = address.hostName, port = address.port, updateDate = System.currentTimeMillis()) } } diff --git a/src/main/kotlin/app/termora/TerminalTabbed.kt b/src/main/kotlin/app/termora/TerminalTabbed.kt index 84df7d4..03a7c8a 100644 --- a/src/main/kotlin/app/termora/TerminalTabbed.kt +++ b/src/main/kotlin/app/termora/TerminalTabbed.kt @@ -356,7 +356,7 @@ class TerminalTabbed( } host = host.copy( - protocol = Protocol.SFTPPty, + protocol = Protocol.SFTPPty, updateDate = System.currentTimeMillis(), options = host.options.copy(env = envs.toPropertiesString()) ) } diff --git a/src/main/kotlin/app/termora/transport/SFTPAction.kt b/src/main/kotlin/app/termora/transport/SFTPAction.kt index 14d46e8..450250b 100644 --- a/src/main/kotlin/app/termora/transport/SFTPAction.kt +++ b/src/main/kotlin/app/termora/transport/SFTPAction.kt @@ -14,7 +14,7 @@ class SFTPAction : AnAction("SFTP", Icons.folder) { val tab = openOrCreateSFTPTerminalTab(evt) ?: return if (host != null) { - connectHost(host.copy(protocol = Protocol.SSH), tab) + connectHost(host.copy(protocol = Protocol.SSH, updateDate = System.currentTimeMillis()), tab) } } diff --git a/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt b/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt index 93aec67..229257b 100644 --- a/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt +++ b/src/main/kotlin/app/termora/transport/SftpFileSystemPanel.kt @@ -107,7 +107,7 @@ class SftpFileSystemPanel( private suspend fun doConnect() { val thisHost = this.host ?: return - var host = thisHost.copy(authentication = thisHost.authentication.copy()) + var host = thisHost.copy(authentication = thisHost.authentication.copy(), updateDate = System.currentTimeMillis()) closeIO() @@ -123,14 +123,14 @@ class SftpFileSystemPanel( val authentication = dialog.getAuthentication() host = host.copy( authentication = authentication, - username = dialog.getUsername(), + username = dialog.getUsername(), updateDate = System.currentTimeMillis(), ) // save if (dialog.isRemembered()) { HostManager.getInstance().addHost( host.copy( authentication = authentication, - username = dialog.getUsername(), + username = dialog.getUsername(), updateDate = System.currentTimeMillis(), ) ) } diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index e29fdc6..e0ec891 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -144,6 +144,10 @@ termora.welcome.contextmenu.new.host=Host termora.welcome.contextmenu.new.folder.name=New Folder termora.welcome.contextmenu.property=Properties termora.welcome.contextmenu.show-more-info=Show more info +termora.welcome.contextmenu.download=Download +termora.welcome.contextmenu.import.csv.download-template=Do you want to import or download the template? +termora.welcome.contextmenu.import.csv.download-template-done=Download the template successfully +termora.welcome.contextmenu.import.csv.download-template-done-open-folder=Download the template successfully, Do you want to open the folder? # New Host termora.new-host.title=Create a new host diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index 941da37..21d830e 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -132,6 +132,11 @@ termora.welcome.contextmenu.new.folder.name=新建文件夹 termora.welcome.contextmenu.property=属性 termora.welcome.contextmenu.show-more-info=显示更多信息 +termora.welcome.contextmenu.download=下载 +termora.welcome.contextmenu.import.csv.download-template=您要导入还是下载模板? +termora.welcome.contextmenu.import.csv.download-template-done=下载成功 +termora.welcome.contextmenu.import.csv.download-template-done-open-folder=下载成功, 是否需要打开所在文件夹? + # New Host termora.new-host.title=新建主机 termora.new-host.general=属性 diff --git a/src/main/resources/i18n/messages_zh_TW.properties b/src/main/resources/i18n/messages_zh_TW.properties index 65e3cda..0666740 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -130,6 +130,10 @@ termora.welcome.contextmenu.new.host=主機 termora.welcome.contextmenu.new.folder.name=新建資料夾 termora.welcome.contextmenu.property=屬性 termora.welcome.contextmenu.show-more-info=顯示更多信息 +termora.welcome.contextmenu.download=下載 +termora.welcome.contextmenu.import.csv.download-template=您要匯入還是下載範本? +termora.welcome.contextmenu.import.csv.download-template-done=下載成功 +termora.welcome.contextmenu.import.csv.download-template-done-open-folder=下載成功, 是否需要開啟所在資料夾? # New Host termora.new-host.title=新主機