feat: WSL support on Windows

This commit is contained in:
hstyi
2025-06-25 11:52:53 +08:00
committed by hstyi
parent 01d0f9d4bd
commit 3d47840aa8
27 changed files with 622 additions and 68 deletions

View File

@@ -92,6 +92,10 @@ object Icons {
val terminalUnread by lazy { DynamicIcon("icons/terminalUnread.svg", "icons/terminalUnread_dark.svg") } val terminalUnread by lazy { DynamicIcon("icons/terminalUnread.svg", "icons/terminalUnread_dark.svg") }
val dbPrimitive by lazy { DynamicIcon("icons/dbPrimitive.svg", "icons/dbPrimitive_dark.svg") } val dbPrimitive by lazy { DynamicIcon("icons/dbPrimitive.svg", "icons/dbPrimitive_dark.svg") }
val linux by lazy { DynamicIcon("icons/linux.svg", "icons/linux_dark.svg") } val linux by lazy { DynamicIcon("icons/linux.svg", "icons/linux_dark.svg") }
val debian by lazy { DynamicIcon("icons/debian.svg") }
val fedora by lazy { DynamicIcon("icons/fedora.svg") }
val almalinux by lazy { DynamicIcon("icons/almalinux.svg") }
val ubuntu by lazy { DynamicIcon("icons/ubuntu.svg") }
val success by lazy { DynamicIcon("icons/success.svg", "icons/success_dark.svg") } val success by lazy { DynamicIcon("icons/success.svg", "icons/success_dark.svg") }
val errorDialog by lazy { DynamicIcon("icons/errorDialog.svg", "icons/errorDialog_dark.svg") } val errorDialog by lazy { DynamicIcon("icons/errorDialog.svg", "icons/errorDialog_dark.svg") }
val network by lazy { DynamicIcon("icons/network.svg", "icons/network_dark.svg") } val network by lazy { DynamicIcon("icons/network.svg", "icons/network_dark.svg") }

View File

@@ -59,6 +59,7 @@ class NewHostDialogV2(owner: Window, private val editHost: Host? = null) : Dialo
toolbar.add(Box.createHorizontalGlue()) toolbar.add(Box.createHorizontalGlue())
val extensions = ProtocolHostPanelExtension.extensions val extensions = ProtocolHostPanelExtension.extensions
.filter { it.canCreateProtocolHostPanel() }
for ((index, extension) in extensions.withIndex()) { for ((index, extension) in extensions.withIndex()) {
val protocol = extension.getProtocolProvider().getProtocol() val protocol = extension.getProtocolProvider().getProtocol()
val icon = FlatSVGIcon( val icon = FlatSVGIcon(

View File

@@ -2,7 +2,6 @@ package app.termora.actions
import app.termora.NewHostDialogV2 import app.termora.NewHostDialogV2
import app.termora.tree.HostTreeNode import app.termora.tree.HostTreeNode
import app.termora.tree.NewHostTreeModel
import javax.swing.tree.TreePath import javax.swing.tree.TreePath
class NewHostAction : AnAction() { class NewHostAction : AnAction() {
@@ -38,12 +37,9 @@ class NewHostAction : AnAction() {
) )
val newNode = HostTreeNode(host) val newNode = HostTreeNode(host)
val model = tree.model val model = tree.simpleTreeModel
if (model is NewHostTreeModel) {
model.insertNodeInto(newNode, lastNode, lastNode.childCount) model.insertNodeInto(newNode, lastNode, lastNode.childCount)
tree.selectionPath = TreePath(model.getPathToRoot(newNode)) tree.selectionPath = TreePath(model.getPathToRoot(newNode))
}
} }
} }

View File

@@ -11,6 +11,7 @@ import app.termora.plugin.internal.rdp.RDPInternalPlugin
import app.termora.plugin.internal.serial.SerialInternalPlugin import app.termora.plugin.internal.serial.SerialInternalPlugin
import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin import app.termora.plugin.internal.sftppty.SFTPPtyInternalPlugin
import app.termora.plugin.internal.ssh.SSHInternalPlugin import app.termora.plugin.internal.ssh.SSHInternalPlugin
import app.termora.plugin.internal.wsl.WSLInternalPlugin
import app.termora.swingCoroutineScope import app.termora.swingCoroutineScope
import app.termora.transfer.internal.local.LocalPlugin import app.termora.transfer.internal.local.LocalPlugin
import app.termora.transfer.internal.sftp.SFTPPlugin import app.termora.transfer.internal.sftp.SFTPPlugin
@@ -115,6 +116,10 @@ internal class PluginManager private constructor() {
plugins.add(PluginDescriptor(LocalInternalPlugin(), origin = PluginOrigin.Internal, version = version)) plugins.add(PluginDescriptor(LocalInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// rdp plugin // rdp plugin
plugins.add(PluginDescriptor(RDPInternalPlugin(), origin = PluginOrigin.Internal, version = version)) plugins.add(PluginDescriptor(RDPInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// wsl plugin
// if (SystemUtils.IS_OS_WINDOWS) {
plugins.add(PluginDescriptor(WSLInternalPlugin(), origin = PluginOrigin.Internal, version = version))
// }
// sftp pty plugin // sftp pty plugin
plugins.add(PluginDescriptor(SFTPPtyInternalPlugin(), origin = PluginOrigin.Internal, version = version)) plugins.add(PluginDescriptor(SFTPPtyInternalPlugin(), origin = PluginOrigin.Internal, version = version))

View File

@@ -0,0 +1,8 @@
package app.termora.plugin.internal.wsl
data class WSLDistribution(
val guid: String,
val distributionName: String,
val flavor:String,
val basePath: String,
)

View File

@@ -0,0 +1,298 @@
package app.termora.plugin.internal.wsl
import app.termora.*
import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.ui.FlatTextBorder
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import org.apache.commons.lang3.StringUtils
import java.awt.BorderLayout
import java.awt.Component
import java.awt.KeyboardFocusManager
import java.awt.Window
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.nio.charset.Charset
import javax.swing.*
internal open class WSLHostOptionsPane : OptionsPane() {
protected val generalOption = GeneralOption()
protected val terminalOption = TerminalOption()
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
init {
addOption(generalOption)
addOption(terminalOption)
}
open fun getHost(): Host {
val name = generalOption.nameTextField.text
val protocol = WSLProtocolProvider.PROTOCOL
val host = (generalOption.hostComboBox.selectedItem as WSLDistribution).distributionName
val options = Options.Companion.Default.copy(
encoding = terminalOption.charsetComboBox.selectedItem as String,
env = terminalOption.environmentTextArea.text,
startupCommand = terminalOption.startupCommandTextField.text,
)
return Host(
name = name,
protocol = protocol,
host = host,
options = options,
sort = System.currentTimeMillis(),
remark = generalOption.remarkTextArea.text,
)
}
fun setHost(host: Host) {
generalOption.nameTextField.text = host.name
generalOption.hostComboBox.selectedItem = host.host
generalOption.remarkTextArea.text = host.remark
generalOption.hostComboBox.selectedItem = null
terminalOption.startupCommandTextField.text = host.options.startupCommand
terminalOption.environmentTextArea.text = host.options.env
terminalOption.charsetComboBox.selectedItem = host.options.encoding
for (i in 0 until generalOption.hostComboBox.itemCount) {
if (generalOption.hostComboBox.getItemAt(i).distributionName == host.host) {
generalOption.hostComboBox.selectedIndex = i
break
}
}
}
fun validateFields(): Boolean {
// general
return (validateField(generalOption.nameTextField)
|| validateField(generalOption.hostComboBox)).not()
}
/**
* 返回 true 表示有错误
*/
private fun validateField(textField: JTextField): Boolean {
if (textField.isEnabled && textField.text.isBlank()) {
setOutlineError(textField)
return true
}
return false
}
/**
* 返回 true 表示有错误
*/
private fun validateField(comboBox: JComboBox<*>): Boolean {
val selectedItem = comboBox.selectedItem
if (comboBox.isEnabled && (selectedItem == null || (selectedItem is String && selectedItem.isBlank()))) {
selectOptionJComponent(comboBox)
comboBox.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
comboBox.requestFocusInWindow()
return true
}
return false
}
private fun setOutlineError(textField: JTextField) {
selectOptionJComponent(textField)
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
textField.requestFocusInWindow()
}
protected inner class GeneralOption : JPanel(BorderLayout()), Option {
val nameTextField = OutlineTextField(128)
val hostComboBox = OutlineComboBox<WSLDistribution>()
val remarkTextArea = FixedLengthTextArea(512)
init {
initView()
initEvents()
}
private fun initView() {
hostComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component? {
val text = if (value is WSLDistribution) value.distributionName else value
val c = super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
icon = null
if (value is WSLDistribution) {
icon = if (StringUtils.containsIgnoreCase(value.flavor, "debian")) {
Icons.debian
} else if (StringUtils.containsIgnoreCase(value.flavor, "ubuntu")) {
Icons.ubuntu
} else if (StringUtils.containsIgnoreCase(value.flavor, "fedora")) {
Icons.fedora
} else if (StringUtils.containsIgnoreCase(value.flavor, "alma")) {
Icons.almalinux
} else {
Icons.linux
}
}
return c
}
}
add(getCenterComponent(), BorderLayout.CENTER)
}
private fun initEvents() {
for (distribution in WSLSupport.getDistributions()) {
hostComboBox.addItem(distribution)
}
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
removeComponentListener(this)
}
})
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.settings
}
override fun getTitle(): String {
return I18n.getString("termora.new-host.general")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
remarkTextArea.setFocusTraversalKeys(
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
)
remarkTextArea.setFocusTraversalKeys(
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
)
remarkTextArea.rows = 8
remarkTextArea.lineWrap = true
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
var rows = 1
val step = 2
val panel = FormBuilder.create().layout(layout).debug(false)
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
.add(nameTextField).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.wsl.distribution")}:").xy(1, rows)
.add(hostComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
.xy(3, rows).apply { rows += step }
.build()
return panel
}
}
protected inner class TerminalOption : JPanel(BorderLayout()), Option {
val charsetComboBox = JComboBox<String>()
val startupCommandTextField = OutlineTextField()
val environmentTextArea = FixedLengthTextArea(2048)
init {
initView()
initEvents()
}
private fun initView() {
add(getCenterComponent(), BorderLayout.CENTER)
startupCommandTextField.placeholderText = "--cd ~"
environmentTextArea.setFocusTraversalKeys(
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
)
environmentTextArea.setFocusTraversalKeys(
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
)
environmentTextArea.rows = 8
environmentTextArea.lineWrap = true
environmentTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
for (e in Charset.availableCharsets()) {
charsetComboBox.addItem(e.key)
}
charsetComboBox.selectedItem = "UTF-8"
}
private fun initEvents() {
}
override fun getIcon(isSelected: Boolean): Icon {
return Icons.terminal
}
override fun getTitle(): String {
return I18n.getString("termora.new-host.terminal")
}
override fun getJComponent(): JComponent {
return this
}
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
var rows = 1
val step = 2
val panel = FormBuilder.create().layout(layout)
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
.add(charsetComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.startup-commands")}:").xy(1, rows)
.add(startupCommandTextField).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.terminal.env")}:").xy(1, rows)
.add(JScrollPane(environmentTextArea).apply { border = FlatTextBorder() }).xy(3, rows)
.apply { rows += step }
.build()
return panel
}
}
}

View File

@@ -0,0 +1,58 @@
package app.termora.plugin.internal.wsl
import app.termora.Host
import app.termora.PtyConnectorFactory
import app.termora.PtyHostTerminalTab
import app.termora.WindowScope
import app.termora.terminal.PtyConnector
import org.apache.commons.io.Charsets
import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.StringUtils
import java.nio.charset.StandardCharsets
import java.util.regex.Pattern
class WSLHostTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
companion object {
fun parseCommand(command: String): List<String> {
val result = mutableListOf<String>()
val matcher = Pattern.compile("\"([^\"]*)\"|(\\S+)").matcher(command)
while (matcher.find()) {
if (matcher.group(1) != null) {
result.add(matcher.group(1)) // 处理双引号部分
} else {
result.add(matcher.group(2).replace("\\\\ ", " "))
}
}
return result
}
}
override suspend fun openPtyConnector(): PtyConnector {
val winSize = terminalPanel.winSize()
val drive = System.getenv("SystemRoot")
val wsl = FileUtils.getFile(drive, "System32", "wsl.exe").absolutePath
val commands = mutableListOf<String>()
commands.add(wsl)
commands.add("-d")
commands.add(host.host)
if (StringUtils.isNoneBlank(host.options.startupCommand)) {
commands.addAll(parseCommand(host.options.startupCommand))
}
val ptyConnector = PtyConnectorFactory.getInstance().createPtyConnector(
commands = commands.toTypedArray(),
rows = winSize.rows, cols = winSize.cols,
env = host.options.envs(),
charset = Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8),
)
return ptyConnector
}
override fun sendStartupCommand(ptyConnector: PtyConnector, bytes: ByteArray) {
// Nothing
}
}

