mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02:58 +08:00
feat: support plugin repository
This commit is contained in:
@@ -14,6 +14,7 @@ import org.jdesktop.swingx.JXHyperlink
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.awt.event.ActionEvent
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.swing.*
|
||||
|
||||
|
||||
@@ -27,6 +28,7 @@ class MarketplacePanel : JPanel(BorderLayout()), Disposable {
|
||||
private val busyLabel = JXBusyLabel()
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val failedLabel = JLabel()
|
||||
private val isLoading = AtomicBoolean(false)
|
||||
|
||||
private val marketplaceManager get() = MarketplaceManager.getInstance()
|
||||
|
||||
@@ -90,39 +92,42 @@ class MarketplacePanel : JPanel(BorderLayout()), Disposable {
|
||||
reload()
|
||||
}
|
||||
|
||||
private fun reload() {
|
||||
coroutineScope.launch {
|
||||
withContext(Dispatchers.Swing) {
|
||||
cardLayout.show(cardPanel, PanelState.Loading.name)
|
||||
}
|
||||
|
||||
// 获取插件
|
||||
try {
|
||||
val loadedPlugins = PluginManager.getInstance().getLoadedPluginDescriptor()
|
||||
val plugins = marketplaceManager.getPlugins()
|
||||
.filterNot { e -> loadedPlugins.any { it.id == e.id && it.version >= e.versions.first().version } }
|
||||
|
||||
fun reload() {
|
||||
if (isLoading.compareAndSet(false, true)) {
|
||||
coroutineScope.launch {
|
||||
withContext(Dispatchers.Swing) {
|
||||
if (plugins.isNotEmpty()) {
|
||||
pluginsPanel.removeAll()
|
||||
for (plugin in plugins) {
|
||||
pluginsPanel.add(createMarketplacePluginPanel(plugin))
|
||||
pluginsPanel.add(JToolBar.Separator())
|
||||
cardLayout.show(cardPanel, PanelState.Loading.name)
|
||||
}
|
||||
|
||||
// 获取插件
|
||||
try {
|
||||
val loadedPlugins = PluginManager.getInstance().getLoadedPluginDescriptor()
|
||||
val plugins = marketplaceManager.getPlugins()
|
||||
.filterNot { e -> loadedPlugins.any { it.id == e.id && it.version >= e.versions.first().version } }
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
if (plugins.isNotEmpty()) {
|
||||
pluginsPanel.removeAll()
|
||||
for (plugin in plugins) {
|
||||
pluginsPanel.add(createMarketplacePluginPanel(plugin))
|
||||
pluginsPanel.add(JToolBar.Separator())
|
||||
}
|
||||
pluginsPanel.add(createRetry())
|
||||
cardLayout.show(cardPanel, PanelState.Plugins.name)
|
||||
} else {
|
||||
failedLabel.text = "No plugins found"
|
||||
cardLayout.show(cardPanel, PanelState.FetchFailed.name)
|
||||
}
|
||||
pluginsPanel.add(createRetry())
|
||||
cardLayout.show(cardPanel, PanelState.Plugins.name)
|
||||
} else {
|
||||
failedLabel.text = "No plugins found"
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
withContext(Dispatchers.Swing) {
|
||||
failedLabel.text = "Failed to fetch the plugins"
|
||||
cardLayout.show(cardPanel, PanelState.FetchFailed.name)
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
withContext(Dispatchers.Swing) {
|
||||
failedLabel.text = "Failed to fetch the plugins"
|
||||
cardLayout.show(cardPanel, PanelState.FetchFailed.name)
|
||||
} finally {
|
||||
isLoading.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import app.termora.plugin.PluginManager
|
||||
import app.termora.plugin.PluginOrigin
|
||||
import app.termora.plugin.PluginXmlParser
|
||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||
import app.termora.plugin.marketplace.MarketplaceManager
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.extras.components.FlatTabbedPane
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
@@ -95,6 +96,19 @@ class PluginOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable, Acc
|
||||
|
||||
private fun showContextMenu() {
|
||||
val popupMenu = FlatPopupMenu()
|
||||
|
||||
val managePluginRepositoryMenu =
|
||||
popupMenu.add(I18n.getString("termora.settings.plugin.manage-plugin-repository"))
|
||||
managePluginRepositoryMenu.addActionListener {
|
||||
val dialog = PluginRepositoryDialog(owner)
|
||||
dialog.isVisible = true
|
||||
if (dialog.changed) {
|
||||
MarketplaceManager.getInstance().clear()
|
||||
marketplacePanel.reload()
|
||||
}
|
||||
}
|
||||
popupMenu.addSeparator()
|
||||
|
||||
val installPluginFromDiskMenu = popupMenu.add(I18n.getString("termora.settings.plugin.install-from-disk"))
|
||||
installPluginFromDiskMenu.addActionListener {
|
||||
val chooser = FileChooser()
|
||||
@@ -104,6 +118,7 @@ class PluginOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable, Acc
|
||||
chooser.fileSelectionMode = JFileChooser.FILES_ONLY
|
||||
chooser.showOpenDialog(owner).thenAccept { if (it.isNotEmpty()) installPluginFromDisk(it.first()) }
|
||||
}
|
||||
|
||||
popupMenu.show(settingsButton, 0, settingsButton.height)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package app.termora.plugin.internal.plugin
|
||||
|
||||
import app.termora.*
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.Window
|
||||
import java.awt.event.ActionEvent
|
||||
import java.net.URI
|
||||
import javax.swing.*
|
||||
|
||||
internal class PluginRepositoryDialog(owner: Window) : DialogWrapper(owner) {
|
||||
|
||||
private val model = DefaultListModel<String>()
|
||||
private val list = JList(model)
|
||||
private val dialog get() = this
|
||||
|
||||
var changed = false
|
||||
private set
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 200, UIManager.getInt("Dialog.height") - 150)
|
||||
isModal = true
|
||||
isResizable = false
|
||||
controlsVisible = false
|
||||
title = "Custom Plugin Repository"
|
||||
list.fixedCellHeight = UIManager.getInt("Tree.rowHeight")
|
||||
for (url in PluginRepositoryManager.getInstance().getRepositories()) {
|
||||
model.addElement(url)
|
||||
}
|
||||
setLocationRelativeTo(owner)
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val pluginRepositoryManager = PluginRepositoryManager.getInstance()
|
||||
val panel = JPanel(BorderLayout())
|
||||
val toolbar = FlatToolBar().apply { isFloatable = false }
|
||||
val addBtn = JButton(Icons.add)
|
||||
val deleteBtn = JButton(Icons.delete)
|
||||
val urlBtn = JButton(Icons.externalLink)
|
||||
deleteBtn.isEnabled = false
|
||||
|
||||
toolbar.add(addBtn)
|
||||
toolbar.add(deleteBtn)
|
||||
toolbar.add(Box.createHorizontalGlue())
|
||||
toolbar.add(urlBtn)
|
||||
toolbar.border = BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor)
|
||||
|
||||
panel.add(toolbar, BorderLayout.NORTH)
|
||||
panel.add(JScrollPane(list).apply { border = BorderFactory.createEmptyBorder() }, BorderLayout.CENTER)
|
||||
panel.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||
|
||||
addBtn.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
val text = OptionPane.showInputDialog(dialog)
|
||||
if (text.isNullOrBlank()) return
|
||||
if ((text.startsWith("http://") || text.startsWith("https://")).not()) {
|
||||
return
|
||||
}
|
||||
pluginRepositoryManager.addRepository(text)
|
||||
model.addElement(text)
|
||||
changed = true
|
||||
}
|
||||
})
|
||||
|
||||
urlBtn.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
Application.browse(URI.create("https://github.com/TermoraDev/termora-marketplace/releases/latest"))
|
||||
}
|
||||
})
|
||||
|
||||
deleteBtn.addActionListener(object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
if (OptionPane.showConfirmDialog(
|
||||
dialog,
|
||||
I18n.getString("termora.keymgr.delete-warning"),
|
||||
I18n.getString("termora.remove"),
|
||||
JOptionPane.YES_NO_OPTION,
|
||||
JOptionPane.QUESTION_MESSAGE
|
||||
) == JOptionPane.YES_OPTION
|
||||
) {
|
||||
for (i in list.selectedIndices.sortedByDescending { it }) {
|
||||
pluginRepositoryManager.removeRepository(model.getElementAt(i))
|
||||
model.removeElementAt(i)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
list.addListSelectionListener { deleteBtn.isEnabled = list.selectedIndex >= 0 }
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
override fun createSouthPanel(): JComponent? {
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package app.termora.plugin.internal.plugin
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.EnableManager
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
internal class PluginRepositoryManager private constructor() {
|
||||
companion object {
|
||||
private const val KEY = "PluginRepositories"
|
||||
fun getInstance(): PluginRepositoryManager {
|
||||
return ApplicationScope.forApplicationScope()
|
||||
.getOrCreate(PluginRepositoryManager::class) { PluginRepositoryManager() }
|
||||
}
|
||||
}
|
||||
|
||||
private val enableManager get() = EnableManager.getInstance()
|
||||
|
||||
fun addRepository(url: String) {
|
||||
synchronized(this) {
|
||||
val repositories = getRepositories().toMutableList()
|
||||
repositories.add(url)
|
||||
enableManager.setFlag(KEY, ohMyJson.encodeToString(repositories))
|
||||
}
|
||||
}
|
||||
|
||||
fun removeRepository(url: String) {
|
||||
synchronized(this) {
|
||||
val repositories = getRepositories().toMutableList()
|
||||
repositories.removeIf { it == url }
|
||||
enableManager.setFlag(KEY, ohMyJson.encodeToString(repositories))
|
||||
}
|
||||
}
|
||||
|
||||
fun getRepositories(): List<String> {
|
||||
synchronized(this) {
|
||||
val text = enableManager.getFlag(KEY, StringUtils.EMPTY)
|
||||
if (text.isBlank()) return emptyList()
|
||||
return runCatching { ohMyJson.decodeFromString<List<String>>(text) }
|
||||
.getOrNull() ?: emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package app.termora.plugin.marketplace
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.plugin.PluginDescription
|
||||
import app.termora.plugin.internal.plugin.PluginRepositoryManager
|
||||
import app.termora.plugin.internal.plugin.PluginSVGIcon
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -18,6 +19,8 @@ import org.slf4j.LoggerFactory
|
||||
import org.xml.sax.InputSource
|
||||
import java.io.StringReader
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
@@ -88,6 +91,55 @@ internal class MarketplaceManager private constructor() {
|
||||
private fun doGetPlugins(): List<MarketplacePlugin> {
|
||||
val version = Semver.parse(Application.getVersion())
|
||||
?: return emptyList()
|
||||
|
||||
val repositories = PluginRepositoryManager.getInstance().getRepositories().toMutableSet()
|
||||
repositories.add("https://github.com/TermoraDev/termora-marketplace/releases/latest/download/plugins.xml")
|
||||
|
||||
val plugins = mutableListOf<MarketplacePlugin>()
|
||||
val executorService = Executors.newVirtualThreadPerTaskExecutor()
|
||||
val futures = repositories
|
||||
.map { url -> executorService.submit<List<MarketplacePlugin>> { getPlugins(url, version) } }
|
||||
for (future in futures) {
|
||||
try {
|
||||
plugins.addAll(future.get(1, TimeUnit.MINUTES))
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn(e.message, e)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (plugins.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val matchedPlugins = mutableListOf<MarketplacePlugin>()
|
||||
|
||||
// 获取到合适的插件
|
||||
for (e in plugins) {
|
||||
for (plugin in e.versions) {
|
||||
if (version.satisfies(plugin.since).not()) continue
|
||||
if (plugin.until.isNotBlank())
|
||||
if (version.satisfies(plugin.until).not()) continue
|
||||
matchedPlugins.add(e.copy(versions = mutableListOf(plugin)))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 因为可能有多个插件仓库,所以要去重
|
||||
val map = matchedPlugins.groupBy { it.id }
|
||||
matchedPlugins.clear()
|
||||
for (entry in map) {
|
||||
matchedPlugins.add(entry.value.maxBy { it.versions.first().version })
|
||||
}
|
||||
|
||||
return matchedPlugins.sortedBy { it.name.length }
|
||||
}
|
||||
|
||||
|
||||
private fun getPlugins(url: String, version: Semver): List<MarketplacePlugin> {
|
||||
|
||||
val language = I18n.containsLanguage(Locale.getDefault()) ?: "en_US"
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
@@ -95,7 +147,7 @@ internal class MarketplaceManager private constructor() {
|
||||
.header("X-Language", language)
|
||||
.header("X-OS", SystemUtils.OS_NAME)
|
||||
.header("X-Arch", SystemUtils.OS_ARCH)
|
||||
.url("https://github.com/TermoraDev/termora-marketplace/releases/latest/download/plugins.xml")
|
||||
.url(url)
|
||||
.build()
|
||||
val response = Application.httpClient.newCall(request).execute()
|
||||
if (response.isSuccessful.not()) {
|
||||
@@ -180,24 +232,7 @@ internal class MarketplaceManager private constructor() {
|
||||
)
|
||||
}
|
||||
|
||||
if (plugins.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val matchedPlugins = mutableListOf<MarketplacePlugin>()
|
||||
|
||||
// 获取到合适的插件
|
||||
for (e in plugins) {
|
||||
for (plugin in e.versions) {
|
||||
if (version.satisfies(plugin.since).not()) continue
|
||||
if (plugin.until.isNotBlank())
|
||||
if (version.satisfies(plugin.until).not()) continue
|
||||
matchedPlugins.add(e.copy(versions = mutableListOf(plugin)))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return matchedPlugins.sortedBy { it.name.length }
|
||||
return plugins
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -110,7 +110,8 @@ termora.settings.plugin.cannot-uninstall=System built-in plugins cannot be unins
|
||||
termora.settings.plugin.uninstall-confirm=Are you sure you want to uninstall <b>{0}</b> ?
|
||||
termora.settings.plugin.uninstall-failed=Uninstall failed
|
||||
termora.settings.plugin.install-failed=Install failed, please try again later
|
||||
termora.settings.plugin.install-from-disk=Install plugin from Disk...
|
||||
termora.settings.plugin.install-from-disk=Install Plugin from Disk...
|
||||
termora.settings.plugin.manage-plugin-repository=Manage Plugin Repository...
|
||||
termora.settings.plugin.install-from-disk-warning=<b>{0}</b> plugin will have access to all your data. Are you sure you want to install it?
|
||||
|
||||
termora.settings.account=Account
|
||||
|
||||
@@ -117,6 +117,7 @@ termora.settings.plugin.cannot-uninstall=系统内置插件不可以卸载
|
||||
termora.settings.plugin.uninstall-confirm=你确定要卸载 <b>{0}</b> 吗?
|
||||
termora.settings.plugin.uninstall-failed=卸载失败
|
||||
termora.settings.plugin.install-from-disk=从磁盘安装插件...
|
||||
termora.settings.plugin.manage-plugin-repository=管理插件仓库...
|
||||
termora.settings.plugin.install-failed=安装失败,请稍后再试
|
||||
termora.settings.plugin.install-from-disk-warning=<b>{0}</b> 插件可以访问你的所有数据,你确定要安装吗?
|
||||
|
||||
|
||||
@@ -128,8 +128,9 @@ termora.settings.plugin.cannot-uninstall=系統內建插件無法卸載
|
||||
termora.settings.plugin.uninstall-confirm=你確定要卸載 <b>{0}</b> 嗎?
|
||||
termora.settings.plugin.uninstall-failed=解除安裝失敗
|
||||
termora.settings.plugin.install-from-disk=從磁碟安裝插件...
|
||||
termora.settings.plugin.manage-plugin-repository=管理插件倉庫...
|
||||
termora.settings.plugin.install-failed=安裝失敗,請稍後再試
|
||||
termora.settings.plugin.install-from-disk-warning=<b>{0}</b> 外掛程式可以存取你的所有數據,你確定要安裝嗎?
|
||||
termora.settings.plugin.install-from-disk-warning=<b>{0}</b> 插件可以存取你的所有數據,你確定要安裝嗎?
|
||||
|
||||
termora.settings.account=帳號
|
||||
termora.settings.account.login=登入
|
||||
|
||||
Reference in New Issue
Block a user