mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12: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.apache.commons.lang3.SystemUtils
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.awt.MenuItem
|
import java.awt.*
|
||||||
import java.awt.PopupMenu
|
|
||||||
import java.awt.SystemTray
|
|
||||||
import java.awt.TrayIcon
|
|
||||||
import java.awt.desktop.AppReopenedEvent
|
import java.awt.desktop.AppReopenedEvent
|
||||||
import java.awt.desktop.AppReopenedListener
|
import java.awt.desktop.AppReopenedListener
|
||||||
import java.awt.desktop.SystemEventListener
|
import java.awt.desktop.SystemEventListener
|
||||||
@@ -202,6 +199,7 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||||
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
|
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
|
||||||
|
UIManager.put(FlatClientProperties.POPUP_FORCE_HEAVY_WEIGHT, true)
|
||||||
|
|
||||||
UIManager.put("Component.arc", 5)
|
UIManager.put("Component.arc", 5)
|
||||||
UIManager.put("TextComponent.arc", UIManager.getInt("Component.arc"))
|
UIManager.put("TextComponent.arc", UIManager.getInt("Component.arc"))
|
||||||
@@ -237,12 +235,24 @@ class ApplicationRunner {
|
|||||||
|
|
||||||
// Linux 更多的是尖锐风格
|
// Linux 更多的是尖锐风格
|
||||||
if (SystemInfo.isMacOS || SystemInfo.isWindows) {
|
if (SystemInfo.isMacOS || SystemInfo.isWindows) {
|
||||||
|
val selectionInsets = Insets(0, 2, 0, 2)
|
||||||
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
|
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("Tree.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("List.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
UIManager.put("ComboBox.selectionArc", UIManager.getInt("Component.arc"))
|
UIManager.put("ComboBox.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("ComboBox.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("Table.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
UIManager.put("MenuBar.selectionArc", UIManager.getInt("Component.arc"))
|
UIManager.put("MenuBar.selectionArc", UIManager.getInt("Component.arc"))
|
||||||
|
UIManager.put("MenuBar.selectionInsets", selectionInsets)
|
||||||
|
|
||||||
UIManager.put("MenuItem.selectionArc", UIManager.getInt("Component.arc"))
|
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 empty by lazy { DynamicIcon("icons/empty.svg") }
|
||||||
val changelog by lazy { DynamicIcon("icons/changelog.svg", "icons/changelog_dark.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 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 softWrap by lazy { DynamicIcon("icons/softWrap.svg", "icons/softWrap_dark.svg") }
|
||||||
val scrollUp by lazy { DynamicIcon("icons/scrollUp.svg", "icons/scrollUp_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") }
|
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) {
|
if (key == DataKey.CurrentDir) {
|
||||||
val dir = DataKey.CurrentDir.clazz.cast(data)
|
val dir = DataKey.CurrentDir.clazz.cast(data)
|
||||||
val navigator = getTransportNavigator() ?: return
|
val navigator = getTransportNavigator() ?: return
|
||||||
val path = navigator.getFileSystem().getPath(dir)
|
val loader = navigator.loader
|
||||||
if (path == navigator.workdir) return
|
if (loader.isOpened().not()) return
|
||||||
navigator.navigateTo(path)
|
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 {
|
try {
|
||||||
val session = getSession()
|
val session = getSession()
|
||||||
val fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
val fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
|
||||||
val support = TransportSupport(fileSystem, fileSystem.defaultDir.absolutePathString())
|
val support = DefaultTransportSupport(fileSystem, fileSystem.defaultDir)
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
val internalTransferManager = MyInternalTransferManager()
|
val internalTransferManager = MyInternalTransferManager()
|
||||||
val transportPanel = TransportPanel(
|
val transportPanel = TransportPanel(
|
||||||
internalTransferManager, tab.host,
|
internalTransferManager, tab.host,
|
||||||
TransportSupportLoader { support })
|
object : TransportSupportLoader {
|
||||||
internalTransferManager.setTransferPanel(transportPanel)
|
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 {
|
Disposer.register(transportPanel, object : Disposable {
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
panel.remove(transportPanel)
|
panel.remove(transportPanel)
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ class DefaultInternalTransferManager(
|
|||||||
|
|
||||||
|
|
||||||
override fun canTransfer(paths: List<Path>): Boolean {
|
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(
|
override fun addTransfer(
|
||||||
@@ -270,7 +272,8 @@ class DefaultInternalTransferManager(
|
|||||||
val isDirectory = pair.second.isDirectory
|
val isDirectory = pair.second.isDirectory
|
||||||
val path = pair.first
|
val path = pair.first
|
||||||
if (isDirectory.not() || mode == TransferMode.Rmrf) {
|
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
|
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)) {
|
if (continueTransfer(node, false)) {
|
||||||
doTransfer(node)
|
doTransfer(node)
|
||||||
} else {
|
} else {
|
||||||
changeState(node, State.Failed)
|
withContext(Dispatchers.Swing) {
|
||||||
|
changeState(node, State.Failed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lock.withLock { condition.signalAll() }
|
lock.withLock { condition.signalAll() }
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package app.termora.transfer
|
|||||||
|
|
||||||
import app.termora.DynamicColor
|
import app.termora.DynamicColor
|
||||||
import app.termora.Icons
|
import app.termora.Icons
|
||||||
import app.termora.OptionPane
|
|
||||||
import app.termora.transfer.TransportPanel.Companion.isWindowsFileSystem
|
import app.termora.transfer.TransportPanel.Companion.isWindowsFileSystem
|
||||||
import com.formdev.flatlaf.FlatClientProperties
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
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.io.FilenameUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.awt.CardLayout
|
import java.awt.CardLayout
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Insets
|
import java.awt.Insets
|
||||||
@@ -24,7 +21,6 @@ import java.awt.event.*
|
|||||||
import java.beans.PropertyChangeEvent
|
import java.beans.PropertyChangeEvent
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.function.Supplier
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.event.PopupMenuEvent
|
import javax.swing.event.PopupMenuEvent
|
||||||
import javax.swing.event.PopupMenuListener
|
import javax.swing.event.PopupMenuListener
|
||||||
@@ -33,13 +29,9 @@ import kotlin.io.path.name
|
|||||||
import kotlin.io.path.pathString
|
import kotlin.io.path.pathString
|
||||||
import kotlin.math.round
|
import kotlin.math.round
|
||||||
|
|
||||||
class TransportNavigationPanel(
|
internal class TransportNavigationPanel(private val navigator: TransportNavigator) : JPanel() {
|
||||||
private val support: Supplier<TransportSupport>,
|
|
||||||
private val navigator: TransportNavigator
|
|
||||||
) : JPanel() {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(TransportNavigationPanel::class.java)
|
|
||||||
private const val TEXT_FIELD = "TextField"
|
private const val TEXT_FIELD = "TextField"
|
||||||
private const val SEGMENTS = "Segments"
|
private const val SEGMENTS = "Segments"
|
||||||
|
|
||||||
@@ -50,7 +42,6 @@ class TransportNavigationPanel(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
|
||||||
private val layeredPane = LayeredPane()
|
private val layeredPane = LayeredPane()
|
||||||
private val textField = FlatTextField()
|
private val textField = FlatTextField()
|
||||||
private val downBtn = JButton(Icons.chevronDown)
|
private val downBtn = JButton(Icons.chevronDown)
|
||||||
@@ -115,8 +106,7 @@ class TransportNavigationPanel(
|
|||||||
val itemListener = object : ItemListener {
|
val itemListener = object : ItemListener {
|
||||||
override fun itemStateChanged(e: ItemEvent) {
|
override fun itemStateChanged(e: ItemEvent) {
|
||||||
val path = comboBox.selectedItem as Path? ?: return
|
val path = comboBox.selectedItem as Path? ?: return
|
||||||
if (navigator.loading) return
|
navigator.navigateTo(path.absolutePathString())
|
||||||
navigator.navigateTo(path)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,17 +169,7 @@ class TransportNavigationPanel(
|
|||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
if (navigator.loading) return
|
if (navigator.loading) return
|
||||||
if (textField.text.isBlank()) return
|
if (textField.text.isBlank()) return
|
||||||
|
navigator.navigateTo(textField.text)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -261,7 +241,7 @@ class TransportNavigationPanel(
|
|||||||
if (path == navigator.workdir) {
|
if (path == navigator.workdir) {
|
||||||
setTextFieldText(path)
|
setTextFieldText(path)
|
||||||
} else {
|
} else {
|
||||||
navigator.navigateTo(path)
|
navigator.navigateTo(path.absolutePathString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -353,7 +333,7 @@ class TransportNavigationPanel(
|
|||||||
text = item.pathString
|
text = item.pathString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
popupMenu.add(text).addActionListener { navigator.navigateTo(item) }
|
popupMenu.add(text).addActionListener { navigator.navigateTo(item.absolutePathString()) }
|
||||||
}
|
}
|
||||||
popupMenu.show(
|
popupMenu.show(
|
||||||
button,
|
button,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ interface TransportNavigator {
|
|||||||
val loading: Boolean
|
val loading: Boolean
|
||||||
val workdir: Path?
|
val workdir: Path?
|
||||||
|
|
||||||
fun navigateTo(destination: Path): Boolean
|
fun navigateTo(destination: String): Boolean
|
||||||
|
|
||||||
fun addPropertyChangeListener(propertyName: String, listener: PropertyChangeListener)
|
fun addPropertyChangeListener(propertyName: String, listener: PropertyChangeListener)
|
||||||
fun removePropertyChangeListener(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.milliseconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class TransportPanel(
|
internal class TransportPanel(
|
||||||
private val transferManager: InternalTransferManager,
|
private val transferManager: InternalTransferManager,
|
||||||
var host: Host,
|
val host: Host,
|
||||||
val loader: TransportSupportLoader,
|
val loader: TransportSupportLoader,
|
||||||
) : JPanel(BorderLayout()), DataProvider, Disposable, TransportNavigator {
|
) : JPanel(BorderLayout()), DataProvider, Disposable, TransportNavigator {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -118,9 +118,6 @@ class TransportPanel(
|
|||||||
private val disposed = AtomicBoolean(false)
|
private val disposed = AtomicBoolean(false)
|
||||||
private val futures = Collections.synchronizedSet(mutableSetOf<Future<*>>())
|
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(prevBtn)
|
||||||
toolbar.add(homeBtn)
|
toolbar.add(homeBtn)
|
||||||
toolbar.add(nextBtn)
|
toolbar.add(nextBtn)
|
||||||
toolbar.add(TransportNavigationPanel(loader, this))
|
toolbar.add(TransportNavigationPanel(this))
|
||||||
toolbar.add(bookmarkBtn)
|
toolbar.add(bookmarkBtn)
|
||||||
toolbar.add(parentBtn)
|
toolbar.add(parentBtn)
|
||||||
toolbar.add(eyeBtn)
|
toolbar.add(eyeBtn)
|
||||||
@@ -235,7 +232,7 @@ class TransportPanel(
|
|||||||
|
|
||||||
Disposer.register(this, editTransferListener)
|
Disposer.register(this, editTransferListener)
|
||||||
|
|
||||||
refreshBtn.addActionListener { reload() }
|
refreshBtn.addActionListener { reload(requestFocus = true) }
|
||||||
|
|
||||||
prevBtn.addActionListener { navigator.back() }
|
prevBtn.addActionListener { navigator.back() }
|
||||||
nextBtn.addActionListener { navigator.forward() }
|
nextBtn.addActionListener { navigator.forward() }
|
||||||
@@ -243,7 +240,7 @@ class TransportPanel(
|
|||||||
parentBtn.addActionListener(createSmartAction(object : AbstractAction() {
|
parentBtn.addActionListener(createSmartAction(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
if (hasParent.not()) return
|
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()
|
bookmarkBtn.isBookmark = bookmarkBtn.isBookmark.not()
|
||||||
} else {
|
} else {
|
||||||
navigateTo(_fileSystem.getPath(e.actionCommand))
|
navigateTo(e.actionCommand)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
homeBtn.addActionListener(createSmartAction(object : AbstractAction() {
|
homeBtn.addActionListener(createSmartAction(object : AbstractAction() {
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
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) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
showHiddenFiles = showHiddenFiles.not()
|
showHiddenFiles = showHiddenFiles.not()
|
||||||
eyeBtn.icon = if (showHiddenFiles) Icons.eye else Icons.eyeClose
|
eyeBtn.icon = if (showHiddenFiles) Icons.eye else Icons.eyeClose
|
||||||
reload()
|
reload(requestFocus = true)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -289,8 +288,11 @@ class TransportPanel(
|
|||||||
transferManager.addTransferListener(object : TransferListener {
|
transferManager.addTransferListener(object : TransferListener {
|
||||||
override fun onTransferChanged(transfer: Transfer, state: TransferTreeTableNode.State) {
|
override fun onTransferChanged(transfer: Transfer, state: TransferTreeTableNode.State) {
|
||||||
if (state != TransferTreeTableNode.State.Done && state != TransferTreeTableNode.State.Failed) return
|
if (state != TransferTreeTableNode.State.Done && state != TransferTreeTableNode.State.Failed) return
|
||||||
if (transfer.target().fileSystem != _fileSystem) return
|
val target = transfer.target()
|
||||||
if (transfer.target() == workdir || transfer.target().parent == workdir) {
|
if (loader.isLoaded()) {
|
||||||
|
if (target.fileSystem != loader.getSyncTransportSupport().getFileSystem()) return
|
||||||
|
}
|
||||||
|
if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) {
|
||||||
reload(requestFocus = false)
|
reload(requestFocus = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -362,7 +364,7 @@ class TransportPanel(
|
|||||||
undoManager.addEdit(object : AbstractUndoableEdit() {
|
undoManager.addEdit(object : AbstractUndoableEdit() {
|
||||||
override fun undo() {
|
override fun undo() {
|
||||||
super.undo()
|
super.undo()
|
||||||
if (navigator.navigateTo(oldValue)) {
|
if (navigator.reload(newPath = oldValue.absolutePathString(), requestFocus = true)) {
|
||||||
undoOrRedo = true
|
undoOrRedo = true
|
||||||
undoOrRedoPath = oldValue
|
undoOrRedoPath = oldValue
|
||||||
}
|
}
|
||||||
@@ -370,7 +372,7 @@ class TransportPanel(
|
|||||||
|
|
||||||
override fun redo() {
|
override fun redo() {
|
||||||
super.redo()
|
super.redo()
|
||||||
if (navigator.navigateTo(newValue)) {
|
if (navigator.reload(newPath = newValue.absolutePathString(), requestFocus = true)) {
|
||||||
undoOrRedo = true
|
undoOrRedo = true
|
||||||
undoOrRedoPath = newValue
|
undoOrRedoPath = newValue
|
||||||
}
|
}
|
||||||
@@ -431,10 +433,10 @@ class TransportPanel(
|
|||||||
if (attributes.isDirectory) {
|
if (attributes.isDirectory) {
|
||||||
enterSelectionFolder()
|
enterSelectionFolder()
|
||||||
} else {
|
} else {
|
||||||
transferManager.addTransfer(
|
val paths = listOf(model.getPath(row) to attributes)
|
||||||
listOf(model.getPath(row) to attributes),
|
if (loader.isOpened() && transferManager.canTransfer(paths.map { it.first })) {
|
||||||
InternalTransferManager.TransferMode.Transfer
|
transferManager.addTransfer(paths, InternalTransferManager.TransferMode.Transfer)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
} else if (SwingUtilities.isRightMouseButton(e)) {
|
} else if (SwingUtilities.isRightMouseButton(e)) {
|
||||||
val r = table.rowAtPoint(e.point)
|
val r = table.rowAtPoint(e.point)
|
||||||
@@ -524,7 +526,6 @@ class TransportPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getTransferData(support: TransferSupport, load: Boolean): TransferData? {
|
private fun getTransferData(support: TransferSupport, load: Boolean): TransferData? {
|
||||||
if (loader.isLoaded.not()) return null
|
|
||||||
val workdir = workdir ?: return null
|
val workdir = workdir ?: return null
|
||||||
val dropLocation = support.dropLocation as? JTable.DropLocation ?: return null
|
val dropLocation = support.dropLocation as? JTable.DropLocation ?: return null
|
||||||
val row = if (dropLocation.isInsertRow) 0 else sorter.convertRowIndexToModel(dropLocation.row)
|
val row = if (dropLocation.isInsertRow) 0 else sorter.convertRowIndexToModel(dropLocation.row)
|
||||||
@@ -540,7 +541,8 @@ class TransportPanel(
|
|||||||
if (transferTransferable.component == panel) return null
|
if (transferTransferable.component == panel) return null
|
||||||
paths.addAll(transferTransferable.files)
|
paths.addAll(transferTransferable.files)
|
||||||
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
} else if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||||
if (_fileSystem.isLocallyFileSystem()) return null
|
if (loader.isLoaded() && loader.getSyncTransportSupport().getFileSystem().isLocallyFileSystem())
|
||||||
|
return null
|
||||||
if (load) {
|
if (load) {
|
||||||
val files = support.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*>
|
val files = support.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<*>
|
||||||
if (files.isEmpty()) return null
|
if (files.isEmpty()) return null
|
||||||
@@ -577,22 +579,12 @@ class TransportPanel(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTableModel(): TransportTableModel {
|
private suspend fun getFileSystem(): FileSystem {
|
||||||
return model
|
return loader.getTransportSupport().getFileSystem()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFileSystem(): FileSystem {
|
private suspend fun getTransportSupport(): TransportSupport {
|
||||||
return _fileSystem
|
return loader.getTransportSupport()
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 不能在 EDT 线程调用
|
|
||||||
*/
|
|
||||||
private fun getSupport(): TransportSupport {
|
|
||||||
if (SwingUtilities.isEventDispatchThread()) {
|
|
||||||
throw WrongThreadException("AWT EventQueue")
|
|
||||||
}
|
|
||||||
return loader.get()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enterSelectionFolder() {
|
private fun enterSelectionFolder() {
|
||||||
@@ -609,7 +601,7 @@ class TransportPanel(
|
|||||||
if (workdir != null) registerSelectRow(workdir.name)
|
if (workdir != null) registerSelectRow(workdir.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
navigator.navigateTo(path)
|
navigator.navigateTo(path.absolutePathString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun registerSelectRow(name: String) {
|
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()
|
assertEventDispatchThread()
|
||||||
|
|
||||||
if (loading) return false
|
if (loading) return false
|
||||||
@@ -662,20 +658,26 @@ class TransportPanel(
|
|||||||
return true
|
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
|
val workdir = newPath ?: oldPath
|
||||||
|
|
||||||
if (workdir == null) {
|
if (workdir == null) {
|
||||||
val path = _fileSystem.getPath(defaultPath)
|
val path = support.getDefaultPath()
|
||||||
return doReload(null, path)
|
return doReload(null, path.absolutePathString())
|
||||||
}
|
}
|
||||||
|
|
||||||
val path = workdir
|
val path = fileSystem.getPath(workdir)
|
||||||
val first = AtomicBoolean(false)
|
val first = AtomicBoolean(false)
|
||||||
var parent = path.parent
|
var parent = path.parent
|
||||||
if (parent == null && _fileSystem.isWindowsFileSystem() && workdir.pathString != _fileSystem.separator) {
|
if (parent == null && fileSystem.isWindowsFileSystem() && path.pathString != fileSystem.separator) {
|
||||||
parent = _fileSystem.getPath(_fileSystem.separator)
|
parent = fileSystem.getPath(fileSystem.separator)
|
||||||
}
|
}
|
||||||
val files = mutableListOf<Pair<Path, Attributes>>()
|
val files = mutableListOf<Pair<Path, Attributes>>()
|
||||||
if ((parent != null).also { hasParent = it }) {
|
if ((parent != null).also { hasParent = it }) {
|
||||||
@@ -696,8 +698,8 @@ class TransportPanel(
|
|||||||
files.clear()
|
files.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_fileSystem.isWindowsFileSystem() && workdir.pathString == _fileSystem.separator) {
|
if (fileSystem.isWindowsFileSystem() && path.pathString == fileSystem.separator) {
|
||||||
for (path in _fileSystem.rootDirectories) {
|
for (path in fileSystem.rootDirectories) {
|
||||||
val attributes = getAttributes(path)
|
val attributes = getAttributes(path)
|
||||||
files.add(path to attributes)
|
files.add(path to attributes)
|
||||||
}
|
}
|
||||||
@@ -716,7 +718,7 @@ class TransportPanel(
|
|||||||
if (requestFocus)
|
if (requestFocus)
|
||||||
coroutineScope.launch(Dispatchers.Swing) { table.requestFocusInWindow() }
|
coroutineScope.launch(Dispatchers.Swing) { table.requestFocusInWindow() }
|
||||||
|
|
||||||
return workdir
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun listFiles(path: Path): Stream<Pair<Path, Attributes>> {
|
private fun listFiles(path: Path): Stream<Pair<Path, Attributes>> {
|
||||||
@@ -787,21 +789,24 @@ class TransportPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun showContextmenu(rows: Array<Int>, e: MouseEvent) {
|
private fun showContextmenu(rows: Array<Int>, e: MouseEvent) {
|
||||||
val files = rows.map { model.getPath(it) to model.getAttributes(it) }
|
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.addActionListener(PopupMenuActionListener(files))
|
||||||
popupMenu.show(table, e.x, e.y)
|
popupMenu.show(table, e.x, e.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateTo(destination: Path): Boolean {
|
|
||||||
|
override fun navigateTo(destination: String): Boolean {
|
||||||
assertEventDispatchThread()
|
assertEventDispatchThread()
|
||||||
|
|
||||||
if (loading) return false
|
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> {
|
override fun getHistory(): List<Path> {
|
||||||
@@ -825,7 +830,7 @@ class TransportPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setNewWorkdir(destination: Path) {
|
private fun setNewWorkdir(destination: Path) {
|
||||||
val oldValue = workdir
|
val oldValue = if (destination.fileSystem == workdir?.fileSystem) workdir else null
|
||||||
workdir = destination
|
workdir = destination
|
||||||
firePropertyChange("workdir", oldValue, destination)
|
firePropertyChange("workdir", oldValue, destination)
|
||||||
}
|
}
|
||||||
@@ -912,8 +917,12 @@ class TransportPanel(
|
|||||||
val millis = Files.getLastModifiedTime(localPath).toMillis()
|
val millis = Files.getLastModifiedTime(localPath).toMillis()
|
||||||
if (oldMillis == millis) continue
|
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
|
oldMillis = millis
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1009,8 +1018,9 @@ class TransportPanel(
|
|||||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.NewFolder || actionCommand == TransportPopupMenu.ActionCommand.NewFile) {
|
} else if (actionCommand == TransportPopupMenu.ActionCommand.NewFolder || actionCommand == TransportPopupMenu.ActionCommand.NewFile) {
|
||||||
val name = e.source.toString()
|
val name = e.source.toString()
|
||||||
val workdir = workdir ?: return
|
val workdir = workdir ?: return
|
||||||
val path = workdir.resolve(name)
|
|
||||||
processPath(e.source.toString()) {
|
processPath(e.source.toString()) {
|
||||||
|
// 因为此时可能已经断线,任何 Path 都不可完全相信
|
||||||
|
val path = getFileSystem().getPath(workdir.resolve(name).absolutePathString())
|
||||||
if (actionCommand == TransportPopupMenu.ActionCommand.NewFolder)
|
if (actionCommand == TransportPopupMenu.ActionCommand.NewFolder)
|
||||||
path.createDirectories()
|
path.createDirectories()
|
||||||
else
|
else
|
||||||
@@ -1022,6 +1032,9 @@ class TransportPanel(
|
|||||||
processPath(e.source.toString()) { source.moveTo(target) }
|
processPath(e.source.toString()) { source.moveTo(target) }
|
||||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.Rmrf) {
|
} else if (actionCommand == TransportPopupMenu.ActionCommand.Rmrf) {
|
||||||
transferManager.addTransfer(files, InternalTransferManager.TransferMode.Rmrf)
|
transferManager.addTransfer(files, InternalTransferManager.TransferMode.Rmrf)
|
||||||
|
} else if (actionCommand == TransportPopupMenu.ActionCommand.Reconnect) {
|
||||||
|
// reload now
|
||||||
|
reload()
|
||||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.ChangePermissions) {
|
} else if (actionCommand == TransportPopupMenu.ActionCommand.ChangePermissions) {
|
||||||
val c = e.source as TransportPopupMenu.ChangePermission
|
val c = e.source as TransportPopupMenu.ChangePermission
|
||||||
val path = files.first().first
|
val path = files.first().first
|
||||||
@@ -1104,7 +1117,7 @@ class TransportPanel(
|
|||||||
|
|
||||||
private inner class MyDefaultTableCellRenderer : DefaultTableCellRenderer() {
|
private inner class MyDefaultTableCellRenderer : DefaultTableCellRenderer() {
|
||||||
override fun getTableCellRendererComponent(
|
override fun getTableCellRendererComponent(
|
||||||
table: JTable?,
|
table: JTable,
|
||||||
value: Any?,
|
value: Any?,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
hasFocus: Boolean,
|
hasFocus: Boolean,
|
||||||
@@ -1133,12 +1146,13 @@ class TransportPanel(
|
|||||||
text = StringUtils.EMPTY
|
text = StringUtils.EMPTY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreground = null
|
||||||
val c = super.getTableCellRendererComponent(table, text, isSelected, hasFocus, row, column)
|
val c = super.getTableCellRendererComponent(table, text, isSelected, hasFocus, row, column)
|
||||||
icon = null
|
icon = null
|
||||||
|
|
||||||
if (column == TransportTableModel.COLUMN_NAME) {
|
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) {
|
icon = if (attributes.isParent) {
|
||||||
NativeIcons.folderIcon
|
NativeIcons.folderIcon
|
||||||
} else {
|
} else {
|
||||||
@@ -1165,6 +1179,12 @@ class TransportPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loader.isOpened().not()) {
|
||||||
|
if (isSelected.not()) {
|
||||||
|
foreground = UIManager.getColor("textInactiveText")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import app.termora.Icons
|
|||||||
import app.termora.OptionPane
|
import app.termora.OptionPane
|
||||||
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
|
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
import org.apache.sshd.sftp.client.fs.SftpFileSystem
|
||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
@@ -13,7 +14,6 @@ import java.awt.datatransfer.StringSelection
|
|||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
import java.awt.event.ActionListener
|
import java.awt.event.ActionListener
|
||||||
import java.awt.event.KeyEvent
|
import java.awt.event.KeyEvent
|
||||||
import java.nio.file.FileSystem
|
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.attribute.PosixFilePermission
|
import java.nio.file.attribute.PosixFilePermission
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -26,11 +26,11 @@ import kotlin.io.path.absolutePathString
|
|||||||
import kotlin.io.path.name
|
import kotlin.io.path.name
|
||||||
|
|
||||||
|
|
||||||
class TransportPopupMenu(
|
internal class TransportPopupMenu(
|
||||||
private val owner: Window,
|
private val owner: Window,
|
||||||
private val model: TransportTableModel,
|
private val model: TransportTableModel,
|
||||||
private val transferManager: InternalTransferManager,
|
private val transferManager: InternalTransferManager,
|
||||||
private val fileSystem: FileSystem,
|
private val loader: TransportSupportLoader,
|
||||||
private val files: List<Pair<Path, TransportTableModel.Attributes>>
|
private val files: List<Pair<Path, TransportTableModel.Attributes>>
|
||||||
) : FlatPopupMenu() {
|
) : FlatPopupMenu() {
|
||||||
private val paths = files.map { it.first }
|
private val paths = files.map { it.first }
|
||||||
@@ -71,25 +71,45 @@ class TransportPopupMenu(
|
|||||||
private fun initView() {
|
private fun initView() {
|
||||||
inheritsPopupMenu = false
|
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(transferMenu)
|
||||||
add(editMenu)
|
add(editMenu)
|
||||||
addSeparator()
|
addSeparator()
|
||||||
add(copyPathMenu)
|
add(copyPathMenu)
|
||||||
if (fileSystem.isLocallyFileSystem()) add(openInFinderMenu)
|
if (fileSystem?.isLocallyFileSystem() == true) {
|
||||||
|
add(openInFinderMenu)
|
||||||
|
}
|
||||||
addSeparator()
|
addSeparator()
|
||||||
add(renameMenu)
|
add(renameMenu)
|
||||||
add(deleteMenu)
|
add(deleteMenu)
|
||||||
if (fileSystem is SftpFileSystem) add(rmrfMenu)
|
if (fileSystem is SftpFileSystem) {
|
||||||
|
add(rmrfMenu)
|
||||||
|
}
|
||||||
add(changePermissionsMenu)
|
add(changePermissionsMenu)
|
||||||
addSeparator()
|
addSeparator()
|
||||||
add(refreshMenu)
|
add(refreshMenu)
|
||||||
addSeparator()
|
addSeparator()
|
||||||
add(newMenu)
|
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)
|
transferMenu.isEnabled = hasParent.not() && files.isNotEmpty() && transferManager.canTransfer(paths)
|
||||||
copyPathMenu.isEnabled = files.isNotEmpty()
|
copyPathMenu.isEnabled = files.isNotEmpty()
|
||||||
openInFinderMenu.isEnabled = files.isNotEmpty() && fileSystem.isLocallyFileSystem()
|
openInFinderMenu.isEnabled = files.isNotEmpty() && fileSystem?.isLocallyFileSystem() == true
|
||||||
editMenu.isEnabled = files.isNotEmpty() && fileSystem.isLocallyFileSystem().not()
|
editMenu.isEnabled = files.isNotEmpty() && fileSystem?.isLocallyFileSystem() != true
|
||||||
&& files.all { it.second.isFile && it.second.isSymbolicLink.not() }
|
&& files.all { it.second.isFile && it.second.isSymbolicLink.not() }
|
||||||
renameMenu.isEnabled = hasParent.not() && files.size == 1
|
renameMenu.isEnabled = hasParent.not() && files.size == 1
|
||||||
deleteMenu.isEnabled = hasParent.not() && files.isNotEmpty()
|
deleteMenu.isEnabled = hasParent.not() && files.isNotEmpty()
|
||||||
@@ -211,6 +231,7 @@ class TransportPopupMenu(
|
|||||||
Refresh,
|
Refresh,
|
||||||
ChangePermissions,
|
ChangePermissions,
|
||||||
Rmrf,
|
Rmrf,
|
||||||
|
Reconnect,
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ChangePermission(val permissions: Set<PosixFilePermission>, val includeSubFolder: Boolean)
|
data class ChangePermission(val permissions: Set<PosixFilePermission>, val includeSubFolder: Boolean)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package app.termora.transfer
|
|||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
import app.termora.protocol.PathHandlerRequest
|
|
||||||
import app.termora.protocol.TransferProtocolProvider
|
import app.termora.protocol.TransferProtocolProvider
|
||||||
import app.termora.tree.*
|
import app.termora.tree.*
|
||||||
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
|
import com.formdev.flatlaf.icons.FlatOptionPaneErrorIcon
|
||||||
@@ -24,9 +23,8 @@ import java.util.concurrent.Executors
|
|||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.event.TreeExpansionEvent
|
import javax.swing.event.TreeExpansionEvent
|
||||||
import javax.swing.event.TreeExpansionListener
|
import javax.swing.event.TreeExpansionListener
|
||||||
import kotlin.io.path.absolutePathString
|
|
||||||
|
|
||||||
class TransportSelectionPanel(
|
internal class TransportSelectionPanel(
|
||||||
private val tabbed: TransportTabbed,
|
private val tabbed: TransportTabbed,
|
||||||
private val transferManager: InternalTransferManager,
|
private val transferManager: InternalTransferManager,
|
||||||
) : JPanel(BorderLayout()), Disposable {
|
) : JPanel(BorderLayout()), Disposable {
|
||||||
@@ -99,19 +97,16 @@ class TransportSelectionPanel(
|
|||||||
|
|
||||||
private suspend fun doConnect(host: Host) {
|
private suspend fun doConnect(host: Host) {
|
||||||
|
|
||||||
val provider = TransferProtocolProvider.valueOf(host.protocol)
|
val loader = ReconnectableTransportSupportLoader(owner, host)
|
||||||
if (provider == null) {
|
|
||||||
throw IllegalStateException(I18n.getString("termora.protocol.not-supported", host.protocol))
|
|
||||||
}
|
|
||||||
|
|
||||||
val handler = provider.createPathHandler(PathHandlerRequest(host, owner))
|
// try load
|
||||||
val support = TransportSupport(handler.fileSystem, handler.path.absolutePathString())
|
loader.getTransportSupport()
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
val panel = TransportPanel(transferManager, host, TransportSupportLoader { support })
|
val panel = TransportPanel(transferManager, host, loader)
|
||||||
Disposer.register(panel, object : Disposable {
|
Disposer.register(panel, object : Disposable {
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
Disposer.dispose(handler)
|
Disposer.dispose(loader)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
swingCoroutineScope.launch {
|
swingCoroutineScope.launch {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package app.termora.transfer
|
package app.termora.transfer
|
||||||
|
|
||||||
import java.nio.file.FileSystem
|
import java.nio.file.FileSystem
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
|
||||||
class TransportSupport(
|
internal interface TransportSupport {
|
||||||
val fileSystem: FileSystem,
|
fun getFileSystem(): FileSystem
|
||||||
val path: String
|
fun getDefaultPath(): Path
|
||||||
)
|
}
|
||||||
@@ -1,52 +1,26 @@
|
|||||||
package app.termora.transfer
|
package app.termora.transfer
|
||||||
|
|
||||||
import okio.withLock
|
import app.termora.Disposable
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
|
||||||
import java.util.function.Supplier
|
|
||||||
|
|
||||||
class TransportSupportLoader(private val support: Supplier<TransportSupport>) : Supplier<TransportSupport> {
|
internal interface TransportSupportLoader : Disposable {
|
||||||
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)
|
|
||||||
|
|
||||||
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)) {
|
fun isLoaded(): Boolean
|
||||||
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 isOpened(): Boolean = true
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ import javax.swing.JToolBar
|
|||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
@Suppress("DuplicatedCode")
|
@Suppress("DuplicatedCode")
|
||||||
class TransportTabbed(
|
internal class TransportTabbed(
|
||||||
private val transferManager: TransferManager,
|
private val transferManager: TransferManager,
|
||||||
) : FlatTabbedPane(), Disposable {
|
) : FlatTabbedPane(), Disposable {
|
||||||
private val addBtn = JButton(Icons.add)
|
private val addBtn = JButton(Icons.add)
|
||||||
@@ -64,14 +64,16 @@ class TransportTabbed(
|
|||||||
// 右键菜单
|
// 右键菜单
|
||||||
addMouseListener(object : MouseAdapter() {
|
addMouseListener(object : MouseAdapter() {
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
if (!SwingUtilities.isRightMouseButton(e)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val index = indexAtLocation(e.x, e.y)
|
val index = indexAtLocation(e.x, e.y)
|
||||||
if (index < 0) return
|
if (index < 0) return
|
||||||
|
if (SwingUtilities.isRightMouseButton(e)) {
|
||||||
showContextMenu(index, 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 {
|
private fun tabClose(c: TransportPanel): Boolean {
|
||||||
if (transferManager.getTransferCount() < 1) return true
|
if (transferManager.getTransferCount() < 1) return true
|
||||||
if (c.loader.isLoaded.not()) return false
|
val loader = c.loader
|
||||||
val fileSystem = c.getFileSystem()
|
if (loader.isLoaded().not()) return false
|
||||||
|
val fileSystem = loader.getSyncTransportSupport()
|
||||||
val transfers = transferManager.getTransfers()
|
val transfers = transferManager.getTransfers()
|
||||||
.filter { it.source().fileSystem == fileSystem || it.target().fileSystem == fileSystem }
|
.filter { it.source().fileSystem == fileSystem || it.target().fileSystem == fileSystem }
|
||||||
if (transfers.isEmpty()) return true
|
if (transfers.isEmpty()) return true
|
||||||
@@ -137,8 +140,21 @@ class TransportTabbed(
|
|||||||
|
|
||||||
fun addLocalTab() {
|
fun addLocalTab() {
|
||||||
val host = Host(name = "Local", protocol = LocalProtocolProvider.PROTOCOL)
|
val host = Host(name = "Local", protocol = LocalProtocolProvider.PROTOCOL)
|
||||||
val support = TransportSupport(FileSystems.getDefault(), getDefaultLocalPath())
|
val fs = FileSystems.getDefault()
|
||||||
val panel = TransportPanel(internalTransferManager, host, TransportSupportLoader { support })
|
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)
|
addTab(I18n.getString("termora.transport.local"), panel)
|
||||||
super.setTabClosable(0, false)
|
super.setTabClosable(0, false)
|
||||||
}
|
}
|
||||||
@@ -165,20 +181,30 @@ class TransportTabbed(
|
|||||||
// 编辑
|
// 编辑
|
||||||
val edit = popupMenu.add(I18n.getString("termora.keymgr.edit"))
|
val edit = popupMenu.add(I18n.getString("termora.keymgr.edit"))
|
||||||
edit.addActionListener(object : AnAction() {
|
edit.addActionListener(object : AnAction() {
|
||||||
|
private val hostManager get() = HostManager.getInstance()
|
||||||
private val accountManager get() = AccountManager.getInstance()
|
private val accountManager get() = AccountManager.getInstance()
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val window = evt.window
|
val window = evt.window
|
||||||
val dialog = NewHostDialogV2(
|
val dialog = NewHostDialogV2(
|
||||||
window,
|
window,
|
||||||
panel.host,
|
getHost(panel),
|
||||||
accountOwner = accountManager.getOwners().first { it.id == panel.host.ownerId })
|
accountOwner = accountManager.getOwners().first { it.id == panel.host.ownerId })
|
||||||
dialog.setLocationRelativeTo(window)
|
dialog.setLocationRelativeTo(window)
|
||||||
dialog.title = panel.host.name
|
dialog.title = panel.host.name
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val host = dialog.host ?: return
|
val host = dialog.host ?: return
|
||||||
HostManager.getInstance().addHost(host, DatabaseChangedExtension.Source.Sync)
|
hostManager.addHost(host, DatabaseChangedExtension.Source.User)
|
||||||
setTitleAt(tabIndex, host.name)
|
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.*
|
||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
|
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import java.nio.file.FileSystems
|
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.JOptionPane
|
import javax.swing.JOptionPane
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
class TransportTerminalTab : RememberFocusTerminalTab() {
|
internal class TransportTerminalTab : RememberFocusTerminalTab() {
|
||||||
private val transportViewer = TransportViewer()
|
private val transportViewer = TransportViewer()
|
||||||
private val sftp get() = DatabaseManager.getInstance().sftp
|
private val sftp get() = DatabaseManager.getInstance().sftp
|
||||||
private val transferManager get() = transportViewer.getTransferManager()
|
private val transferManager get() = transportViewer.getTransferManager()
|
||||||
val leftTabbed get() = transportViewer.getLeftTabbed()
|
private val leftTabbed get() = transportViewer.getLeftTabbed()
|
||||||
val rightTabbed get() = transportViewer.getRightTabbed()
|
val rightTabbed get() = transportViewer.getRightTabbed()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -66,8 +66,8 @@ class TransportTerminalTab : RememberFocusTerminalTab() {
|
|||||||
private fun hasActiveTab(tabbed: TransportTabbed): Boolean {
|
private fun hasActiveTab(tabbed: TransportTabbed): Boolean {
|
||||||
for (i in 0 until tabbed.tabCount) {
|
for (i in 0 until tabbed.tabCount) {
|
||||||
val c = tabbed.getComponentAt(i) ?: continue
|
val c = tabbed.getComponentAt(i) ?: continue
|
||||||
if (c is TransportPanel && c.loader.isLoaded) {
|
if (c is TransportPanel && c.loader.isOpened()) {
|
||||||
if (c.getFileSystem() != FileSystems.getDefault()) {
|
if (c.loader.getSyncTransportSupport().getFileSystem().isLocallyFileSystem().not()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,22 +3,19 @@ package app.termora.transfer
|
|||||||
import app.termora.Disposable
|
import app.termora.Disposable
|
||||||
import app.termora.Disposer
|
import app.termora.Disposer
|
||||||
import app.termora.DynamicColor
|
import app.termora.DynamicColor
|
||||||
|
import app.termora.Icons
|
||||||
import app.termora.actions.DataProvider
|
import app.termora.actions.DataProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.event.ComponentAdapter
|
import java.awt.event.ComponentAdapter
|
||||||
import java.awt.event.ComponentEvent
|
import java.awt.event.ComponentEvent
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
||||||
class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
||||||
companion object {
|
|
||||||
private val log = LoggerFactory.getLogger(TransportViewer::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
private val splitPane = JSplitPane()
|
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, leftTabbed)
|
||||||
Disposer.register(this, rightTabbed)
|
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(
|
private fun createInternalTransferManager(
|
||||||
source: TransportTabbed,
|
source: TransportTabbed,
|
||||||
target: TransportTabbed
|
target: TransportTabbed
|
||||||
@@ -118,4 +138,8 @@ class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
|||||||
return rightTabbed
|
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