View File

@@ -0,0 +1,24 @@
package app.termora.plugin.internal.wsl
import app.termora.plugin.Extension
import app.termora.plugin.InternalPlugin
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProviderExtension
internal class WSLInternalPlugin : InternalPlugin() {
init {
support.addExtension(ProtocolProviderExtension::class.java) { WSLProtocolProviderExtension.instance }
support.addExtension(ProtocolHostPanelExtension::class.java) { WSLProtocolHostPanelExtension.instance }
}
override fun getName(): String {
return "WSL Protocol"
}
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
return support.getExtensions(clazz)
}
}

View File

@@ -0,0 +1,36 @@
package app.termora.plugin.internal.wsl
import app.termora.Disposer
import app.termora.Host
import app.termora.protocol.ProtocolHostPanel
import java.awt.BorderLayout
class WSLProtocolHostPanel : ProtocolHostPanel() {
private val pane = WSLHostOptionsPane()
init {
initView()
initEvents()
}
private fun initView() {
add(pane, BorderLayout.CENTER)
Disposer.register(this, pane)
}
private fun initEvents() {}
override fun getHost(): Host {
return pane.getHost()
}
override fun setHost(host: Host) {
pane.setHost(host)
}
override fun validateFields(): Boolean {
return pane.validateFields()
}
}

