mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02:58 +08:00
feat: transfer support disconnection and reconnection
This commit is contained in:
@@ -22,10 +22,7 @@ import org.apache.commons.lang3.LocaleUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import org.json.JSONObject
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.MenuItem
|
||||
import java.awt.PopupMenu
|
||||
import java.awt.SystemTray
|
||||
import java.awt.TrayIcon
|
||||
import java.awt.*
|
||||
import java.awt.desktop.AppReopenedEvent
|
||||
import java.awt.desktop.AppReopenedListener
|
||||
import java.awt.desktop.SystemEventListener
|
||||
@@ -202,6 +199,7 @@ class ApplicationRunner {
|
||||
|
||||
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
|
||||
UIManager.put(FlatClientProperties.POPUP_FORCE_HEAVY_WEIGHT, true)
|
||||
|
||||
UIManager.put("Component.arc", 5)
|
||||
UIManager.put("TextComponent.arc", UIManager.getInt("Component.arc"))
|
||||
@@ -237,12 +235,24 @@ class ApplicationRunner {
|
||||
|
||||
// Linux 更多的是尖锐风格
|
||||
if (SystemInfo.isMacOS || SystemInfo.isWindows) {
|
||||
val selectionInsets = Insets(0, 2, 0, 2)
|
||||
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("Tree.selectionInsets", selectionInsets)
|
||||
|
||||
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("List.selectionInsets", selectionInsets)
|
||||
|
||||
UIManager.put("ComboBox.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("ComboBox.selectionInsets", selectionInsets)
|
||||
|
||||
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("Table.selectionInsets", selectionInsets)
|
||||
|
||||
UIManager.put("MenuBar.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("MenuBar.selectionInsets", selectionInsets)
|
||||
|
||||
UIManager.put("MenuItem.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("MenuItem.selectionInsets", selectionInsets)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ object Icons {
|
||||
val empty by lazy { DynamicIcon("icons/empty.svg") }
|
||||
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.svg") }
|
||||
val add by lazy { DynamicIcon("icons/add.svg", "icons/add_dark.svg") }
|
||||
val breakpoint by lazy { DynamicIcon("icons/breakpoint.svg", "icons/breakpoint_dark.svg") }
|
||||
val softWrap by lazy { DynamicIcon("icons/softWrap.svg", "icons/softWrap_dark.svg") }
|
||||
val scrollUp by lazy { DynamicIcon("icons/scrollUp.svg", "icons/scrollUp_dark.svg") }
|
||||
val reformatCode by lazy { DynamicIcon("icons/reformatCode.svg", "icons/reformatCode_dark.svg") }
|
||||
|
||||
@@ -99,9 +99,11 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
||||
if (key == DataKey.CurrentDir) {
|
||||
val dir = DataKey.CurrentDir.clazz.cast(data)
|
||||
val navigator = getTransportNavigator() ?: return
|
||||
val path = navigator.getFileSystem().getPath(dir)
|
||||
if (path == navigator.workdir) return
|
||||
navigator.navigateTo(path)
|
||||
val loader = navigator.loader
|
||||
if (loader.isOpened().not()) return
|
||||
val fileSystem = loader.getSyncTransportSupport().getFileSystem()
|
||||
val path = fileSystem.getPath(dir)
|
||||
navigator.navigateTo(path.absolutePathString())
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -146,14 +148,25 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
|
||||
try {
|
||||
val session = getSession()
|
||||
val fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
||||
val support = TransportSupport(fileSystem, fileSystem.defaultDir.absolutePathString())
|
||||
val support = DefaultTransportSupport(fileSystem, fileSystem.defaultDir)
|
||||
withContext(Dispatchers.Swing) {
|
||||
val internalTransferManager = MyInternalTransferManager()
|
||||
val transportPanel = TransportPanel(
|
||||
internalTransferManager, tab.host,
|
||||
TransportSupportLoader { support })
|
||||
internalTransferManager.setTransferPanel(transportPanel)
|
||||
object : TransportSupportLoader {
|
||||
override suspend fun getTransportSupport(): TransportSupport {
|
||||
return support
|
||||
}
|
||||
|
||||
override fun getSyncTransportSupport(): TransportSupport {
|
||||
return support
|
||||
}
|
||||
|
||||
override fun isLoaded(): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
internalTransferManager.setTransferPanel(transportPanel)
|
||||
Disposer.register(transportPanel, object : Disposable {
|
||||
override fun dispose() {
|
||||
panel.remove(transportPanel)
|
||||
|
||||
@@ -65,7 +65,9 @@ class DefaultInternalTransferManager(
|
||||
|
||||
|
||||
override fun canTransfer(paths: List<Path>): Boolean {
|
||||
return paths.isNotEmpty() && target.getWorkdir() != null
|
||||
val c = target.getWorkdir() ?: return false
|
||||
if (c.fileSystem.isOpen.not()) return false
|
||||
return paths.isNotEmpty()
|
||||
}
|
||||
|
||||
override fun addTransfer(
|
||||
@@ -270,7 +272,8 @@ class DefaultInternalTransferManager(
|
||||
val isDirectory = pair.second.isDirectory
|
||||
val path = pair.first
|
||||
if (isDirectory.not() || mode == TransferMode.Rmrf) {
|
||||
val transfer = createTransfer(path, workdir.resolve(path.name), isDirectory, StringUtils.EMPTY, mode, action)
|
||||
val transfer =
|
||||
createTransfer(path, workdir.resolve(path.name), isDirectory, StringUtils.EMPTY, mode, action)
|
||||
return if (transferManager.addTransfer(transfer)) FileVisitResult.CONTINUE else FileVisitResult.TERMINATE
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.termora.transfer
|
||||
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Path
|
||||
|
||||
class DefaultTransportSupport(private val fileSystem: FileSystem, private val defaultPath: Path) : TransportSupport {
|
||||
override fun getFileSystem(): FileSystem {
|
||||
return fileSystem
|
||||
}
|
||||
|
||||
override fun getDefaultPath(): Path {
|
||||
return defaultPath
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package app.termora.transfer
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.protocol.PathHandler
|
||||
import app.termora.protocol.PathHandlerRequest
|
||||
import app.termora.protocol.TransferProtocolProvider
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.Window
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
internal class ReconnectableTransportSupportLoader(private val owner: Window, private val host: Host) :
|
||||
TransportSupportLoader {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(ReconnectableTransportSupportLoader::class.java)
|
||||
}
|
||||
|
||||
private val mutex = Mutex()
|
||||
private val reference = AtomicReference<MyTransportSupport>()
|
||||
|
||||
private var support: MyTransportSupport?
|
||||
set(value) = reference.set(value)
|
||||
get() = reference.get()
|
||||
|
||||
override suspend fun getTransportSupport(): TransportSupport {
|
||||
mutex.withLock {
|
||||
var c = support
|
||||
if (c != null) {
|
||||
if (c.getFileSystem().isOpen) {
|
||||
return c
|
||||
}
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn("Host {} has been disconnected and will reconnect soon", host.name)
|
||||
}
|
||||
support = null
|
||||
Disposer.dispose(c)
|
||||
}
|
||||
c = connect().also { support = it }
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSyncTransportSupport(): TransportSupport {
|
||||
assertEventDispatchThread()
|
||||
val c = support
|
||||
if (c == null) throw IllegalStateException("No transport support")
|
||||
return c
|
||||
}
|
||||
|
||||
override fun isLoaded(): Boolean {
|
||||
assertEventDispatchThread()
|
||||
return support != null
|
||||
}
|
||||
|
||||
override fun isOpened(): Boolean {
|
||||
if (isLoaded().not()) return false
|
||||
val c = support ?: return false
|
||||
return c.getFileSystem().isOpen
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
val c = support
|
||||
if (c != null) {
|
||||
Disposer.dispose(c)
|
||||
}
|
||||
}
|
||||
|
||||
private fun connect(): MyTransportSupport {
|
||||
val provider = TransferProtocolProvider.valueOf(host.protocol)
|
||||
if (provider == null) {
|
||||
throw IllegalStateException(I18n.getString("termora.protocol.not-supported", host.protocol))
|
||||
}
|
||||
val handler = provider.createPathHandler(PathHandlerRequest(host, owner))
|
||||
return MyTransportSupport(handler)
|
||||
}
|
||||
|
||||
|
||||
private inner class MyTransportSupport(private val handler: PathHandler) : TransportSupport, Disposable {
|
||||
|
||||
init {
|
||||
Disposer.register(this, handler)
|
||||
}
|
||||
|
||||
override fun getFileSystem(): FileSystem {
|
||||
return handler.fileSystem
|
||||
}
|
||||
|
||||
override fun getDefaultPath(): Path {
|
||||
return handler.path
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -398,7 +398,9 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) :
|
||||
if (continueTransfer(node, false)) {
|
||||
doTransfer(node)
|
||||
} else {
|
||||
changeState(node, State.Failed)
|
||||
withContext(Dispatchers.Swing) {
|
||||
changeState(node, State.Failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
lock.withLock { condition.signalAll() }
|
||||
|
||||
@@ -2,7 +2,6 @@ package app.termora.transfer
|
||||
|
||||
import app.termora.DynamicColor
|
||||
import app.termora.Icons
|
||||
import app.termora.OptionPane
|
||||
import app.termora.transfer.TransportPanel.Companion.isWindowsFileSystem
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||
@@ -14,8 +13,6 @@ 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
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.CardLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Insets
|
||||
@@ -24,7 +21,6 @@ import java.awt.event.*
|
||||
import java.beans.PropertyChangeEvent
|
||||
import java.beans.PropertyChangeListener
|
||||
import java.nio.file.Path
|
||||
import java.util.function.Supplier
|
||||
import javax.swing.*
|
||||
import javax.swing.event.PopupMenuEvent
|
||||
import javax.swing.event.PopupMenuListener
|
||||
@@ -33,13 +29,9 @@ import kotlin.io.path.name
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.math.round
|
||||
|
||||
class TransportNavigationPanel(
|
||||
private val support: Supplier<TransportSupport>,
|
||||
private val navigator: TransportNavigator
|
||||
) : JPanel() {
|
||||
internal class TransportNavigationPanel(private val navigator: TransportNavigator) : JPanel() {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(TransportNavigationPanel::class.java)
|
||||
private const val TEXT_FIELD = "TextField"
|
||||
private const val SEGMENTS = "Segments"
|
||||
|
||||
@@ -50,7 +42,6 @@ class TransportNavigationPanel(
|
||||
|
||||
}
|
||||
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||
private val layeredPane = LayeredPane()
|
||||
private val textField = FlatTextField()
|
||||
private val downBtn = JButton(Icons.chevronDown)
|
||||
@@ -115,8 +106,7 @@ class TransportNavigationPanel(
|
||||
val itemListener = object : ItemListener {
|
||||
override fun itemStateChanged(e: ItemEvent) {
|
||||
val path = comboBox.selectedItem as Path? ?: return
|
||||
if (navigator.loading) return
|
||||
navigator.navigateTo(path)
|
||||
navigator.navigateTo(path.absolutePathString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,17 +169,7 @@ class TransportNavigationPanel(
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
if (navigator.loading) return
|
||||
if (textField.text.isBlank()) return
|
||||
|
||||
try {
|
||||
val path = support.get().fileSystem.getPath(textField.text)
|
||||
navigator.navigateTo(path)
|
||||
} catch (e: Exception) {
|
||||
if (log.isErrorEnabled) log.error(e.message, e)
|
||||
OptionPane.showMessageDialog(
|
||||
owner, ExceptionUtils.getRootCauseMessage(e),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
navigator.navigateTo(textField.text)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -261,7 +241,7 @@ class TransportNavigationPanel(
|
||||
if (path == navigator.workdir) {
|
||||
setTextFieldText(path)
|
||||
} else {
|
||||
navigator.navigateTo(path)
|
||||
navigator.navigateTo(path.absolutePathString())
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -353,7 +333,7 @@ class TransportNavigationPanel(
|
||||
text = item.pathString
|
||||
}
|
||||
}
|
||||
popupMenu.add(text).addActionListener { navigator.navigateTo(item) }
|
||||
popupMenu.add(text).addActionListener { navigator.navigateTo(item.absolutePathString()) }
|
||||
}
|
||||
popupMenu.show(
|
||||
button,
|
||||
|
||||
@@ -8,7 +8,7 @@ interface TransportNavigator {
|
||||
val loading: Boolean
|
||||
val workdir: Path?
|
||||
|
||||
fun navigateTo(destination: Path): Boolean
|
||||
fun navigateTo(destination: String): Boolean
|
||||
|
||||
fun addPropertyChangeListener(propertyName: String, listener: PropertyChangeListener)
|
||||
fun removePropertyChangeListener(propertyName: String, listener: PropertyChangeListener)
|
||||
|
||||
@@ -59,9 +59,9 @@ import kotlin.io.path.*
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class TransportPanel(
|
||||
internal class TransportPanel(
|
||||
private val transferManager: InternalTransferManager,
|
||||
var host: Host,
|
||||
val host: Host,
|
||||
val loader: TransportSupportLoader,
|
||||
) : JPanel(BorderLayout()), DataProvider, Disposable, TransportNavigator {
|
||||
companion object {
|
||||
@@ -118,9 +118,6 @@ class TransportPanel(
|
||||
private val disposed = AtomicBoolean(false)
|
||||
private val futures = Collections.synchronizedSet(mutableSetOf<Future<*>>())
|
||||
|
||||
private val _fileSystem by lazy { getSupport().fileSystem }
|
||||
private val defaultPath by lazy { getSupport().path }
|
||||
|
||||
|
||||
/**
|
||||
* 工作目录
|
||||
@@ -154,7 +151,7 @@ class TransportPanel(
|
||||
toolbar.add(prevBtn)
|
||||
toolbar.add(homeBtn)
|
||||
toolbar.add(nextBtn)
|
||||
toolbar.add(TransportNavigationPanel(loader, this))
|
||||
toolbar.add(TransportNavigationPanel(this))
|
||||
toolbar.add(bookmarkBtn)
|
||||
toolbar.add(parentBtn)
|
||||
toolbar.add(eyeBtn)
|
||||
@@ -235,7 +232,7 @@ class TransportPanel(
|
||||
|
||||
Disposer.register(this, editTransferListener)
|
||||
|
||||
refreshBtn.addActionListener { reload() }
|
||||
refreshBtn.addActionListener { reload(requestFocus = true) }
|
||||
|
||||
prevBtn.addActionListener { navigator.back() }
|
||||
nextBtn.addActionListener { navigator.forward() }
|
||||
@@ -243,7 +240,7 @@ class TransportPanel(
|
||||
parentBtn.addActionListener(createSmartAction(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
if (hasParent.not()) return
|
||||
navigator.navigateTo(model.getPath(0))
|
||||
reload(newPath = model.getPath(0).absolutePathString(), requestFocus = true)
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -258,14 +255,16 @@ class TransportPanel(
|
||||
}
|
||||
bookmarkBtn.isBookmark = bookmarkBtn.isBookmark.not()
|
||||
} else {
|
||||
navigateTo(_fileSystem.getPath(e.actionCommand))
|
||||
navigateTo(e.actionCommand)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
homeBtn.addActionListener(createSmartAction(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
navigator.navigateTo(_fileSystem.getPath(defaultPath))
|
||||
if (loader.isLoaded()) {
|
||||
navigator.navigateTo(loader.getSyncTransportSupport().getDefaultPath().absolutePathString())
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -273,7 +272,7 @@ class TransportPanel(
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
showHiddenFiles = showHiddenFiles.not()
|
||||
eyeBtn.icon = if (showHiddenFiles) Icons.eye else Icons.eyeClose
|
||||
reload()
|
||||
reload(requestFocus = true)
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -289,8 +288,11 @@ class TransportPanel(
|
||||
transferManager.addTransferListener(object : TransferListener {
|
||||
override fun onTransferChanged(transfer: Transfer, state: TransferTreeTableNode.State) {
|
||||
if (state != TransferTreeTableNode.State.Done && state != TransferTreeTableNode.State.Failed) return
|
||||
if (transfer.target().fileSystem != _fileSystem) return
|
||||
if (transfer.target() == workdir || transfer.target().parent == workdir) {
|
||||
val target = transfer.target()
|
||||
if (loader.isLoaded()) {
|
||||
if (target.fileSystem != loader.getSyncTransportSupport().getFileSystem()) return
|
||||
}
|
||||
if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) {
|
||||
reload(requestFocus = false)
|
||||
}
|
||||
}
|
||||
@@ -362,7 +364,7 @@ class TransportPanel(
|
||||
undoManager.addEdit(object : AbstractUndoableEdit() {
|
||||
override fun undo() {
|
||||
super.undo()
|
||||
if (navigator.navigateTo(oldValue)) {
|
||||
if (navigator.reload(newPath = oldValue.absolutePathString(), requestFocus = true)) {
|
||||
undoOrRedo = true
|
||||
undoOrRedoPath = oldValue
|
||||
}
|
||||
@@ -370,7 +372,7 @@ class TransportPanel(
|
||||
|
||||
override fun redo() {
|
||||
super.redo()
|
||||
if (navigator.navigateTo(newValue)) {
|
||||
if (navigator.reload(newPath = newValue.absolutePathString(), requestFocus = true)) {
|
||||
undoOrRedo = true
|
||||
undoOrRedoPath = newValue
|
||||
}
|
||||
@@ -431,10 +433,10 @@ class TransportPanel(
|
||||
if (attributes.isDirectory) {
|
||||
enterSelectionFolder()
|
||||
} else {
|
||||
transferManager.addTransfer(
|
||||
listOf(model.getPath(row) to attributes),
|
||||
InternalTransferManager.TransferMode.Transfer
|
||||
)
|
||||
val paths = listOf(model.getPath(row) to attributes)
|
||||
if (loader.isOpened() && transferManager.canTransfer(paths.map { it.first })) {
|
||||
transferManager.addTransfer(paths, InternalTransferManager.TransferMode.Transfer)
|
||||
}
|
||||
}
|
||||
} else if (SwingUtilities.isRightMouseButton(e)) {
|
||||
val r = table.rowAtPoint(e.point)
|
||||
@@ -524,7 +526,6 @@ class TransportPanel(
|
||||
}
|
||||
|
||||
private fun getTransferData(support: TransferSupport, load: Boolean): TransferData? {
|
||||
if (loader.isLoaded.not()) return null
|
||||
val workdir = workdir ?: return null
|
||||
val dropLocation = support.dropLocation as? JTable.DropLocation ?: return null
|
||||
val row = if (dropLocation.isInsertRow) 0 else sorter.convertRowIndexToModel(dropLocation.row)
|
||||
@@ -540,7 +541,8 @@ class TransportPanel(
|
||||
if (transferTransferable.component == panel) return null
|
||||
paths.addAll(transferTransferable.files)
|
||||
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||
if (_fileSystem.isLocallyFileSystem()) return null
|
||||
if (loader.isLoaded() && loader.getSyncTransportSupport().getFileSystem().isLocallyFileSystem())
|
||||
return null
|
||||
if (load) {
|
||||
val files = support.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*>
|
||||
if (files.isEmpty()) return null
|
||||
@@ -577,22 +579,12 @@ class TransportPanel(
|
||||
|
||||
}
|
||||
|
||||
fun getTableModel(): TransportTableModel {
|
||||
return model
|
||||
private suspend fun getFileSystem(): FileSystem {
|
||||
return loader.getTransportSupport().getFileSystem()
|
||||
}
|
||||
|
||||
fun getFileSystem(): FileSystem {
|
||||
return _fileSystem
|
||||
}
|
||||
|
||||
/**
|
||||
* 不能在 EDT 线程调用
|
||||
*/
|
||||
private fun getSupport(): TransportSupport {
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
throw WrongThreadException("AWT EventQueue")
|
||||
}
|
||||
return loader.get()
|
||||
private suspend fun getTransportSupport(): TransportSupport {
|
||||
return loader.getTransportSupport()
|
||||
}
|
||||
|
||||
private fun enterSelectionFolder() {
|
||||
@@ -609,7 +601,7 @@ class TransportPanel(
|
||||
if (workdir != null) registerSelectRow(workdir.name)
|
||||
}
|
||||
|
||||
navigator.navigateTo(path)
|
||||
navigator.navigateTo(path.absolutePathString())
|
||||
}
|
||||
|
||||
private fun registerSelectRow(name: String) {
|
||||
@@ -626,7 +618,11 @@ class TransportPanel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun reload(oldPath: Path? = workdir, newPath: Path? = workdir, requestFocus: Boolean = false): Boolean {
|
||||
fun reload(
|
||||
oldPath: String? = workdir?.absolutePathString(),
|
||||
newPath: String? = workdir?.absolutePathString(),
|
||||
requestFocus: Boolean = false
|
||||
): Boolean {
|
||||
assertEventDispatchThread()
|
||||
|
||||
if (loading) return false
|
||||
@@ -662,20 +658,26 @@ class TransportPanel(
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun doReload(oldPath: Path? = null, newPath: Path? = null, requestFocus: Boolean = false): Path {
|
||||
private suspend fun doReload(
|
||||
oldPath: String? = null,
|
||||
newPath: String? = null,
|
||||
requestFocus: Boolean = false
|
||||
): Path {
|
||||
|
||||
val support = getTransportSupport()
|
||||
val fileSystem = support.getFileSystem()
|
||||
val workdir = newPath ?: oldPath
|
||||
|
||||
if (workdir == null) {
|
||||
val path = _fileSystem.getPath(defaultPath)
|
||||
return doReload(null, path)
|
||||
val path = support.getDefaultPath()
|
||||
return doReload(null, path.absolutePathString())
|
||||
}
|
||||
|
||||
val path = workdir
|
||||
val path = fileSystem.getPath(workdir)
|
||||
val first = AtomicBoolean(false)
|
||||
var parent = path.parent
|
||||
if (parent == null && _fileSystem.isWindowsFileSystem() && workdir.pathString != _fileSystem.separator) {
|
||||
parent = _fileSystem.getPath(_fileSystem.separator)
|
||||
if (parent == null && fileSystem.isWindowsFileSystem() && path.pathString != fileSystem.separator) {
|
||||
parent = fileSystem.getPath(fileSystem.separator)
|
||||
}
|
||||
val files = mutableListOf<Pair<Path, Attributes>>()
|
||||
if ((parent != null).also { hasParent = it }) {
|
||||
@@ -696,8 +698,8 @@ class TransportPanel(
|
||||
files.clear()
|
||||
}
|
||||
|
||||
if (_fileSystem.isWindowsFileSystem() && workdir.pathString == _fileSystem.separator) {
|
||||
for (path in _fileSystem.rootDirectories) {
|
||||
if (fileSystem.isWindowsFileSystem() && path.pathString == fileSystem.separator) {
|
||||
for (path in fileSystem.rootDirectories) {
|
||||
val attributes = getAttributes(path)
|
||||
files.add(path to attributes)
|
||||
}
|
||||
@@ -716,7 +718,7 @@ class TransportPanel(
|
||||
if (requestFocus)
|
||||
coroutineScope.launch(Dispatchers.Swing) { table.requestFocusInWindow() }
|
||||
|
||||
return workdir
|
||||
return path
|
||||
}
|
||||
|
||||
private fun listFiles(path: Path): Stream<Pair<Path, Attributes>> {
|
||||
@@ -787,21 +789,24 @@ 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, _fileSystem, files)
|
||||
val popupMenu = TransportPopupMenu(owner, model, transferManager, loader, files)
|
||||
popupMenu.addActionListener(PopupMenuActionListener(files))
|
||||
popupMenu.show(table, e.x, e.y)
|
||||
}
|
||||
|
||||
override fun navigateTo(destination: Path): Boolean {
|
||||
|
||||
override fun navigateTo(destination: String): Boolean {
|
||||
assertEventDispatchThread()
|
||||
|
||||
if (loading) return false
|
||||
if (workdir == destination) return false
|
||||
|
||||
return reload(workdir, destination)
|
||||
if (loader.isOpened()) {
|
||||
if (workdir?.absolutePathString() == destination) return false
|
||||
}
|
||||
|
||||
return reload(newPath = destination)
|
||||
}
|
||||
|
||||
override fun getHistory(): List<Path> {
|
||||
@@ -825,7 +830,7 @@ class TransportPanel(
|
||||
}
|
||||
|
||||
private fun setNewWorkdir(destination: Path) {
|
||||
val oldValue = workdir
|
||||
val oldValue = if (destination.fileSystem == workdir?.fileSystem) workdir else null
|
||||
workdir = destination
|
||||
firePropertyChange("workdir", oldValue, destination)
|
||||
}
|
||||
@@ -912,8 +917,12 @@ class TransportPanel(
|
||||
val millis = Files.getLastModifiedTime(localPath).toMillis()
|
||||
if (oldMillis == millis) continue
|
||||
|
||||
// 正在编辑时可能会出现断线的情况 ,安全获取
|
||||
val fs = getFileSystem()
|
||||
if (fs.isOpen.not()) continue
|
||||
|
||||
// 发送到服务器
|
||||
transferManager.addHighTransfer(localPath, target)
|
||||
transferManager.addHighTransfer(localPath, fs.getPath(target.absolutePathString()))
|
||||
oldMillis = millis
|
||||
}
|
||||
}
|
||||
@@ -1009,8 +1018,9 @@ class TransportPanel(
|
||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.NewFolder || actionCommand == TransportPopupMenu.ActionCommand.NewFile) {
|
||||
val name = e.source.toString()
|
||||
val workdir = workdir ?: return
|
||||
val path = workdir.resolve(name)
|
||||
processPath(e.source.toString()) {
|
||||
// 因为此时可能已经断线,任何 Path 都不可完全相信
|
||||
val path = getFileSystem().getPath(workdir.resolve(name).absolutePathString())
|
||||
if (actionCommand == TransportPopupMenu.ActionCommand.NewFolder)
|
||||
path.createDirectories()
|
||||
else
|
||||
@@ -1022,6 +1032,9 @@ class TransportPanel(
|
||||
processPath(e.source.toString()) { source.moveTo(target) }
|
||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.Rmrf) {
|
||||
transferManager.addTransfer(files, InternalTransferManager.TransferMode.Rmrf)
|
||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.Reconnect) {
|
||||
// reload now
|
||||
reload()
|
||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.ChangePermissions) {
|
||||
val c = e.source as TransportPopupMenu.ChangePermission
|
||||
val path = files.first().first
|
||||
@@ -1104,7 +1117,7 @@ class TransportPanel(
|
||||
|
||||
private inner class MyDefaultTableCellRenderer : DefaultTableCellRenderer() {
|
||||
override fun getTableCellRendererComponent(
|
||||
table: JTable?,
|
||||
table: JTable,
|
||||
value: Any?,
|
||||
isSelected: Boolean,
|
||||
hasFocus: Boolean,
|
||||
@@ -1133,12 +1146,13 @@ class TransportPanel(
|
||||
text = StringUtils.EMPTY
|
||||
}
|
||||
|
||||
foreground = null
|
||||
val c = super.getTableCellRendererComponent(table, text, isSelected, hasFocus, row, column)
|
||||
icon = null
|
||||
|
||||
if (column == TransportTableModel.COLUMN_NAME) {
|
||||
if (_fileSystem.isWindowsFileSystem()) {
|
||||
val path = model.getPath(sorter.convertRowIndexToModel(row))
|
||||
val path = model.getPath(sorter.convertRowIndexToModel(row))
|
||||
if (path.fileSystem.isWindowsFileSystem()) {
|
||||
icon = if (attributes.isParent) {
|
||||
NativeIcons.folderIcon
|
||||
} else {
|
||||
@@ -1165,6 +1179,12 @@ class TransportPanel(
|
||||
}
|
||||
}
|
||||
|
||||
if (loader.isOpened().not()) {
|
||||
if (isSelected.not()) {
|
||||
foreground = UIManager.getColor("textInactiveText")
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import app.termora.Icons
|
||||
import app.termora.OptionPane
|
||||
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
||||
import java.awt.Window
|
||||
@@ -13,7 +14,6 @@ import java.awt.datatransfer.StringSelection
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.ActionListener
|
||||
import java.awt.event.KeyEvent
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.attribute.PosixFilePermission
|
||||
import java.util.*
|
||||
@@ -26,11 +26,11 @@ import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.name
|
||||
|
||||
|
||||
class TransportPopupMenu(
|
||||
internal class TransportPopupMenu(
|
||||
private val owner: Window,
|
||||
private val model: TransportTableModel,
|
||||
private val transferManager: InternalTransferManager,
|
||||
private val fileSystem: FileSystem,
|
||||
private val loader: TransportSupportLoader,
|
||||
private val files: List<Pair<Path, TransportTableModel.Attributes>>
|
||||
) : FlatPopupMenu() {
|
||||
private val paths = files.map { it.first }
|
||||
@@ -71,25 +71,45 @@ class TransportPopupMenu(
|
||||
private fun initView() {
|
||||
inheritsPopupMenu = false
|
||||
|
||||
if (loader.isOpened().not()) {
|
||||
val reconnect = add(I18n.getString("termora.tabbed.contextmenu.reconnect"))
|
||||
reconnect.addActionListener { e -> fireActionPerformed(e, ActionCommand.Reconnect) }
|
||||
return
|
||||
}
|
||||
|
||||
val fileSystem = if (loader.isLoaded()) loader.getSyncTransportSupport().getFileSystem() else null
|
||||
|
||||
add(transferMenu)
|
||||
add(editMenu)
|
||||
addSeparator()
|
||||
add(copyPathMenu)
|
||||
if (fileSystem.isLocallyFileSystem()) add(openInFinderMenu)
|
||||
if (fileSystem?.isLocallyFileSystem() == true) {
|
||||
add(openInFinderMenu)
|
||||
}
|
||||
addSeparator()
|
||||
add(renameMenu)
|
||||
add(deleteMenu)
|
||||
if (fileSystem is SftpFileSystem) add(rmrfMenu)
|
||||
if (fileSystem is SftpFileSystem) {
|
||||
add(rmrfMenu)
|
||||
}
|
||||
add(changePermissionsMenu)
|
||||
addSeparator()
|
||||
add(refreshMenu)
|
||||
addSeparator()
|
||||
add(newMenu)
|
||||
|
||||
// 开发环境提供断线
|
||||
if (Application.getAppPath().isBlank() && loader.isOpened()) {
|
||||
addSeparator()
|
||||
add("Disconnect").addActionListener {
|
||||
IOUtils.closeQuietly(loader.getSyncTransportSupport().getFileSystem())
|
||||
}
|
||||
}
|
||||
|
||||
transferMenu.isEnabled = hasParent.not() && files.isNotEmpty() && transferManager.canTransfer(paths)
|
||||
copyPathMenu.isEnabled = files.isNotEmpty()
|
||||
openInFinderMenu.isEnabled = files.isNotEmpty() && fileSystem.isLocallyFileSystem()
|
||||
editMenu.isEnabled = files.isNotEmpty() && fileSystem.isLocallyFileSystem().not()
|
||||
openInFinderMenu.isEnabled = files.isNotEmpty() && fileSystem?.isLocallyFileSystem() == true
|
||||
editMenu.isEnabled = files.isNotEmpty() && fileSystem?.isLocallyFileSystem() != true
|
||||
&& files.all { it.second.isFile && it.second.isSymbolicLink.not() }
|
||||
renameMenu.isEnabled = hasParent.not() && files.size == 1
|
||||
deleteMenu.isEnabled = hasParent.not() && files.isNotEmpty()
|
||||
@@ -211,6 +231,7 @@ class TransportPopupMenu(
|
||||
Refresh,
|
||||
ChangePermissions,
|
||||
Rmrf,
|
||||
Reconnect,
|
||||
}
|
||||
|
||||
data class ChangePermission(val permissions: Set<PosixFilePermission>, val includeSubFolder: Boolean)
|
||||
|
||||
@@ -2,7 +2,6 @@ package app.termora.transfer
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.protocol.PathHandlerRequest
|
||||
import app.termora.protocol.TransferProtocolProvider
|
||||
import app.termora.tree.*
|
||||
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
|
||||
@@ -24,9 +23,8 @@ import java.util.concurrent.Executors
|
||||
import javax.swing.*
|
||||
import javax.swing.event.TreeExpansionEvent
|
||||
import javax.swing.event.TreeExpansionListener
|
||||
import kotlin.io.path.absolutePathString
|
||||
|
||||
class TransportSelectionPanel(
|
||||
internal class TransportSelectionPanel(
|
||||
private val tabbed: TransportTabbed,
|
||||
private val transferManager: InternalTransferManager,
|
||||
) : JPanel(BorderLayout()), Disposable {
|
||||
@@ -99,19 +97,16 @@ class TransportSelectionPanel(
|
||||
|
||||
private suspend fun doConnect(host: Host) {
|
||||
|
||||
val provider = TransferProtocolProvider.valueOf(host.protocol)
|
||||
if (provider == null) {
|
||||
throw IllegalStateException(I18n.getString("termora.protocol.not-supported", host.protocol))
|
||||
}
|
||||
val loader = ReconnectableTransportSupportLoader(owner, host)
|
||||
|
||||
val handler = provider.createPathHandler(PathHandlerRequest(host, owner))
|
||||
val support = TransportSupport(handler.fileSystem, handler.path.absolutePathString())
|
||||
// try load
|
||||
loader.getTransportSupport()
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
val panel = TransportPanel(transferManager, host, TransportSupportLoader { support })
|
||||
val panel = TransportPanel(transferManager, host, loader)
|
||||
Disposer.register(panel, object : Disposable {
|
||||
override fun dispose() {
|
||||
Disposer.dispose(handler)
|
||||
Disposer.dispose(loader)
|
||||
}
|
||||
})
|
||||
swingCoroutineScope.launch {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package app.termora.transfer
|
||||
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
class TransportSupport(
|
||||
val fileSystem: FileSystem,
|
||||
val path: String
|
||||
)
|
||||
internal interface TransportSupport {
|
||||
fun getFileSystem(): FileSystem
|
||||
fun getDefaultPath(): Path
|
||||
}
|
||||
@@ -1,52 +1,26 @@
|
||||
package app.termora.transfer
|
||||
|
||||
import okio.withLock
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import java.util.function.Supplier
|
||||
import app.termora.Disposable
|
||||
|
||||
class TransportSupportLoader(private val support: Supplier<TransportSupport>) : Supplier<TransportSupport> {
|
||||
private val loading = AtomicBoolean(false)
|
||||
private lateinit var mySupport: TransportSupport
|
||||
private val lock = ReentrantLock()
|
||||
private val condition = lock.newCondition()
|
||||
private val exceptionReference = AtomicReference<Exception>(null)
|
||||
internal interface TransportSupportLoader : Disposable {
|
||||
|
||||
val isLoaded get() = ::mySupport.isInitialized
|
||||
/**
|
||||
* 获取传输支持
|
||||
*/
|
||||
suspend fun getTransportSupport(): TransportSupport
|
||||
|
||||
/**
|
||||
* 只有当 [isLoaded] 返回 true 时才能调用,为了不出现问题,只有 EDT 线程才能调用
|
||||
*/
|
||||
fun getSyncTransportSupport(): TransportSupport
|
||||
|
||||
override fun get(): TransportSupport {
|
||||
if (isLoaded) return mySupport
|
||||
|
||||
if (loading.compareAndSet(false, true)) {
|
||||
try {
|
||||
mySupport = support.get()
|
||||
} catch (e: Exception) {
|
||||
exceptionReference.set(e)
|
||||
throw e
|
||||
} finally {
|
||||
lock.withLock {
|
||||
loading.set(false)
|
||||
condition.signalAll()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lock.lock()
|
||||
try {
|
||||
condition.await()
|
||||
} finally {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
val exception = exceptionReference.get()
|
||||
if (exception != null) {
|
||||
throw exception
|
||||
}
|
||||
|
||||
return get()
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已经加载,已经加载不表示可以正常使用,它仅证明已经加载可以同步调用
|
||||
*/
|
||||
fun isLoaded(): Boolean
|
||||
|
||||
/**
|
||||
* 快速检查是否已经成功打开
|
||||
*/
|
||||
fun isOpened(): Boolean = true
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import javax.swing.JToolBar
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
@Suppress("DuplicatedCode")
|
||||
class TransportTabbed(
|
||||
internal class TransportTabbed(
|
||||
private val transferManager: TransferManager,
|
||||
) : FlatTabbedPane(), Disposable {
|
||||
private val addBtn = JButton(Icons.add)
|
||||
@@ -64,14 +64,16 @@ class TransportTabbed(
|
||||
// 右键菜单
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (!SwingUtilities.isRightMouseButton(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
val index = indexAtLocation(e.x, e.y)
|
||||
if (index < 0) return
|
||||
|
||||
showContextMenu(index, e)
|
||||
if (SwingUtilities.isRightMouseButton(e)) {
|
||||
showContextMenu(index, e)
|
||||
} else if (SwingUtilities.isLeftMouseButton(e) && e.clickCount % 2 == 0) {
|
||||
val tab = getTransportPanel(index) ?: return
|
||||
if (tab.loader.isOpened().not()) {
|
||||
tab.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -105,8 +107,9 @@ class TransportTabbed(
|
||||
|
||||
private fun tabClose(c: TransportPanel): Boolean {
|
||||
if (transferManager.getTransferCount() < 1) return true
|
||||
if (c.loader.isLoaded.not()) return false
|
||||
val fileSystem = c.getFileSystem()
|
||||
val loader = c.loader
|
||||
if (loader.isLoaded().not()) return false
|
||||
val fileSystem = loader.getSyncTransportSupport()
|
||||
val transfers = transferManager.getTransfers()
|
||||
.filter { it.source().fileSystem == fileSystem || it.target().fileSystem == fileSystem }
|
||||
if (transfers.isEmpty()) return true
|
||||
@@ -137,8 +140,21 @@ class TransportTabbed(
|
||||
|
||||
fun addLocalTab() {
|
||||
val host = Host(name = "Local", protocol = LocalProtocolProvider.PROTOCOL)
|
||||
val support = TransportSupport(FileSystems.getDefault(), getDefaultLocalPath())
|
||||
val panel = TransportPanel(internalTransferManager, host, TransportSupportLoader { support })
|
||||
val fs = FileSystems.getDefault()
|
||||
val support = DefaultTransportSupport(fs, fs.getPath(getDefaultLocalPath()))
|
||||
val panel = TransportPanel(internalTransferManager, host, object : TransportSupportLoader {
|
||||
override suspend fun getTransportSupport(): TransportSupport {
|
||||
return support
|
||||
}
|
||||
|
||||
override fun getSyncTransportSupport(): TransportSupport {
|
||||
return support
|
||||
}
|
||||
|
||||
override fun isLoaded(): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
addTab(I18n.getString("termora.transport.local"), panel)
|
||||
super.setTabClosable(0, false)
|
||||
}
|
||||
@@ -165,20 +181,30 @@ class TransportTabbed(
|
||||
// 编辑
|
||||
val edit = popupMenu.add(I18n.getString("termora.keymgr.edit"))
|
||||
edit.addActionListener(object : AnAction() {
|
||||
private val hostManager get() = HostManager.getInstance()
|
||||
private val accountManager get() = AccountManager.getInstance()
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val window = evt.window
|
||||
val dialog = NewHostDialogV2(
|
||||
window,
|
||||
panel.host,
|
||||
getHost(panel),
|
||||
accountOwner = accountManager.getOwners().first { it.id == panel.host.ownerId })
|
||||
dialog.setLocationRelativeTo(window)
|
||||
dialog.title = panel.host.name
|
||||
dialog.isVisible = true
|
||||
val host = dialog.host ?: return
|
||||
HostManager.getInstance().addHost(host, DatabaseChangedExtension.Source.Sync)
|
||||
hostManager.addHost(host, DatabaseChangedExtension.Source.User)
|
||||
setTitleAt(tabIndex, host.name)
|
||||
panel.host = host
|
||||
setHost(panel, host)
|
||||
}
|
||||
|
||||
|
||||
private fun getHost(panel: TransportPanel): Host {
|
||||
return panel.getClientProperty("EditHost") as Host? ?: panel.host
|
||||
}
|
||||
|
||||
private fun setHost(panel: TransportPanel, host: Host) {
|
||||
panel.putClientProperty("EditHost", host)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -3,18 +3,18 @@ package app.termora.transfer
|
||||
import app.termora.*
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
|
||||
import java.beans.PropertyChangeListener
|
||||
import java.nio.file.FileSystems
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
class TransportTerminalTab : RememberFocusTerminalTab() {
|
||||
internal class TransportTerminalTab : RememberFocusTerminalTab() {
|
||||
private val transportViewer = TransportViewer()
|
||||
private val sftp get() = DatabaseManager.getInstance().sftp
|
||||
private val transferManager get() = transportViewer.getTransferManager()
|
||||
val leftTabbed get() = transportViewer.getLeftTabbed()
|
||||
private val leftTabbed get() = transportViewer.getLeftTabbed()
|
||||
val rightTabbed get() = transportViewer.getRightTabbed()
|
||||
|
||||
init {
|
||||
@@ -66,8 +66,8 @@ class TransportTerminalTab : RememberFocusTerminalTab() {
|
||||
private fun hasActiveTab(tabbed: TransportTabbed): Boolean {
|
||||
for (i in 0 until tabbed.tabCount) {
|
||||
val c = tabbed.getComponentAt(i) ?: continue
|
||||
if (c is TransportPanel && c.loader.isLoaded) {
|
||||
if (c.getFileSystem() != FileSystems.getDefault()) {
|
||||
if (c is TransportPanel && c.loader.isOpened()) {
|
||||
if (c.loader.getSyncTransportSupport().getFileSystem().isLocallyFileSystem().not()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,19 @@ package app.termora.transfer
|
||||
import app.termora.Disposable
|
||||
import app.termora.Disposer
|
||||
import app.termora.DynamicColor
|
||||
import app.termora.Icons
|
||||
import app.termora.actions.DataProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import org.slf4j.LoggerFactory
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.nio.file.Path
|
||||
import javax.swing.*
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(TransportViewer::class.java)
|
||||
}
|
||||
internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val splitPane = JSplitPane()
|
||||
@@ -78,10 +75,33 @@ class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
coroutineScope.launch(Dispatchers.Swing) {
|
||||
while (isActive) {
|
||||
delay(250.milliseconds)
|
||||
checkDisconnected(leftTabbed)
|
||||
checkDisconnected(rightTabbed)
|
||||
}
|
||||
}
|
||||
|
||||
Disposer.register(this, leftTabbed)
|
||||
Disposer.register(this, rightTabbed)
|
||||
}
|
||||
|
||||
private fun checkDisconnected(tabbed: TransportTabbed) {
|
||||
for (i in 0 until tabbed.tabCount) {
|
||||
val tab = tabbed.getTransportPanel(i) ?: continue
|
||||
val icon = tabbed.getIconAt(i)
|
||||
if (tab.loader.isOpened()) {
|
||||
if (icon == null) continue
|
||||
tabbed.setIconAt(i, null)
|
||||
} else {
|
||||
if (icon == Icons.breakpoint) continue
|
||||
tabbed.setIconAt(i, Icons.breakpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createInternalTransferManager(
|
||||
source: TransportTabbed,
|
||||
target: TransportTabbed
|
||||
@@ -118,4 +138,8 @@ class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
||||
return rightTabbed
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
}
|
||||
4
src/main/resources/icons/breakpoint.svg
Normal file
4
src/main/resources/icons/breakpoint.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14Z" fill="#E55765"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 402 B |
4
src/main/resources/icons/breakpoint_dark.svg
Normal file
4
src/main/resources/icons/breakpoint_dark.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14Z" fill="#DB5C5C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 402 B |
Reference in New Issue
Block a user