diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 630f2bf..da2d482 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin = "2.1.10" +kotlin = "2.1.20" slf4j = "2.0.17" pty4j = "0.13.2" tinylog = "2.7.0" diff --git a/src/main/kotlin/app/termora/Icons.kt b/src/main/kotlin/app/termora/Icons.kt index 5b47546..bf6829e 100644 --- a/src/main/kotlin/app/termora/Icons.kt +++ b/src/main/kotlin/app/termora/Icons.kt @@ -93,6 +93,7 @@ object Icons { val sortedSet by lazy { DynamicIcon("icons/sortedSet.svg", "icons/sortedSet_dark.svg") } val colorPicker by lazy { DynamicIcon("icons/colorPicker.svg", "icons/colorPicker_dark.svg") } val folder by lazy { DynamicIcon("icons/folder.svg", "icons/folder_dark.svg") } + val file by lazy { DynamicIcon("icons/file.svg", "icons/file_dark.svg") } val listFiles by lazy { DynamicIcon("icons/listFiles.svg", "icons/listFiles_dark.svg") } val left by lazy { DynamicIcon("icons/left.svg", "icons/left_dark.svg") } val right by lazy { DynamicIcon("icons/right.svg", "icons/right_dark.svg") } diff --git a/src/main/kotlin/app/termora/OptionPane.kt b/src/main/kotlin/app/termora/OptionPane.kt index 7a3e99c..6453e2e 100644 --- a/src/main/kotlin/app/termora/OptionPane.kt +++ b/src/main/kotlin/app/termora/OptionPane.kt @@ -29,6 +29,7 @@ object OptionPane { icon: Icon? = null, options: Array? = null, initialValue: Any? = null, + customizeDialog: (JDialog) -> Unit = {}, ): Int { val panel = if (message is JComponent) { @@ -47,6 +48,9 @@ object OptionPane { override fun selectInitialValue() { super.selectInitialValue() if (message is JComponent) { + if (message.getClientProperty("SKIP_requestFocusInWindow") == true) { + return + } message.requestFocusInWindow() } } @@ -58,6 +62,7 @@ object OptionPane { } }) dialog.setLocationRelativeTo(parentComponent) + customizeDialog.invoke(dialog) dialog.isVisible = true dialog.dispose() val selectedValue = pane.value diff --git a/src/main/kotlin/app/termora/sftp/FileSystemViewTable.kt b/src/main/kotlin/app/termora/sftp/FileSystemViewTable.kt index d1e2790..87a58bc 100644 --- a/src/main/kotlin/app/termora/sftp/FileSystemViewTable.kt +++ b/src/main/kotlin/app/termora/sftp/FileSystemViewTable.kt @@ -4,13 +4,17 @@ import app.termora.* import app.termora.actions.AnActionEvent import app.termora.actions.SettingsAction import com.formdev.flatlaf.FlatClientProperties +import com.formdev.flatlaf.extras.FlatSVGIcon import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.util.SystemInfo +import com.jgoodies.forms.builder.FormBuilder +import com.jgoodies.forms.layout.FormLayout import kotlinx.coroutines.* import kotlinx.coroutines.swing.Swing import org.apache.commons.io.FileUtils import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.exception.ExceptionUtils +import org.apache.commons.lang3.time.DateFormatUtils import org.apache.sshd.sftp.client.SftpClient import org.apache.sshd.sftp.client.fs.SftpFileSystem import org.apache.sshd.sftp.client.fs.SftpPath @@ -18,6 +22,7 @@ import org.apache.sshd.sftp.client.fs.SftpPosixFileAttributes import org.jdesktop.swingx.action.ActionManager import org.slf4j.LoggerFactory import java.awt.Component +import java.awt.Dimension import java.awt.Insets import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.StringSelection @@ -37,6 +42,7 @@ import javax.swing.* import javax.swing.table.DefaultTableCellRenderer import kotlin.collections.ArrayDeque import kotlin.io.path.* +import kotlin.math.max import kotlin.time.Duration.Companion.milliseconds @@ -556,22 +562,6 @@ class FileSystemViewTable( fileSystemViewPanel.newFolderOrFile(text, isFile) } - private fun transfer( - attrs: Array, - fromLocalSystem: Boolean = false, - targetWorkdir: Path? = null - ) { - coroutineScope.launch { - try { - doTransfer(attrs, fromLocalSystem, targetWorkdir) - } catch (e: Exception) { - if (log.isErrorEnabled) { - log.error(e.message, e) - } - } - } - } - private fun deletePaths(paths: Array, rm: Boolean = false) { if (OptionPane.showConfirmDialog( SwingUtilities.getWindowAncestor(this), @@ -657,6 +647,183 @@ class FileSystemViewTable( } + private fun transfer( + attrs: Array, + fromLocalSystem: Boolean = false, + targetWorkdir: Path? = null + ) { + + assertEventDispatchThread() + + val target = sftpPanel.getTarget(table) ?: return + val table = target.getData(SFTPDataProviders.FileSystemViewTable) ?: return + var overwriteAll = false + + for (attr in attrs) { + + if (!overwriteAll) { + val targetAttr = 0.rangeUntil(table.model.rowCount).map { table.model.getAttr(it) } + .find { it.name == attr.name } + if (targetAttr != null) { + val askTransfer = askTransfer(attr, targetAttr) + if (askTransfer.option != JOptionPane.YES_OPTION) { + continue + } + if (askTransfer.action == AskTransfer.Action.Skip) { + if (askTransfer.applyAll) break + continue + } else if (askTransfer.action == AskTransfer.Action.Overwrite) { + overwriteAll = askTransfer.applyAll + } + } + } + + coroutineScope.launch { + try { + doTransfer(arrayOf(attr), fromLocalSystem, targetWorkdir) + } catch (e: Exception) { + if (log.isErrorEnabled) { + log.error(e.message, e) + } + } + } + } + + } + + private data class AskTransfer( + val option: Int, + val action: Action, + val applyAll: Boolean + ) { + enum class Action { + Overwrite, + Skip + } + } + + private fun askTransfer( + sourceAttr: FileSystemViewTableModel.Attr, + targetAttr: FileSystemViewTableModel.Attr + ): AskTransfer { + val formMargin = "7dlu" + val layout = FormLayout( + "left:pref, $formMargin, default:grow, 2dlu, left:pref", + "pref, 12dlu, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, 16dlu, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref" + ) + + val iconSize = 36 + + val targetIcon = if (SystemInfo.isWindows) + NativeFileIcons.getIcon(targetAttr.name, targetAttr.isFile, iconSize, iconSize).first + else if (targetAttr.isDirectory) { + FlatSVGIcon(Icons.folder.name, iconSize, iconSize) + } else { + FlatSVGIcon(Icons.file.name, iconSize, iconSize) + } + + val sourceIcon = if (SystemInfo.isWindows) + NativeFileIcons.getIcon(sourceAttr.name, sourceAttr.isFile, iconSize, iconSize).first + else if (sourceAttr.isDirectory) { + FlatSVGIcon(Icons.folder.name, iconSize, iconSize) + } else { + FlatSVGIcon(Icons.file.name, iconSize, iconSize) + } + + val sourceModified = if (sourceAttr.modified > 0) DateFormatUtils.format( + Date(sourceAttr.modified), + "yyyy/MM/dd HH:mm" + ) else "-" + + val targetModified = if (targetAttr.modified > 0) DateFormatUtils.format( + Date(targetAttr.modified), + "yyyy/MM/dd HH:mm" + ) else "-" + + val actionsComBoBox = JComboBox() + actionsComBoBox.addItem(AskTransfer.Action.Overwrite) + actionsComBoBox.addItem(AskTransfer.Action.Skip) + actionsComBoBox.renderer = object : DefaultListCellRenderer() { + override fun getListCellRendererComponent( + list: JList<*>?, + value: Any?, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component { + var text = value?.toString() ?: StringUtils.EMPTY + if (value == AskTransfer.Action.Overwrite) { + text = I18n.getString("termora.transport.sftp.already-exists.overwrite") + } else if (value == AskTransfer.Action.Skip) { + text = I18n.getString("termora.transport.sftp.already-exists.skip") + } + return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus) + } + } + val applyAllCheckbox = JCheckBox(I18n.getString("termora.transport.sftp.already-exists.apply-all")) + val box = Box.createHorizontalBox() + box.add(actionsComBoBox) + box.add(Box.createHorizontalStrut(8)) + box.add(applyAllCheckbox) + box.add(Box.createHorizontalGlue()) + + val ttBox = Box.createVerticalBox() + ttBox.add(JLabel(I18n.getString("termora.transport.sftp.already-exists.message1"))) + ttBox.add(JLabel(I18n.getString("termora.transport.sftp.already-exists.message2"))) + + val warningIcon = FlatSVGIcon( + Icons.warningIntroduction.name, + iconSize, + iconSize + ) + + var rows = 1 + val step = 2 + val panel = FormBuilder.create().layout(layout) + // tip + .add(JLabel(warningIcon)).xy(1, rows, "center, fill") + .add(ttBox).xyw(3, rows, 3).apply { rows += step } + // name + .add(JLabel("${I18n.getString("termora.transport.sftp.already-exists.name")}:")).xy(1, rows) + .add(sourceAttr.name).xyw(3, rows, 3).apply { rows += step } + // separator + .addSeparator(StringUtils.EMPTY).xyw(1, rows, 5).apply { rows += step } + // Destination + .add("${I18n.getString("termora.transport.sftp.already-exists.destination")}:").xy(1, rows) + .apply { rows += step } + // Folder + .add(JLabel(targetIcon)).xy(1, rows, "center, fill") + .add(targetModified).xyw(3, rows, 3).apply { rows += step } + // Source + .add("${I18n.getString("termora.transport.sftp.already-exists.source")}:").xy(1, rows) + .apply { rows += step } + // Folder + .add(JLabel(sourceIcon)).xy(1, rows, "center, fill") + .add(sourceModified).xyw(3, rows, 3).apply { rows += step } + // separator + .addSeparator(StringUtils.EMPTY).xyw(1, rows, 5).apply { rows += step } + // name + .add(JLabel("${I18n.getString("termora.transport.sftp.already-exists.actions")}:")).xy(1, rows) + .add(box).xyw(3, rows, 3).apply { rows += step } + .build() + panel.putClientProperty("SKIP_requestFocusInWindow", true) + + return AskTransfer( + option = OptionPane.showConfirmDialog( + owner, panel, + messageType = JOptionPane.PLAIN_MESSAGE, + optionType = JOptionPane.OK_CANCEL_OPTION, + title = sourceAttr.name, + initialValue = JOptionPane.YES_OPTION, + ) { + it.size = Dimension(max(UIManager.getInt("Dialog.width") - 220, it.width), it.height) + }, + action = actionsComBoBox.selectedItem as AskTransfer.Action, + applyAll = applyAllCheckbox.isSelected + ) + + } + /** * 开始查找所有子,查找到之后立即添加任务,如果添加失败(任意一个)那么立即终止 diff --git a/src/main/kotlin/app/termora/sftp/NativeFileIcons.kt b/src/main/kotlin/app/termora/sftp/NativeFileIcons.kt index b3de0b5..2386bf8 100644 --- a/src/main/kotlin/app/termora/sftp/NativeFileIcons.kt +++ b/src/main/kotlin/app/termora/sftp/NativeFileIcons.kt @@ -7,6 +7,7 @@ import com.formdev.flatlaf.icons.FlatTreeLeafIcon import com.formdev.flatlaf.util.SystemInfo import org.apache.commons.io.FileUtils import org.apache.commons.io.FilenameUtils +import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.SystemUtils import org.eclipse.jgit.util.LRUMap import java.util.* @@ -24,7 +25,7 @@ object NativeFileIcons { init { if (SystemUtils.IS_OS_UNIX) { - cache[SystemUtils.USER_HOME] = Pair(FlatTreeClosedIcon(), I18n.getString("termora.folder")) + cache[SystemUtils.USER_HOME] = Pair(folderIcon, I18n.getString("termora.folder")) } } @@ -36,35 +37,30 @@ object NativeFileIcons { return getIcon(filename, true).first } - fun getIcon(filename: String, isFile: Boolean = true): Pair { - if (isFile) { - val extension = FilenameUtils.getExtension(filename) - if (cache.containsKey(extension)) { - return cache.getValue(extension) - } - } else { - if (cache.containsKey(SystemUtils.USER_HOME)) { - return cache.getValue(SystemUtils.USER_HOME) - } + fun getIcon(filename: String, isFile: Boolean = true, width: Int = 16, height: Int = 16): Pair { + val key = if (isFile) FilenameUtils.getExtension(filename) + "." + width + "@" + height + else SystemUtils.USER_HOME + "." + width + "@" + height + + if (cache.containsKey(key)) { + return cache.getValue(key) } val isDirectory = !isFile if (SystemInfo.isWindows) { + val file = if (isDirectory) FileUtils.getFile(SystemUtils.USER_HOME) else FileUtils.getFile(Application.getTemporaryDir(), "${UUID.randomUUID()}.${filename}") if (isFile && !file.exists()) { file.createNewFile() } - val icon = getFileSystemView().getSystemIcon(file, 16, 16) + + val icon = getFileSystemView().getSystemIcon(file, width, height) ?: if (isFile) fileIcon else folderIcon val description = getFileSystemView().getSystemTypeDescription(file) + ?: StringUtils.defaultString(file.extension) val pair = icon to description - if (isDirectory) { - cache[SystemUtils.USER_HOME] = pair - } else { - cache[FilenameUtils.getExtension(file.name)] = pair - } + cache[key] = pair if (isFile) FileUtils.deleteQuietly(file) diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index d779a19..84ea044 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -314,6 +314,16 @@ termora.transport.sftp.status.waiting=Waiting termora.transport.sftp.status.done=Done termora.transport.sftp.status.failed=Failed +termora.transport.sftp.already-exists.message1=This folder already contains an object named as below +termora.transport.sftp.already-exists.message2=Select a task to do +termora.transport.sftp.already-exists.overwrite=Overwrite +termora.transport.sftp.already-exists.skip=Skip +termora.transport.sftp.already-exists.apply-all=Apply all +termora.transport.sftp.already-exists.name=Name +termora.transport.sftp.already-exists.destination=Destination +termora.transport.sftp.already-exists.source=Source +termora.transport.sftp.already-exists.actions=Actions + # transport job termora.transport.jobs.table.name=Name diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index 44ff742..3f42713 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -293,6 +293,19 @@ termora.transport.sftp.status.done=已完成 termora.transport.sftp.status.failed=已失败 +termora.transport.sftp.already-exists.message1=此文件夹已包含一下名称的对象 +termora.transport.sftp.already-exists.message2=请选择要执行的操作 +termora.transport.sftp.already-exists.overwrite=覆盖 +termora.transport.sftp.already-exists.skip=跳过 +termora.transport.sftp.already-exists.apply-all=应用全部 +termora.transport.sftp.already-exists.name=名称 +termora.transport.sftp.already-exists.destination=目标文件 +termora.transport.sftp.already-exists.source=源文件 +termora.transport.sftp.already-exists.actions=操作 + + + + # Permission termora.transport.permissions=更改权限 termora.transport.permissions.file-folder-permissions=文件/文件夹权限 diff --git a/src/main/resources/i18n/messages_zh_TW.properties b/src/main/resources/i18n/messages_zh_TW.properties index 4bd1f62..fadf22a 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -287,6 +287,16 @@ termora.transport.sftp.status.waiting=等待中 termora.transport.sftp.status.done=已完成 termora.transport.sftp.status.failed=已失敗 +termora.transport.sftp.already-exists.message1=此資料夾已包含一下名稱的對象 +termora.transport.sftp.already-exists.message2=請選擇要執行的操作 +termora.transport.sftp.already-exists.overwrite=覆蓋 +termora.transport.sftp.already-exists.skip=跳過 +termora.transport.sftp.already-exists.apply-all=應用全部 +termora.transport.sftp.already-exists.name=名稱 +termora.transport.sftp.already-exists.destination=目標文件 +termora.transport.sftp.already-exists.source=原始檔 +termora.transport.sftp.already-exists.actions=操作 + # transport job termora.transport.jobs.table.name=名稱 termora.transport.jobs.table.status=狀態 diff --git a/src/main/resources/icons/file.svg b/src/main/resources/icons/file.svg new file mode 100644 index 0000000..054b75c --- /dev/null +++ b/src/main/resources/icons/file.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/icons/file_dark.svg b/src/main/resources/icons/file_dark.svg new file mode 100644 index 0000000..2aed2d4 --- /dev/null +++ b/src/main/resources/icons/file_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file