View File

@@ -0,0 +1,24 @@
package app.termora.plugin.internal.wsl
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
internal class WSLProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
companion object {
val instance by lazy { WSLProtocolHostPanelExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return WSLProtocolProvider.instance
}
override fun canCreateProtocolHostPanel(): Boolean {
return WSLSupport.isSupported
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
return WSLProtocolHostPanel()
}
}

View File

@@ -0,0 +1,30 @@
package app.termora.plugin.internal.wsl
import app.termora.*
import app.termora.actions.DataProvider
import app.termora.protocol.GenericProtocolProvider
internal class WSLProtocolProvider private constructor() : GenericProtocolProvider {
companion object {
val instance by lazy { WSLProtocolProvider() }
const val PROTOCOL = "WSL"
}
override fun getProtocol(): String {
return PROTOCOL
}
override fun createTerminalTab(dataProvider: DataProvider, windowScope: WindowScope, host: Host): TerminalTab {
return WSLHostTerminalTab(windowScope, host)
}
override fun getIcon(width: Int, height: Int): DynamicIcon {
return Icons.linux
}
override fun canCreateTerminalTab(dataProvider: DataProvider, windowScope: WindowScope, host: Host): Boolean {
return WSLSupport.isSupported
}
}

View File

@@ -0,0 +1,14 @@
package app.termora.plugin.internal.wsl
import app.termora.protocol.ProtocolProvider
import app.termora.protocol.ProtocolProviderExtension
internal class WSLProtocolProviderExtension private constructor() : ProtocolProviderExtension {
companion object {
val instance by lazy { WSLProtocolProviderExtension() }
}
override fun getProtocolProvider(): ProtocolProvider {
return WSLProtocolProvider.instance
}
}

View File

@@ -0,0 +1,45 @@
package app.termora.plugin.internal.wsl
import com.formdev.flatlaf.util.SystemInfo
import com.sun.jna.platform.win32.Advapi32Util
import com.sun.jna.platform.win32.WinReg
import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.StringUtils
object WSLSupport {
val isSupported by lazy { checkSupported() }
private fun checkSupported(): Boolean {
if (SystemInfo.isWindows.not()) return false
val drive = System.getenv("SystemRoot") ?: return false
val wsl = FileUtils.getFile(drive, "System32", "wsl.exe")
return wsl.exists()
}
fun getDistributions(): List<WSLDistribution> {
if (isSupported.not()) return emptyList()
val baseKeyPath = "Software\\Microsoft\\Windows\\CurrentVersion\\Lxss"
val guids = Advapi32Util.registryGetKeys(WinReg.HKEY_CURRENT_USER, baseKeyPath)
val distributions = mutableListOf<WSLDistribution>()
for (guid in guids) {
val key = baseKeyPath + "\\" + guid
val distroName = Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "DistributionName")
val basePath = Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "BasePath")
val flavor = Advapi32Util.registryGetStringValue(WinReg.HKEY_CURRENT_USER, key, "Flavor")
if (StringUtils.isAnyBlank(distroName, guid, basePath, flavor)) continue
distributions.add(
WSLDistribution(
guid = guid,
flavor = flavor,
basePath = basePath,
distributionName = distroName
)
)
}
return distributions
}
}

View File

