diff --git a/plugins/THIRDPARTY b/plugins/THIRDPARTY index 4cfb657..d4360b0 100644 --- a/plugins/THIRDPARTY +++ b/plugins/THIRDPARTY @@ -72,4 +72,8 @@ https://www.apache.org/licenses/LICENSE-2.0.html GeoLite2 (https://www.maxmind.com) Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) -https://creativecommons.org/licenses/by-sa/4.0/ \ No newline at end of file +https://creativecommons.org/licenses/by-sa/4.0/ + +smbj +Apache License, Version 2.0 +https://github.com/hierynomus/smbj/blob/master/LICENSE_HEADER \ No newline at end of file diff --git a/plugins/smb/build.gradle.kts b/plugins/smb/build.gradle.kts new file mode 100644 index 0000000..d3dfbd6 --- /dev/null +++ b/plugins/smb/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(libs.plugins.kotlin.jvm) +} + +project.version = "0.0.1" + +dependencies { + testImplementation(kotlin("test")) + implementation("com.hierynomus:smbj:0.14.0") + compileOnly(project(":")) +} + + +apply(from = "$rootDir/plugins/common.gradle.kts") diff --git a/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBFileSystem.kt b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBFileSystem.kt new file mode 100644 index 0000000..79844b5 --- /dev/null +++ b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBFileSystem.kt @@ -0,0 +1,15 @@ +package app.termora.plugins.smb + +import app.termora.transfer.s3.S3FileSystem +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 close() { + share.close() + super.close() + } + +} diff --git a/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBFileSystemProvider.kt b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBFileSystemProvider.kt new file mode 100644 index 0000000..c61169e --- /dev/null +++ b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBFileSystemProvider.kt @@ -0,0 +1,111 @@ +package app.termora.plugins.smb + +import app.termora.transfer.s3.S3FileSystemProvider +import app.termora.transfer.s3.S3Path +import com.hierynomus.msdtyp.AccessMask +import com.hierynomus.msfscc.FileAttributes +import com.hierynomus.mssmb2.SMB2CreateDisposition +import com.hierynomus.mssmb2.SMB2CreateOptions +import com.hierynomus.mssmb2.SMB2ShareAccess +import com.hierynomus.smbj.session.Session +import com.hierynomus.smbj.share.DiskShare +import org.apache.commons.io.FilenameUtils +import org.apache.commons.io.IOUtils +import org.apache.commons.lang3.StringUtils +import java.io.InputStream +import java.io.OutputStream +import java.nio.file.AccessMode +import java.nio.file.NoSuchFileException +import java.nio.file.Path +import java.nio.file.attribute.FileAttribute +import kotlin.io.path.absolutePathString + + +class SMBFileSystemProvider(private val share: DiskShare, private val session: Session) : S3FileSystemProvider() { + + override fun getScheme(): String? { + return "smb" + } + + override fun getOutputStream(path: S3Path): OutputStream { + val file = share.openFile( + path.absolutePathString(), + setOf(AccessMask.GENERIC_WRITE), + setOf(FileAttributes.FILE_ATTRIBUTE_NORMAL), + setOf(SMB2ShareAccess.FILE_SHARE_READ), + SMB2CreateDisposition.FILE_OVERWRITE_IF, + setOf(SMB2CreateOptions.FILE_NON_DIRECTORY_FILE) + ) + val os = file.outputStream + + return object : OutputStream() { + override fun write(b: Int) { + os.write(b) + } + + override fun close() { + IOUtils.closeQuietly(os) + file.closeNoWait() + } + } + } + + override fun getInputStream(path: S3Path): InputStream { + val file = share.openFile( + path.absolutePathString(), + setOf(AccessMask.GENERIC_READ), + setOf(FileAttributes.FILE_ATTRIBUTE_NORMAL), + setOf(SMB2ShareAccess.FILE_SHARE_READ), + SMB2CreateDisposition.FILE_OPEN, + setOf(SMB2CreateOptions.FILE_NON_DIRECTORY_FILE) + ) + val input = file.inputStream + return object : InputStream() { + override fun read(): Int = input.read() + override fun close() { + IOUtils.closeQuietly(input) + file.closeNoWait() + } + } + } + + + override fun fetchChildren(path: S3Path): MutableList { + val paths = mutableListOf() + val absolutePath = FilenameUtils.separatorsToUnix(path.absolutePathString()) + for (information in share.list(if (absolutePath == path.fileSystem.separator) StringUtils.EMPTY else absolutePath)) { + if (information.fileName == "." || information.fileName == "..") continue + val isDir = information.fileAttributes and FileAttributes.FILE_ATTRIBUTE_DIRECTORY.value != 0L + val path = path.resolve(information.fileName) + path.attributes = path.attributes.copy( + directory = isDir, regularFile = isDir.not(), + size = information.endOfFile, + lastModifiedTime = information.lastWriteTime.toDate().time, + lastAccessTime = information.lastAccessTime.toDate().time, + ) + paths.add(path) + } + return paths + } + + override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) { + share.mkdir(dir.absolutePathString()) + } + + override fun delete(path: S3Path, isDirectory: Boolean) { + if (isDirectory) { + share.rmdir(path.absolutePathString(), false) + } else { + share.rm(path.absolutePathString()) + } + } + + + override fun checkAccess(path: S3Path, vararg modes: AccessMode) { + if (share.fileExists(path.absolutePathString()) || share.folderExists(path.absolutePathString())) { + return + } + throw NoSuchFileException(path.absolutePathString()) + } + +} \ No newline at end of file diff --git a/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBHostOptionsPane.kt b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBHostOptionsPane.kt new file mode 100644 index 0000000..649bf51 --- /dev/null +++ b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBHostOptionsPane.kt @@ -0,0 +1,261 @@ +package app.termora.plugins.smb + +import app.termora.* +import com.formdev.flatlaf.FlatClientProperties +import com.formdev.flatlaf.ui.FlatTextBorder +import com.hierynomus.smbj.SMBClient +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.KeyboardFocusManager +import java.awt.event.ComponentAdapter +import java.awt.event.ComponentEvent +import javax.swing.* + +class SMBHostOptionsPane : OptionsPane() { + private val generalOption = GeneralOption() + private val sftpOption = SFTPOption() + + init { + addOption(generalOption) + addOption(sftpOption) + + } + + + fun getHost(): Host { + val name = generalOption.nameTextField.text + val protocol = SMBProtocolProvider.PROTOCOL + val host = generalOption.hostTextField.text + val port = generalOption.portTextField.value as Int + var authentication = Authentication.Companion.No + val authenticationType = AuthenticationType.Password + + authentication = authentication.copy( + type = authenticationType, + password = String(generalOption.passwordTextField.password) + ) + + + val options = Options.Default.copy( + sftpDefaultDirectory = sftpOption.defaultDirectoryField.text, + extras = mutableMapOf( + "smb.share" to generalOption.shareTextField.text, + ) + ) + + return Host( + name = name, + protocol = protocol, + host = host, + port = port, + username = generalOption.usernameTextField.selectedItem as String, + authentication = authentication, + sort = System.currentTimeMillis(), + remark = generalOption.remarkTextArea.text, + options = options, + ) + } + + fun setHost(host: Host) { + generalOption.nameTextField.text = host.name + generalOption.usernameTextField.selectedItem = host.username + generalOption.hostTextField.text = host.host + generalOption.portTextField.value = host.port + generalOption.remarkTextArea.text = host.remark + generalOption.passwordTextField.text = host.authentication.password + generalOption.shareTextField.text = host.options.extras["smb.share"] ?: StringUtils.EMPTY + + sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory + } + + fun validateFields(): Boolean { + + // general + if (validateField(generalOption.nameTextField) + || validateField(generalOption.hostTextField) + || validateField(generalOption.shareTextField) + ) { + return false + } + + val username = generalOption.usernameTextField.selectedItem as String? + if (username.isNullOrBlank()) { + setOutlineError(generalOption.usernameTextField) + return false + } + + + + return true + } + + /** + * 返回 true 表示有错误 + */ + private fun validateField(textField: JTextField): Boolean { + if (textField.isEnabled && textField.text.isBlank()) { + setOutlineError(textField) + return true + } + return false + } + + private fun setOutlineError(textField: JComponent) { + selectOptionJComponent(textField) + textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR) + textField.requestFocusInWindow() + } + + + private inner class GeneralOption : JPanel(BorderLayout()), Option { + val portTextField = PortSpinner(SMBClient.DEFAULT_PORT) + val nameTextField = OutlineTextField(128) + val shareTextField = OutlineTextField(256) + val usernameTextField = OutlineComboBox() + val hostTextField = OutlineTextField(255) + val passwordTextField = OutlinePasswordField(255) + val remarkTextArea = FixedLengthTextArea(512) + + init { + initView() + initEvents() + } + + private fun initView() { + usernameTextField.isEditable = true + usernameTextField.addItem("Guest") + usernameTextField.addItem("Anonymous") + + add(getCenterComponent(), BorderLayout.CENTER) + + } + + 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.password")}:").xy(1, rows) + .add(passwordTextField).xyw(3, rows, 5).apply { rows += step } + + .add("${SMBI18n.getString("termora.plugins.smb.share")}:").xy(1, rows) + .add(shareTextField).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) + + + init { + initView() + initEvents() + } + + private fun initView() { + 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.settings.sftp.default-directory")}:").xy(1, rows) + .add(defaultDirectoryField).xy(3, rows).apply { rows += step } + .build() + + + return panel + } + } + + +} \ No newline at end of file diff --git a/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBI18n.kt b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBI18n.kt new file mode 100644 index 0000000..14fffd7 --- /dev/null +++ b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBI18n.kt @@ -0,0 +1,24 @@ +package app.termora.plugins.smb + +import app.termora.I18n +import app.termora.NamedI18n +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.* + +object SMBI18n : NamedI18n("i18n/messages") { + private val log = LoggerFactory.getLogger(SMBI18n::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) + } + } + +} \ No newline at end of file diff --git a/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBPathHandler.kt b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBPathHandler.kt new file mode 100644 index 0000000..c295e52 --- /dev/null +++ b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBPathHandler.kt @@ -0,0 +1,20 @@ +package app.termora.plugins.smb + +import app.termora.protocol.PathHandler +import com.hierynomus.smbj.SMBClient +import com.hierynomus.smbj.session.Session +import org.apache.commons.io.IOUtils +import java.nio.file.FileSystem +import java.nio.file.Path + +class SMBPathHandler( + private val client: SMBClient, + private val session: Session, + fileSystem: FileSystem, path: Path +) : PathHandler(fileSystem, path) { + override fun dispose() { + super.dispose() + session.close() + IOUtils.closeQuietly(client) + } +} \ No newline at end of file diff --git a/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBPlugin.kt b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBPlugin.kt new file mode 100644 index 0000000..8a09622 --- /dev/null +++ b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBPlugin.kt @@ -0,0 +1,31 @@ +package app.termora.plugins.smb + +import app.termora.plugin.Extension +import app.termora.plugin.ExtensionSupport +import app.termora.plugin.PaidPlugin +import app.termora.protocol.ProtocolHostPanelExtension +import app.termora.protocol.ProtocolProviderExtension + +class SMBPlugin : PaidPlugin { + private val support = ExtensionSupport() + + init { + support.addExtension(ProtocolProviderExtension::class.java) { SMBProtocolProviderExtension.instance } + support.addExtension(ProtocolHostPanelExtension::class.java) { SMBProtocolHostPanelExtension.instance } + } + + override fun getAuthor(): String { + return "TermoraDev" + } + + override fun getName(): String { + return "SMB" + } + + + override fun getExtensions(clazz: Class): List { + return support.getExtensions(clazz) + } + + +} \ No newline at end of file diff --git a/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBProtocolHostPanel.kt b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBProtocolHostPanel.kt new file mode 100644 index 0000000..9a53b87 --- /dev/null +++ b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBProtocolHostPanel.kt @@ -0,0 +1,36 @@ +package app.termora.plugins.smb + +import app.termora.Disposer +import app.termora.Host +import app.termora.protocol.ProtocolHostPanel +import java.awt.BorderLayout + +class SMBProtocolHostPanel : ProtocolHostPanel() { + + private val pane = SMBHostOptionsPane() + + init { + initView() + initEvents() + } + + + private fun initView() { + add(pane, BorderLayout.CENTER) + Disposer.register(this, pane) + } + + private fun initEvents() {} + + override fun getHost(): Host { + return pane.getHost() + } + + override fun setHost(host: Host) { + pane.setHost(host) + } + + override fun validateFields(): Boolean { + return pane.validateFields() + } +} \ No newline at end of file diff --git a/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBProtocolHostPanelExtension.kt b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBProtocolHostPanelExtension.kt new file mode 100644 index 0000000..1c6ebeb --- /dev/null +++ b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBProtocolHostPanelExtension.kt @@ -0,0 +1,19 @@ +package app.termora.plugins.smb + +import app.termora.protocol.ProtocolHostPanel +import app.termora.protocol.ProtocolHostPanelExtension +import app.termora.protocol.ProtocolProvider + +class SMBProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension { + companion object { + val instance by lazy { SMBProtocolHostPanelExtension() } + } + + override fun getProtocolProvider(): ProtocolProvider { + return SMBProtocolProvider.instance + } + + override fun createProtocolHostPanel(): ProtocolHostPanel { + return SMBProtocolHostPanel() + } +} \ No newline at end of file diff --git a/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBProtocolProvider.kt b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBProtocolProvider.kt new file mode 100644 index 0000000..06f81df --- /dev/null +++ b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBProtocolProvider.kt @@ -0,0 +1,57 @@ +package app.termora.plugins.smb + +import app.termora.DynamicIcon +import app.termora.Icons +import app.termora.protocol.PathHandler +import app.termora.protocol.PathHandlerRequest +import app.termora.protocol.TransferProtocolProvider +import com.hierynomus.smbj.SMBClient +import com.hierynomus.smbj.auth.AuthenticationContext +import com.hierynomus.smbj.share.DiskShare +import org.apache.commons.io.FilenameUtils +import org.apache.commons.lang3.StringUtils + +class SMBProtocolProvider private constructor() : TransferProtocolProvider { + + companion object { + val instance by lazy { SMBProtocolProvider() } + const val PROTOCOL = "SMB" + } + + override fun getProtocol(): String { + return PROTOCOL + } + + override fun getIcon(width: Int, height: Int): DynamicIcon { + return Icons.windows7 + } + + override fun createPathHandler(requester: PathHandlerRequest): PathHandler { + val client = SMBClient() + val host = requester.host + val connection = client.connect(host.host, host.port) + val session = when (host.username) { + "Guest" -> connection.authenticate(AuthenticationContext.guest()) + "Anonymous" -> connection.authenticate(AuthenticationContext.anonymous()) + else -> connection.authenticate( + AuthenticationContext( + host.username, + host.authentication.password.toCharArray(), + null + ) + ) + } + val share = session.connectShare(host.options.extras["smb.share"] ?: StringUtils.EMPTY) as DiskShare + var sftpDefaultDirectory = StringUtils.defaultString(host.options.sftpDefaultDirectory) + sftpDefaultDirectory = if (sftpDefaultDirectory.isNotBlank()) { + FilenameUtils.separatorsToUnix(sftpDefaultDirectory) + } else { + "/" + } + + val fs = SMBFileSystem(share, session) + return SMBPathHandler(client, session, fs, fs.getPath(sftpDefaultDirectory)) + } + + +} \ No newline at end of file diff --git a/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBProtocolProviderExtension.kt b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBProtocolProviderExtension.kt new file mode 100644 index 0000000..3716e7c --- /dev/null +++ b/plugins/smb/src/main/kotlin/app/termora/plugins/smb/SMBProtocolProviderExtension.kt @@ -0,0 +1,14 @@ +package app.termora.plugins.smb + +import app.termora.protocol.ProtocolProvider +import app.termora.protocol.ProtocolProviderExtension + +class SMBProtocolProviderExtension private constructor() : ProtocolProviderExtension { + companion object { + val instance by lazy { SMBProtocolProviderExtension() } + } + + override fun getProtocolProvider(): ProtocolProvider { + return SMBProtocolProvider.instance + } +} \ No newline at end of file diff --git a/plugins/smb/src/main/resources/META-INF/plugin.xml b/plugins/smb/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000..c4d1695 --- /dev/null +++ b/plugins/smb/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,24 @@ + + + smb + + SMB + + + + ${projectVersion} + + + + app.termora.plugins.smb.SMBPlugin + + + Connecting to SMB + 支持连接到 SMB + 支援連接到 SMB + + + TermoraDev + + + diff --git a/plugins/smb/src/main/resources/META-INF/pluginIcon.svg b/plugins/smb/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 0000000..c775631 --- /dev/null +++ b/plugins/smb/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/smb/src/main/resources/i18n/messages.properties b/plugins/smb/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..8e09059 --- /dev/null +++ b/plugins/smb/src/main/resources/i18n/messages.properties @@ -0,0 +1 @@ +termora.plugins.smb.share=Share name diff --git a/plugins/smb/src/main/resources/i18n/messages_zh_CN.properties b/plugins/smb/src/main/resources/i18n/messages_zh_CN.properties new file mode 100644 index 0000000..ef750e8 --- /dev/null +++ b/plugins/smb/src/main/resources/i18n/messages_zh_CN.properties @@ -0,0 +1 @@ +termora.plugins.smb.share=共享名称 diff --git a/plugins/smb/src/main/resources/i18n/messages_zh_TW.properties b/plugins/smb/src/main/resources/i18n/messages_zh_TW.properties new file mode 100644 index 0000000..7d0ca4d --- /dev/null +++ b/plugins/smb/src/main/resources/i18n/messages_zh_TW.properties @@ -0,0 +1 @@ +termora.plugins.smb.share=共享名稱 diff --git a/settings.gradle.kts b/settings.gradle.kts index 5e4af04..4f727d4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,3 +14,4 @@ include("plugins:migration") include("plugins:editor") include("plugins:geo") include("plugins:webdav") +include("plugins:smb") diff --git a/src/main/kotlin/app/termora/DynamicIcon.kt b/src/main/kotlin/app/termora/DynamicIcon.kt index 3b9efcf..6bd6344 100644 --- a/src/main/kotlin/app/termora/DynamicIcon.kt +++ b/src/main/kotlin/app/termora/DynamicIcon.kt @@ -2,7 +2,7 @@ package app.termora import com.formdev.flatlaf.extras.FlatSVGIcon -open class DynamicIcon(name: String, private val darkName: String, val allowColorFilter: Boolean = true) : +open class DynamicIcon(name: String, private val darkName: String = name, val allowColorFilter: Boolean = true) : FlatSVGIcon(name) { constructor(name: String) : this(name, name) diff --git a/src/main/kotlin/app/termora/Icons.kt b/src/main/kotlin/app/termora/Icons.kt index 3fcbd49..da21634 100644 --- a/src/main/kotlin/app/termora/Icons.kt +++ b/src/main/kotlin/app/termora/Icons.kt @@ -80,6 +80,7 @@ object Icons { val ssh by lazy { DynamicIcon("icons/ssh.svg", "icons/ssh_dark.svg") } val ftp by lazy { DynamicIcon("icons/ftp.svg", "icons/ftp_dark.svg") } val minio by lazy { DynamicIcon("icons/minio.svg", "icons/minio_dark.svg") } + val windows7 by lazy { DynamicIcon("icons/windows7.svg", allowColorFilter = false) } val powershell by lazy { DynamicIcon("icons/powershell.svg", "icons/powershell_dark.svg") } val serial by lazy { DynamicIcon("icons/serial.svg", "icons/serial_dark.svg") } val fileFormat by lazy { DynamicIcon("icons/fileFormat.svg", "icons/fileFormat_dark.svg") } diff --git a/src/main/kotlin/app/termora/transfer/TransportNavigationPanel.kt b/src/main/kotlin/app/termora/transfer/TransportNavigationPanel.kt index 11585a7..394614b 100644 --- a/src/main/kotlin/app/termora/transfer/TransportNavigationPanel.kt +++ b/src/main/kotlin/app/termora/transfer/TransportNavigationPanel.kt @@ -11,6 +11,7 @@ import com.formdev.flatlaf.extras.components.FlatTextField import com.formdev.flatlaf.extras.components.FlatToolBar import com.formdev.flatlaf.ui.FlatLineBorder import com.formdev.flatlaf.util.SystemInfo +import org.apache.commons.io.FilenameUtils import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.SystemUtils import org.apache.commons.lang3.exception.ExceptionUtils @@ -205,7 +206,7 @@ class TransportNavigationPanel( if (path.fileSystem.isWindowsFileSystem() && path.pathString == path.fileSystem.separator) { textField.text = StringUtils.EMPTY } else { - textField.text = path.absolutePathString() + textField.text = FilenameUtils.separatorsToUnix(path.absolutePathString()) } } diff --git a/src/main/resources/icons/windows7.svg b/src/main/resources/icons/windows7.svg new file mode 100644 index 0000000..c775631 --- /dev/null +++ b/src/main/resources/icons/windows7.svg @@ -0,0 +1 @@ + \ No newline at end of file