@@ -16,6 +16,11 @@ interface ProtocolHostPanelExtension : Extension {
*/ */
fun getProtocolProvider(): ProtocolProvider fun getProtocolProvider(): ProtocolProvider
/**
* 是否可以创建协议主机面板
*/
fun canCreateProtocolHostPanel(): Boolean = true
/** /**
* 创建协议主机面板 * 创建协议主机面板
*/ */

View File

@@ -15,7 +15,7 @@ import javax.swing.SwingUtilities
import javax.swing.tree.TreePath import javax.swing.tree.TreePath
class SnippetTree : SimpleTree() { class SnippetTree : SimpleTree() {
override val model = SnippetTreeModel() override val simpleTreeModel = SnippetTreeModel()
private val snippetManager get() = SnippetManager.getInstance() private val snippetManager get() = SnippetManager.getInstance()
@@ -25,7 +25,7 @@ class SnippetTree : SimpleTree() {
} }
private fun initViews() { private fun initViews() {
super.setModel(model) super.setModel(simpleTreeModel)
isEditable = true isEditable = true
dragEnabled = true dragEnabled = true
dropMode = DropMode.ON_OR_INSERT dropMode = DropMode.ON_OR_INSERT
@@ -68,16 +68,16 @@ class SnippetTree : SimpleTree() {
newFile(SnippetTreeNode(snippet)) newFile(SnippetTreeNode(snippet))
} }
rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) } rename.addActionListener { startEditingAtPath(TreePath(simpleTreeModel.getPathToRoot(lastNode))) }
refresh.addActionListener { model.reload(lastNode) } refresh.addActionListener { simpleTreeModel.reload(lastNode) }
expandAll.addActionListener { expandAll.addActionListener {
for (node in getSelectionSimpleTreeNodes(true)) { for (node in getSelectionSimpleTreeNodes(true)) {
expandPath(TreePath(model.getPathToRoot(node))) expandPath(TreePath(simpleTreeModel.getPathToRoot(node)))
} }
} }
colspanAll.addActionListener { colspanAll.addActionListener {
for (node in getSelectionSimpleTreeNodes(true).reversed()) { for (node in getSelectionSimpleTreeNodes(true).reversed()) {
collapsePath(TreePath(model.getPathToRoot(node))) collapsePath(TreePath(simpleTreeModel.getPathToRoot(node)))
} }
} }
remove.addActionListener(object : AnAction() { remove.addActionListener(object : AnAction() {
@@ -94,7 +94,7 @@ class SnippetTree : SimpleTree() {
) { ) {
for (c in nodes) { for (c in nodes) {
snippetManager.addSnippet(c.data.copy(deleted = true, updateDate = System.currentTimeMillis())) snippetManager.addSnippet(c.data.copy(deleted = true, updateDate = System.currentTimeMillis()))
model.removeNodeFromParent(c) simpleTreeModel.removeNodeFromParent(c)
// 将所有子孙也删除 // 将所有子孙也删除
for (child in c.getAllChildren()) { for (child in c.getAllChildren()) {
snippetManager.addSnippet( snippetManager.addSnippet(
@@ -110,7 +110,7 @@ class SnippetTree : SimpleTree() {
}) })
rename.isEnabled = lastNode != model.root rename.isEnabled = lastNode != simpleTreeModel.root
remove.isEnabled = rename.isEnabled remove.isEnabled = rename.isEnabled
newFolder.isEnabled = lastNode.data.type == SnippetType.Folder newFolder.isEnabled = lastNode.data.type == SnippetType.Folder
newSnippet.isEnabled = newFolder.isEnabled newSnippet.isEnabled = newFolder.isEnabled
@@ -130,18 +130,18 @@ class SnippetTree : SimpleTree() {
val n = node as? SnippetTreeNode ?: return val n = node as? SnippetTreeNode ?: return
n.data = n.data.copy(name = text, updateDate = System.currentTimeMillis()) n.data = n.data.copy(name = text, updateDate = System.currentTimeMillis())
snippetManager.addSnippet(n.data) snippetManager.addSnippet(n.data)
model.nodeStructureChanged(n) simpleTreeModel.nodeStructureChanged(n)
} }
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>, index: Int) { override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>, index: Int) {
// 从原来的父移除 // 从原来的父移除
model.removeNodeFromParent(node) simpleTreeModel.removeNodeFromParent(node)
val nNode = node as? SnippetTreeNode ?: return val nNode = node as? SnippetTreeNode ?: return
val nParent = parent as? SnippetTreeNode ?: return val nParent = parent as? SnippetTreeNode ?: return
nNode.data = nNode.data.copy(parentId = nParent.data.id, updateDate = System.currentTimeMillis()) nNode.data = nNode.data.copy(parentId = nParent.data.id, updateDate = System.currentTimeMillis())
model.insertNodeInto(nNode, nParent, index) simpleTreeModel.insertNodeInto(nNode, nParent, index)
} }
override fun getSelectionSimpleTreeNodes(include: Boolean): List<SnippetTreeNode> { override fun getSelectionSimpleTreeNodes(include: Boolean): List<SnippetTreeNode> {

View File

@@ -5,6 +5,7 @@ import app.termora.*
import app.termora.actions.DataProvider import app.termora.actions.DataProvider
import app.termora.database.DatabaseManager import app.termora.database.DatabaseManager
import app.termora.plugin.ExtensionManager import app.termora.plugin.ExtensionManager
import app.termora.plugin.internal.wsl.WSLHostTerminalTab
import app.termora.transfer.TransportTableModel.Attributes import app.termora.transfer.TransportTableModel.Attributes
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatToolBar import com.formdev.flatlaf.extras.components.FlatToolBar
@@ -47,7 +48,6 @@ import java.util.*
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future import java.util.concurrent.Future
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.regex.Pattern
import java.util.stream.Stream import java.util.stream.Stream
import javax.swing.* import javax.swing.*
import javax.swing.TransferHandler import javax.swing.TransferHandler
@@ -952,7 +952,7 @@ class TransportPanel(
val p = localPath.absolutePathString() val p = localPath.absolutePathString()
if (editCommand.isNotBlank()) { if (editCommand.isNotBlank()) {
ProcessBuilder(parseCommand(MessageFormat.format(editCommand, p))).start() ProcessBuilder(WSLHostTerminalTab.parseCommand(MessageFormat.format(editCommand, p))).start()
} else if (SystemInfo.isMacOS) { } else if (SystemInfo.isMacOS) {
ProcessBuilder("open", "-a", "TextEdit", "-W", p).start().onExit() ProcessBuilder("open", "-a", "TextEdit", "-W", p).start().onExit()
.whenComplete { _, _ -> if (disposed.get().not()) Disposer.dispose(disposable) } .whenComplete { _, _ -> if (disposed.get().not()) Disposer.dispose(disposable) }
@@ -973,20 +973,6 @@ class TransportPanel(
return disposable return disposable
} }
private fun parseCommand(command: String): List<String> {
val result = mutableListOf<String>()
val matcher = Pattern.compile("\"([^\"]*)\"|(\\S+)").matcher(command)
while (matcher.find()) {
if (matcher.group(1) != null) {
result.add(matcher.group(1)) // 处理双引号部分
} else {
result.add(matcher.group(2).replace("\\\\ ", " "))
}
}
return result
}
override fun dispose() { override fun dispose() {
transferIds.clear() transferIds.clear()
} }

View File

@@ -73,7 +73,7 @@ class NewHostTree : SimpleTree(), Disposable {
get() = enableManager.isShowTags() get() = enableManager.isShowTags()
set(value) = enableManager.setShowTags(value) set(value) = enableManager.setShowTags(value)
private var isPopupMenu = false private var isPopupMenu = false
override val model = NewHostTreeModel.getInstance() override val simpleTreeModel = NewHostTreeModel.getInstance()
/** /**
* 是否允许显示右键菜单 * 是否允许显示右键菜单
@@ -95,7 +95,7 @@ class NewHostTree : SimpleTree(), Disposable {
} }
private fun initViews() { private fun initViews() {
super.setModel(model) super.setModel(simpleTreeModel)
isEditable = true isEditable = true
dragEnabled = true dragEnabled = true
isRootVisible = false isRootVisible = false
@@ -124,7 +124,7 @@ class NewHostTree : SimpleTree(), Disposable {
if (e.keyCode == KeyEvent.VK_ENTER && doubleClickConnection) { if (e.keyCode == KeyEvent.VK_ENTER && doubleClickConnection) {
val nodes = getSelectionSimpleTreeNodes() val nodes = getSelectionSimpleTreeNodes()
if (nodes.size == 1 && nodes.first().isFolder) { if (nodes.size == 1 && nodes.first().isFolder) {
val path = TreePath(model.getPathToRoot(nodes.first())) val path = TreePath(simpleTreeModel.getPathToRoot(nodes.first()))
if (isExpanded(path)) { if (isExpanded(path)) {
collapsePath(path) collapsePath(path)
} else { } else {
@@ -161,7 +161,7 @@ class NewHostTree : SimpleTree(), Disposable {
override fun canImport(support: TransferHandler.TransferSupport): Boolean { override fun canImport(support: TransferHandler.TransferSupport): Boolean {
val dropLocation = support.dropLocation as? DropLocation ?: return false val dropLocation = support.dropLocation as? DropLocation ?: return false
val node = dropLocation.path.lastPathComponent as? SimpleTreeNode<*> ?: return false val node = dropLocation.path.lastPathComponent as? SimpleTreeNode<*> ?: return false
return node != model.getRoot() return node != simpleTreeModel.getRoot()
} }
override fun canCreateTransferable(c: JComponent): Boolean { override fun canCreateTransferable(c: JComponent): Boolean {
@@ -178,7 +178,7 @@ class NewHostTree : SimpleTree(), Disposable {
val tags = TagManager.getInstance().getTags(lastNode.host.ownerId) val tags = TagManager.getInstance().getTags(lastNode.host.ownerId)
val nodes = getSelectionSimpleTreeNodes() val nodes = getSelectionSimpleTreeNodes()
val fullNodes = getSelectionSimpleTreeNodes(true) val fullNodes = getSelectionSimpleTreeNodes(true)
val lastNodeParent = lastNode.parent ?: model.root val lastNodeParent = lastNode.parent ?: simpleTreeModel.root
val lastHost = lastNode.host val lastHost = lastNode.host
val hasTeamNode = nodes.any { it is TeamTreeNode } val hasTeamNode = nodes.any { it is TeamTreeNode }
@@ -252,8 +252,8 @@ class NewHostTree : SimpleTree(), Disposable {
parentId = lastNode.id, parentId = lastNode.id,
) )
val node = HostTreeNode(host) val node = HostTreeNode(host)
model.insertNodeInto(node, lastNode, lastNode.folderCount) simpleTreeModel.insertNodeInto(node, lastNode, lastNode.folderCount)
selectionPath = TreePath(model.getPathToRoot(node)) selectionPath = TreePath(simpleTreeModel.getPathToRoot(node))
startEditingAtPath(selectionPath) startEditingAtPath(selectionPath)
} }
remove.addActionListener(object : ActionListener { remove.addActionListener(object : ActionListener {
@@ -268,7 +268,7 @@ class NewHostTree : SimpleTree(), Disposable {
) == JOptionPane.YES_OPTION ) == JOptionPane.YES_OPTION
) { ) {
for (c in nodes) { for (c in nodes) {
model.removeNodeFromParent(c) simpleTreeModel.removeNodeFromParent(c)
} }
} }
} }
@@ -278,20 +278,20 @@ class NewHostTree : SimpleTree(), Disposable {
val p = c.parent ?: continue val p = c.parent ?: continue
val newNode = copyNode(c, p.host.id) val newNode = copyNode(c, p.host.id)
// 先入 Model // 先入 Model
model.insertNodeInto(newNode, p, lastNodeParent.getIndex(c) + 1) simpleTreeModel.insertNodeInto(newNode, p, lastNodeParent.getIndex(c) + 1)
// 开启编辑 // 开启编辑
selectionPath = TreePath(model.getPathToRoot(newNode)) selectionPath = TreePath(simpleTreeModel.getPathToRoot(newNode))
} }
} }
rename.addActionListener { startEditingAtPath(TreePath(model.getPathToRoot(lastNode))) } rename.addActionListener { startEditingAtPath(TreePath(simpleTreeModel.getPathToRoot(lastNode))) }
expandAll.addActionListener { expandAll.addActionListener {
for (node in fullNodes) { for (node in fullNodes) {
expandPath(TreePath(model.getPathToRoot(node))) expandPath(TreePath(simpleTreeModel.getPathToRoot(node)))
} }
} }
colspanAll.addActionListener { colspanAll.addActionListener {
for (node in fullNodes.reversed()) { for (node in fullNodes.reversed()) {
collapsePath(TreePath(model.getPathToRoot(node))) collapsePath(TreePath(simpleTreeModel.getPathToRoot(node)))
} }
} }
newHost.addActionListener(object : ActionListener { newHost.addActionListener(object : ActionListener {
@@ -306,8 +306,8 @@ class NewHostTree : SimpleTree(), Disposable {
) )
val newNode = HostTreeNode(host) val newNode = HostTreeNode(host)
model.insertNodeInto(newNode, lastNode, lastNode.childCount) simpleTreeModel.insertNodeInto(newNode, lastNode, lastNode.childCount)
selectionPath = TreePath(model.getPathToRoot(newNode)) selectionPath = TreePath(simpleTreeModel.getPathToRoot(newNode))
} }
}) })
property.addActionListener(object : ActionListener { property.addActionListener(object : ActionListener {
@@ -318,10 +318,10 @@ class NewHostTree : SimpleTree(), Disposable {
dialog.isVisible = true dialog.isVisible = true
val host = dialog.host ?: return val host = dialog.host ?: return
lastNode.host = host lastNode.host = host
model.nodeStructureChanged(lastNode) simpleTreeModel.nodeStructureChanged(lastNode)
} }
}) })
refresh.addActionListener { model.reload(lastNode) } refresh.addActionListener { simpleTreeModel.reload(lastNode) }
newMenu.isEnabled = lastNode.isFolder newMenu.isEnabled = lastNode.isFolder
remove.isEnabled = getSelectionSimpleTreeNodes().none { it.id == "0" } && hasTeamNode.not() remove.isEnabled = getSelectionSimpleTreeNodes().none { it.id == "0" } && hasTeamNode.not()
@@ -353,7 +353,7 @@ class NewHostTree : SimpleTree(), Disposable {
tags.add(tag.id) tags.add(tag.id)
} }
lastNode.host = lastHost.copy(options = lastHost.options.copy(tags = tags)) lastNode.host = lastHost.copy(options = lastHost.options.copy(tags = tags))
model.nodeStructureChanged(lastNode) simpleTreeModel.nodeStructureChanged(lastNode)
} }
} }
@@ -387,7 +387,7 @@ class NewHostTree : SimpleTree(), Disposable {
override fun onRenamed(node: SimpleTreeNode<*>, text: String) { override fun onRenamed(node: SimpleTreeNode<*>, text: String) {
val lastNode = node as? HostTreeNode ?: return val lastNode = node as? HostTreeNode ?: return
lastNode.host = lastNode.host.copy(name = text) lastNode.host = lastNode.host.copy(name = text)
model.nodeStructureChanged(lastNode) simpleTreeModel.nodeStructureChanged(lastNode)
} }
override fun createTreeModelListener(): TreeModelListener { override fun createTreeModelListener(): TreeModelListener {
@@ -402,7 +402,7 @@ class NewHostTree : SimpleTree(), Disposable {
override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>, index: Int) { override fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>, index: Int) {
if (parent !is HostTreeNode || node !is HostTreeNode) return if (parent !is HostTreeNode || node !is HostTreeNode) return
// 从原来的父移除 // 从原来的父移除
model.removeNodeFromParent(node) simpleTreeModel.removeNodeFromParent(node)
node.data = node.data.copy( node.data = node.data.copy(
id = randomUUID(), id = randomUUID(),
@@ -411,7 +411,7 @@ class NewHostTree : SimpleTree(), Disposable {
ownerType = parent.host.ownerType, ownerType = parent.host.ownerType,
) )
model.insertNodeInto(node, parent, index) simpleTreeModel.insertNodeInto(node, parent, index)
// 子也需要变基 // 子也需要变基
for ((idx, e) in node.childrenNode().withIndex()) { for ((idx, e) in node.childrenNode().withIndex()) {
@@ -445,7 +445,7 @@ class NewHostTree : SimpleTree(), Disposable {
if (host.isFolder) { if (host.isFolder) {
for (child in node.children()) { for (child in node.children()) {
if (child is HostTreeNode) { if (child is HostTreeNode) {
model.insertNodeInto( simpleTreeModel.insertNodeInto(
copyNode(child, newHost.id, idGenerator, level + 1), copyNode(child, newHost.id, idGenerator, level + 1),
newNode, node.getIndex(child) newNode, node.getIndex(child)
) )
@@ -650,10 +650,10 @@ class NewHostTree : SimpleTree(), Disposable {
} }
// 重新加载 // 重新加载
model.reload(folder) simpleTreeModel.reload(folder)
// expand root // expand root
expandPath(TreePath(model.getPathToRoot(folder))) expandPath(TreePath(simpleTreeModel.getPathToRoot(folder)))
} }
private fun parseFromWindTerm(folder: HostTreeNode, file: File): List<HostTreeNode> { private fun parseFromWindTerm(folder: HostTreeNode, file: File): List<HostTreeNode> {

View File

@@ -5,6 +5,7 @@ import app.termora.plugin.internal.extension.DynamicExtensionHandler
import app.termora.plugin.internal.rdp.RDPProtocolProvider import app.termora.plugin.internal.rdp.RDPProtocolProvider
import app.termora.plugin.internal.serial.SerialProtocolProvider import app.termora.plugin.internal.serial.SerialProtocolProvider
import app.termora.plugin.internal.ssh.SSHProtocolProvider import app.termora.plugin.internal.ssh.SSHProtocolProvider
import app.termora.plugin.internal.wsl.WSLProtocolProvider
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import java.awt.Graphics2D import java.awt.Graphics2D
import javax.swing.JComponent import javax.swing.JComponent
@@ -32,6 +33,12 @@ class ShowMoreInfoSimpleTreeCellRendererExtension private constructor() : Simple
.let { Disposer.register(this, it) } .let { Disposer.register(this, it) }
} }
// wsl
// key: guid
// value: name
private val map = mutableMapOf<String, String?>()
@Suppress("CascadeIf")
override fun createAnnotations( override fun createAnnotations(
tree: JTree, tree: JTree,
value: Any?, value: Any?,
@@ -58,6 +65,8 @@ class ShowMoreInfoSimpleTreeCellRendererExtension private constructor() : Simple
} }
} else if (host.protocol == SerialProtocolProvider.PROTOCOL) { } else if (host.protocol == SerialProtocolProvider.PROTOCOL) {
text = host.options.serialComm.port text = host.options.serialComm.port
} else if (host.protocol == WSLProtocolProvider.PROTOCOL) {
text = host.host
} }
} }

View File

@@ -22,7 +22,7 @@ import kotlin.math.min
open class SimpleTree : JXTree() { open class SimpleTree : JXTree() {
protected open val model get() = super.getModel() as SimpleTreeModel<*> open val simpleTreeModel get() = super.getModel() as SimpleTreeModel<*>
private val editor = OutlineTextField(64) private val editor = OutlineTextField(64)
protected val tree get() = this protected val tree get() = this
@@ -123,12 +123,12 @@ open class SimpleTree : JXTree() {
if (tree.canCreateTransferable(c).not()) return null if (tree.canCreateTransferable(c).not()) return null
val nodes = getSelectionSimpleTreeNodes().toMutableList() val nodes = getSelectionSimpleTreeNodes().toMutableList()
if (nodes.isEmpty()) return null if (nodes.isEmpty()) return null
if (nodes.contains(model.root)) return null if (nodes.contains(simpleTreeModel.root)) return null
val iterator = nodes.iterator() val iterator = nodes.iterator()
while (iterator.hasNext()) { while (iterator.hasNext()) {
val node = iterator.next() val node = iterator.next()
val parents = model.getPathToRoot(node).filter { it != node } val parents = simpleTreeModel.getPathToRoot(node).filter { it != node }
if (parents.any { nodes.contains(it) }) { if (parents.any { nodes.contains(it) }) {
iterator.remove() iterator.remove()
} }
@@ -211,11 +211,11 @@ open class SimpleTree : JXTree() {
} }
rebase(e, node, min(index, node.childCount)) rebase(e, node, min(index, node.childCount))
selectionPath = TreePath(model.getPathToRoot(e)) selectionPath = TreePath(simpleTreeModel.getPathToRoot(e))
} }
// 先展开最顶级的 // 先展开最顶级的
expandPath(TreePath(model.getPathToRoot(node))) expandPath(TreePath(simpleTreeModel.getPathToRoot(node)))
return true return true
} }
@@ -245,8 +245,8 @@ open class SimpleTree : JXTree() {
private fun newNode(newNode: SimpleTreeNode<*>, index: Int): Boolean { private fun newNode(newNode: SimpleTreeNode<*>, index: Int): Boolean {
val lastNode = lastSelectedPathComponent val lastNode = lastSelectedPathComponent
if (lastNode !is SimpleTreeNode<*>) return false if (lastNode !is SimpleTreeNode<*>) return false
model.insertNodeInto(newNode, lastNode, index) simpleTreeModel.insertNodeInto(newNode, lastNode, index)
selectionPath = TreePath(model.getPathToRoot(newNode)) selectionPath = TreePath(simpleTreeModel.getPathToRoot(newNode))
startEditingAtPath(selectionPath) startEditingAtPath(selectionPath)
return true return true
} }
@@ -291,7 +291,7 @@ open class SimpleTree : JXTree() {
} }
protected open fun isCellEditable(e: EventObject?): Boolean { protected open fun isCellEditable(e: EventObject?): Boolean {
return getLastSelectedPathNode() != model.root return getLastSelectedPathNode() != simpleTreeModel.root
} }
protected open fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>, index: Int) { protected open fun rebase(node: SimpleTreeNode<*>, parent: SimpleTreeNode<*>, index: Int) {

View File

@@ -87,9 +87,9 @@ open class SimpleTreeCellRenderer : DefaultTreeCellRenderer() {
val icon = this.icon val icon = this.icon
if (icon is DynamicIcon && FlatLaf.isLafDark().not()) { if (icon is DynamicIcon && FlatLaf.isLafDark().not()) {
val oldColorFilter = icon.colorFilter val oldColorFilter = icon.colorFilter
icon.colorFilter = colorFilter // icon.colorFilter = colorFilter
icon.paintIcon(c, g, x, y) icon.paintIcon(c, g, x, y)
icon.colorFilter = oldColorFilter // icon.colorFilter = oldColorFilter
return return
} }
} }

View File

@@ -212,6 +212,9 @@ termora.new-host.tunneling.delete=${termora.remove}
termora.new-host.rdp.desktop-placeholder=Default full screen (e.g. 1920×1080) termora.new-host.rdp.desktop-placeholder=Default full screen (e.g. 1920×1080)
termora.new-host.rdp.resolution=Resolution termora.new-host.rdp.resolution=Resolution
termora.new-host.wsl.distribution=DistroName
termora.new-host.test-connection=Test Connection termora.new-host.test-connection=Test Connection
termora.new-host.test-connection-successful=Connection successful termora.new-host.test-connection-successful=Connection successful

View File

@@ -205,6 +205,8 @@ termora.new-host.tunneling.delete=${termora.remove}
termora.new-host.rdp.desktop-placeholder=默认全屏例如1920×1080 termora.new-host.rdp.desktop-placeholder=默认全屏例如1920×1080
termora.new-host.rdp.resolution=分辨率 termora.new-host.rdp.resolution=分辨率
termora.new-host.wsl.distribution=分发版
termora.new-host.jump-hosts=跳板机 termora.new-host.jump-hosts=跳板机
# Key manager # Key manager

View File

@@ -204,6 +204,8 @@ termora.new-host.tunneling.delete=${termora.remove}
termora.new-host.rdp.desktop-placeholder=預設全螢幕例如1920×1080 termora.new-host.rdp.desktop-placeholder=預設全螢幕例如1920×1080
termora.new-host.rdp.resolution=解析度 termora.new-host.rdp.resolution=解析度
termora.new-host.wsl.distribution=分發版
termora.new-host.jump-hosts=跳板機 termora.new-host.jump-hosts=跳板機
# Key manager # Key manager

View File

@@ -0,0 +1 @@
<svg t="1750821026851" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2837" width="16" height="16"><path d="M892.32 569.44c38.624-2.976 69.824 22.272 72.8 60.896 2.976 40.128-25.28 72.8-63.872 75.776a68.032 68.032 0 0 1-72.8-62.4c-2.976-40.096 23.776-69.824 63.872-74.272z" fill="#86DA2F" p-id="2838"></path><path d="M422.848 885.888c0-38.624 29.728-68.352 65.376-68.352s69.824 32.704 69.824 69.824c0 35.68-29.696 66.88-63.872 68.352-43.072 0-71.328-26.752-71.328-69.824z" fill="#24C2FF" p-id="2839"></path><path d="M528.32 452.064c-5.92 2.976-8.896-1.472-10.4-5.92-54.944-102.528-38.624-231.776 57.952-309.024 25.28-20.8 72.8-25.28 93.6-4.48 8.928 7.456 10.4 16.352 11.872 26.752 2.976 22.304 7.424 44.576 22.304 62.4 16.32 19.328 37.12 26.752 60.896 25.28 20.8 0 41.6-2.976 54.976 20.8 7.424 13.344 4.48 65.344-7.424 75.744-5.952 4.48-10.4 1.504-14.848 0-34.176-13.376-69.824-13.376-105.504-7.424-11.872 1.472-17.824-1.472-17.824-14.848-1.472-22.304-5.92-43.104-17.824-62.4-22.272-40.128-63.872-41.6-90.624-4.48-22.272 29.76-28.224 65.376-34.176 101.056-5.92 31.2-4.448 63.872-2.976 96.544 0 0-1.472 0 0 0z" fill="#FFCB12" p-id="2840"></path><path d="M565.472 474.368c-2.976-4.48-1.472-8.928 2.976-11.904 84.672-77.248 210.976-92.096 309.024-16.32 25.248 20.8 41.6 63.872 28.224 89.12-5.952 10.4-13.376 14.88-22.304 17.824-20.8 8.928-40.096 17.856-53.472 37.152-13.376 19.328-16.32 41.6-10.4 65.376 4.48 19.296 11.904 40.096-7.424 57.92-10.4 10.4-60.928 19.328-74.272 10.4-5.952-4.448-4.48-8.896-2.976-14.848 4.448-37.12-4.48-71.296-17.824-104-4.48-11.872-2.976-17.824 8.896-20.8 20.8-5.92 40.128-16.32 54.976-31.2 32.672-31.2 25.28-71.296-17.824-89.12-34.176-14.88-69.824-11.904-104-8.928-32.672 1.504-63.872 10.4-93.6 19.328z" fill="#86DA2F" p-id="2841"></path><path d="M546.144 512.96c4.48-4.448 7.456-2.944 11.904 0 98.048 59.456 148.544 176.8 104 291.2-11.904 29.728-50.528 59.456-78.72 52-11.904-2.944-17.856-8.896-23.808-16.32-13.376-17.856-28.224-34.176-50.496-41.6-23.776-7.424-44.576-2.976-65.376 8.896-17.824 10.4-35.648 23.776-57.92 10.4-13.376-7.424-35.68-53.472-31.232-68.32 2.976-5.952 8.928-5.952 14.88-5.952 37.12-5.952 66.848-23.776 95.072-47.552 8.928-7.424 16.32-7.424 23.776 2.976 11.872 17.824 26.752 32.672 46.048 43.072 38.624 22.304 75.776 2.976 80.224-41.6 4.48-37.12-8.928-69.792-20.8-102.496a589.632 589.632 0 0 0-47.552-84.672z" fill="#24C2FF" p-id="2842"></path><path d="M498.624 521.92c-5.952 29.696-19.328 57.92-37.12 83.2-53.504 80.192-130.752 112.896-225.856 104-34.144-3.008-62.4-31.232-65.344-59.456-1.504-11.872 1.472-20.8 8.896-29.696 10.4-13.376 19.328-25.28 23.776-41.6 8.928-32.704-2.976-59.424-26.752-83.2-32.672-32.704-28.224-62.4 10.4-86.176 4.48-2.976 10.4-5.952 16.352-8.928 8.928-4.448 16.32-4.448 19.328 5.952 13.344 34.176 40.096 59.424 69.824 80.224 10.4 8.928 10.4 14.848 1.472 25.28a110.304 110.304 0 0 0-29.696 69.792c-2.976 32.704 16.32 53.504 49.024 53.504 20.8 0 40.096-7.424 57.92-16.32 46.08-23.808 81.728-57.952 115.904-93.632 4.448-1.472 5.92-4.448 11.872-2.976z" fill="#0069DA" p-id="2843"></path><path d="M254.976 209.92c2.976 0 10.4 1.472 17.824 2.976 54.976 10.4 89.152-8.928 106.976-60.928 11.872-34.176 37.12-44.576 69.824-26.752 1.472 0 1.472 1.504 2.976 1.504 34.176 19.296 34.176 22.272 13.376 52-17.824 23.776-26.752 50.496-31.2 78.72-2.976 16.352-8.928 19.328-23.776 13.376-23.776-8.896-49.024-8.896-74.304 0-28.224 8.928-40.096 34.176-31.168 62.4 11.872 37.12 44.544 53.504 72.768 72.8 28.256 19.328 60.928 29.728 92.128 43.072 4.48 1.504 11.872 1.504 10.4 8.928-1.472 4.48-7.424 4.48-13.376 4.48-66.88 2.944-130.72-7.456-182.72-52.032-49.024-40.096-84.704-89.12-78.752-157.44 4.48-22.304 20.8-38.656 49.024-43.104z" fill="#FF4649" p-id="2844"></path><path d="M133.152 627.392c-35.648 4.48-71.296-25.28-74.272-62.4-2.976-35.648 26.72-71.328 60.896-74.272 38.624-4.48 74.272 22.272 77.248 57.92 1.504 34.176-20.8 75.776-63.872 78.72z" fill="#0069DA" p-id="2845"></path><path d="M757.12 98.496c37.12-2.976 72.8 26.752 75.776 63.904 2.976 35.648-26.752 69.824-62.4 72.768-38.624 2.976-72.8-25.248-75.776-62.4-2.976-37.12 23.776-71.296 62.4-74.24z" fill="#FFCB12" p-id="2846"></path><path d="M369.376 126.72c4.48 38.624-22.304 71.296-62.4 77.248-34.176 4.48-69.824-23.776-74.272-56.448-4.48-43.072 19.296-74.272 59.424-78.72 37.12-4.48 72.768 23.744 77.248 57.92z" fill="#FF4649" p-id="2847"></path></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -0,0 +1 @@
<svg t="1750820989771" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2335" width="16" height="16"><path d="M962 512.0140625C962 263.48046875 760.52304688 62 511.99296875 62 263.571875 62 62.17578125 263.31171875 62 511.69765625v348.23671875c0.13359375 56.39765625 45.87539063 102.05507812 102.30820312 102.05507812h347.86757813C760.625 961.89101563 962 760.48085937 962 512.0140625" fill="#294172" p-id="2336"></path><path d="M644.1171875 168.54804688c-116.39882812 0-211.09570313 94.69335938-211.09570313 211.09570312v112.04296875H321.44257813c-116.39882812 0-211.09570313 94.70039062-211.09570313 211.09921875 0 116.3953125 94.696875 211.09570313 211.09570313 211.09570313s211.09570313-94.70039062 211.09570312-211.09570313v-112.04648438h111.57890625c116.39882812 0 211.09921875-94.696875 211.09921875-211.09570312 0-116.40234375-94.70039062-211.09570313-211.09921875-211.09570313z m-210.31523438 534.23789062c0 61.95234375-50.40351563 112.35585937-112.359375 112.35585937s-112.359375-50.40351563-112.359375-112.35585937c0-61.95585938 50.40351563-112.359375 112.359375-112.359375h111.57890625v0.31289062h0.78046875v112.04648438z m210.31523438-210.7828125h-111.57890625v-0.31640625h-0.77695313v-112.04296875c0-61.95585938 50.40351563-112.359375 112.35585938-112.359375s112.359375 50.40351563 112.359375 112.359375-50.40703125 112.359375-112.359375 112.359375z" fill="#3C6EB4" p-id="2337"></path><path d="M690.77304688 174.95c-16.3828125-4.28203125-28.96171875-6.27890625-46.65585938-6.27890625-116.63789062 0-211.20117188 94.56679688-211.20117188 211.19765625v111.94453125h-88.45312499c-27.58007813 0-49.86914063 21.67382813-49.8515625 49.2046875 0 27.35859375 22.04296875 49.12382813 49.33125 49.12382813l73.23398437 0.0140625c8.69414062 0 15.74648438 7.03125 15.74648438 15.71132812v96.86601563c-0.10898438 61.49179688-49.98867188 111.30117187-111.48046876 111.30117187-20.83007813 0-25.9875-2.728125-40.20820312-2.728125-29.87226563 0-49.85859375 20.025-49.85859375 47.559375 0.00703125 22.77773438 19.52578125 42.35976563 43.4109375 48.61054688 16.3828125 4.28203125 28.96171875 6.28242187 46.65585937 6.28242187 116.63789062 0 211.20117188-94.56679688 211.20117188-211.20117187v-111.94101563h88.453125c27.58007813 0 49.86914063-21.67382813 49.8515625-49.2046875 0-27.36210938-22.04296875-49.12382813-49.33125-49.12382812l-73.23398438-0.0140625a15.73242188 15.73242188 0 0 1-15.74648437-15.71484375V379.69296875c0.10898438-61.49179688 49.98867188-111.30117187 111.48046875-111.30117187 20.83007813 0 25.9875 2.73164062 40.20820312 2.73164062 29.87226563 0 49.85859375-20.02851563 49.85859375-47.559375-0.00703125-22.78125-19.52578125-42.36328125-43.4109375-48.6140625" fill="#FFFFFF" p-id="2338"></path></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1 @@
<svg t="1750821074847" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3059" width="16" height="16"><path d="M962 511.54954912c0 248.64864873-201.80180214 449.54954912-449.54954912 449.54955S62 760.19819785 62 511.54954912 263.80180215 62 511.54954912 62c248.64864873 0 450.45045088 201.80180214 450.45045088 449.54954912z" fill="#DD4814" p-id="3060"></path><path d="M206.14414414 452.09009023a60.36036065 60.36036065 0 1 0 60.36036065 60.36036065c0-33.33333339-27.02702724-60.36036065-60.36036065-60.36036065z m428.82882891 272.97297247c-28.82882901 16.21621583-38.73873867 53.15315273-21.62162198 81.98198261 16.21621583 28.82882901 53.15315273 38.73873867 81.98198262 21.6216211 28.82882901-16.21621583 38.73873867-53.15315273 21.6216211-81.98198174-17.1171167-28.82882901-53.15315273-38.73873867-81.98198174-21.62162198zM336.7747751 511.54954912c0-56.75675713 27.92792813-110.81081075 74.77477471-143.24324238l-44.14414483-72.97297383c-53.15315273 35.13513516-91.89189229 89.18918877-108.10810811 151.35135205 36.03603604 29.72972988 41.44144131 82.88288261 12.61261319 118.91891866-3.60360352 4.5045044-8.10810791 8.10810791-12.61261319 12.6126123 16.21621583 62.1621624 54.95495537 116.21621602 108.10810811 151.35135205l44.14414483-73.87387471c-46.84684658-33.33333339-74.77477471-86.48648613-74.77477471-144.14414414z m174.77477402-174.77477402c90.99099141 0 166.66666699 69.36936943 174.7747749 160.36035996l85.58558614-0.90090088c-4.5045044-63.96396416-31.53153164-124.32432393-77.47747735-169.36936963-43.24324307 16.21621583-91.89189229-5.40540528-109.00900898-48.64864833-1.80180176-5.40540528-3.60360352-10.81081055-4.50450528-16.21621583-62.1621624-17.1171167-127.92792832-10.81081055-185.58558545 18.01801758l41.44144131 74.77477471c24.32432461-12.61261231 49.54954922-18.01801846 74.77477471-18.01801758z m0 350.4504498c-25.22522549 0-50.4504501-5.40540528-73.87387383-16.21621582l-41.44144131 74.77477471c57.65765801 28.82882901 123.42342305 35.13513516 185.58558545 18.01801758 7.20720703-45.9459457 50.4504501-77.47747734 96.39639669-69.36936856 5.40540528 0.90090088 10.81081055 2.70270263 16.21621669 4.5045044 45.9459457-45.04504482 72.97297295-105.40540547 77.47747735-169.36936963l-85.58558614-0.90090088c-8.10810791 89.18918877-83.78378349 158.5585582-174.7747749 158.5585582z m123.42342393-388.28828759c28.82882901 16.21621583 65.76576592 6.30630615 81.98198174-21.62162198 16.21621583-28.82882901 6.30630615-65.76576592-21.6216211-81.98198174-29.72972988-17.1171167-65.76576592-7.20720703-82.8828835 21.6216211-16.21621583 28.82882901-6.30630615 65.76576592 22.52252286 81.98198262z" fill="#FFFFFF" p-id="3061"></path></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB