mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
chore: improve team sync
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugins.cos
|
package app.termora.plugins.cos
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -13,7 +14,7 @@ class COSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
|
|||||||
return COSProtocolProvider.instance
|
return COSProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return COSProtocolHostPanel()
|
return COSProtocolHostPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugins.obs
|
package app.termora.plugins.obs
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -13,7 +14,7 @@ class OBSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
|
|||||||
return OBSProtocolProvider.instance
|
return OBSProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return OBSProtocolHostPanel()
|
return OBSProtocolHostPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugins.oss
|
package app.termora.plugins.oss
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -13,7 +14,7 @@ class OSSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
|
|||||||
return OSSProtocolProvider.instance
|
return OSSProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return OSSProtocolHostPanel()
|
return OSSProtocolHostPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugins.s3
|
package app.termora.plugins.s3
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -13,7 +14,7 @@ class S3ProtocolHostPanelExtension private constructor() : ProtocolHostPanelExte
|
|||||||
return S3ProtocolProvider.instance
|
return S3ProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return S3ProtocolHostPanel()
|
return S3ProtocolHostPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugins.smb
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -13,7 +14,7 @@ class SMBProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
|
|||||||
return SMBProtocolProvider.instance
|
return SMBProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return SMBProtocolHostPanel()
|
return SMBProtocolHostPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugins.webdav
|
package app.termora.plugins.webdav
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -13,7 +14,7 @@ class WebDAVProtocolHostPanelExtension private constructor() : ProtocolHostPanel
|
|||||||
return WebDAVProtocolProvider.instance
|
return WebDAVProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return WebDAVProtocolHostPanel()
|
return WebDAVProtocolHostPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.protocol.*
|
import app.termora.protocol.*
|
||||||
@@ -18,7 +19,11 @@ import java.awt.Dimension
|
|||||||
import java.awt.Window
|
import java.awt.Window
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
class NewHostDialogV2(owner: Window, private val editHost: Host? = null) : DialogWrapper(owner) {
|
class NewHostDialogV2(
|
||||||
|
owner: Window,
|
||||||
|
private val editHost: Host? = null,
|
||||||
|
private val accountOwner: AccountOwner,
|
||||||
|
) : DialogWrapper(owner) {
|
||||||
|
|
||||||
private object Current {
|
private object Current {
|
||||||
var card: ProtocolHostPanel? = null
|
var card: ProtocolHostPanel? = null
|
||||||
@@ -65,11 +70,11 @@ 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() }
|
.filter { it.canCreateProtocolHostPanel(accountOwner) }
|
||||||
for ((index, extension) in extensions.withIndex()) {
|
for ((index, extension) in extensions.withIndex()) {
|
||||||
val protocol = extension.getProtocolProvider().getProtocol()
|
val protocol = extension.getProtocolProvider().getProtocol()
|
||||||
val icon = ScaleIcon(extension.getProtocolProvider().getIcon(), 22)
|
val icon = ScaleIcon(extension.getProtocolProvider().getIcon(), 22)
|
||||||
val hostPanel = extension.createProtocolHostPanel()
|
val hostPanel = extension.createProtocolHostPanel(accountOwner)
|
||||||
val button = JToggleButton(protocol, icon).apply { buttonGroup.add(this) }
|
val button = JToggleButton(protocol, icon).apply { buttonGroup.add(this) }
|
||||||
button.setVerticalTextPosition(SwingConstants.BOTTOM)
|
button.setVerticalTextPosition(SwingConstants.BOTTOM)
|
||||||
button.setHorizontalTextPosition(SwingConstants.CENTER)
|
button.setHorizontalTextPosition(SwingConstants.CENTER)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ open class Scope(
|
|||||||
return get(clazz)
|
return get(clazz)
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized(clazz) {
|
synchronized(this) {
|
||||||
if (beans.containsKey(clazz)) {
|
if (beans.containsKey(clazz)) {
|
||||||
return get(clazz)
|
return get(clazz)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
|
||||||
|
import app.termora.account.AccountManager
|
||||||
import app.termora.actions.*
|
import app.termora.actions.*
|
||||||
import app.termora.database.DatabaseChangedExtension
|
import app.termora.database.DatabaseChangedExtension
|
||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
@@ -244,10 +245,15 @@ class TerminalTabbed(
|
|||||||
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 hostManager get() = HostManager.getInstance()
|
||||||
|
private val accountManager get() = AccountManager.getInstance()
|
||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
if (tab is HostTerminalTab) {
|
if (tab is HostTerminalTab) {
|
||||||
val host = hostManager.getHost(tab.host.id) ?: return
|
val host = hostManager.getHost(tab.host.id) ?: return
|
||||||
val dialog = NewHostDialogV2(evt.window, host)
|
val dialog = NewHostDialogV2(
|
||||||
|
evt.window, host,
|
||||||
|
accountManager.getOwners().first { it.id == host.ownerId },
|
||||||
|
)
|
||||||
dialog.setLocationRelativeTo(evt.window)
|
dialog.setLocationRelativeTo(evt.window)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
|
|
||||||
|
|||||||
@@ -183,8 +183,8 @@ object AccountHttp {
|
|||||||
|
|
||||||
if (isRefreshing.compareAndSet(false, true)) {
|
if (isRefreshing.compareAndSet(false, true)) {
|
||||||
try {
|
try {
|
||||||
// 刷新 token
|
// 刷新 token 和用户
|
||||||
accountManager.refreshToken()
|
accountManager.refresh()
|
||||||
} finally {
|
} finally {
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
isRefreshing.set(false)
|
isRefreshing.set(false)
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||||||
import org.apache.commons.codec.binary.Base64
|
import org.apache.commons.codec.binary.Base64
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import javax.swing.SwingUtilities
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
class AccountManager private constructor() : ApplicationRunnerExtension {
|
class AccountManager private constructor() : ApplicationRunnerExtension {
|
||||||
companion object {
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(AccountManager::class.java)
|
||||||
|
|
||||||
fun getInstance(): AccountManager {
|
fun getInstance(): AccountManager {
|
||||||
return ApplicationScope.forApplicationScope()
|
return ApplicationScope.forApplicationScope()
|
||||||
.getOrCreate(AccountManager::class) { AccountManager() }
|
.getOrCreate(AccountManager::class) { AccountManager() }
|
||||||
@@ -30,6 +33,7 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val serverManager get() = ServerManager.getInstance()
|
||||||
private var account = locally()
|
private var account = locally()
|
||||||
private val accountProperties get() = AccountProperties.getInstance()
|
private val accountProperties get() = AccountProperties.getInstance()
|
||||||
|
|
||||||
@@ -48,10 +52,14 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
|
|||||||
fun getAccessToken() = account.accessToken
|
fun getAccessToken() = account.accessToken
|
||||||
fun getRefreshToken() = account.refreshToken
|
fun getRefreshToken() = account.refreshToken
|
||||||
fun getOwnerIds() = account.teams.map { it.id }.toMutableList().apply { add(getAccountId()) }.toSet()
|
fun getOwnerIds() = account.teams.map { it.id }.toMutableList().apply { add(getAccountId()) }.toSet()
|
||||||
fun getOwners() =
|
fun getOwners(): Set<AccountOwner> {
|
||||||
account.teams.map { AccountOwner(it.id, it.name, OwnerType.Team) }
|
val owners = mutableSetOf<AccountOwner>()
|
||||||
.toMutableList().apply { AccountOwner(getAccountId(), getEmail(), OwnerType.User) }
|
owners.add(AccountOwner(getAccountId(), getEmail(), OwnerType.User))
|
||||||
.toSet()
|
for (team in getTeams()) {
|
||||||
|
owners.add(AccountOwner(team.id, team.name, OwnerType.Team))
|
||||||
|
}
|
||||||
|
return owners
|
||||||
|
}
|
||||||
|
|
||||||
fun isFreePlan(): Boolean {
|
fun isFreePlan(): Boolean {
|
||||||
return isLocally() || getSubscription().plan == SubscriptionPlan.Free
|
return isLocally() || getSubscription().plan == SubscriptionPlan.Free
|
||||||
@@ -126,37 +134,39 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
|
|||||||
* 设置账户信息,可以多次调用,每次修改用户信息都要通过这个方法
|
* 设置账户信息,可以多次调用,每次修改用户信息都要通过这个方法
|
||||||
*/
|
*/
|
||||||
internal fun login(account: Account) {
|
internal fun login(account: Account) {
|
||||||
|
synchronized(this) {
|
||||||
|
|
||||||
val oldAccount = this.account
|
val oldAccount = this.account
|
||||||
|
|
||||||
this.account = account
|
this.account = account
|
||||||
|
|
||||||
// 立即保存到数据库
|
// 立即保存到数据库
|
||||||
val accountProperties = AccountProperties.getInstance()
|
val accountProperties = AccountProperties.getInstance()
|
||||||
accountProperties.id = account.id
|
accountProperties.id = account.id
|
||||||
accountProperties.server = account.server
|
accountProperties.server = account.server
|
||||||
accountProperties.email = account.email
|
accountProperties.email = account.email
|
||||||
accountProperties.teams = ohMyJson.encodeToString(account.teams)
|
accountProperties.teams = ohMyJson.encodeToString(account.teams)
|
||||||
accountProperties.subscriptions = ohMyJson.encodeToString(account.subscriptions)
|
accountProperties.subscriptions = ohMyJson.encodeToString(account.subscriptions)
|
||||||
accountProperties.accessToken = account.accessToken
|
accountProperties.accessToken = account.accessToken
|
||||||
accountProperties.refreshToken = account.refreshToken
|
accountProperties.refreshToken = account.refreshToken
|
||||||
accountProperties.secretKey = ohMyJson.encodeToString(account.secretKey)
|
accountProperties.secretKey = ohMyJson.encodeToString(account.secretKey)
|
||||||
|
|
||||||
// 如果变更账户了,那么同步时间从0开始
|
// 如果变更账户了,那么同步时间从0开始
|
||||||
if (oldAccount.id != account.id) {
|
if (oldAccount.id != account.id) {
|
||||||
accountProperties.nextSynchronizationSince = 0
|
accountProperties.nextSynchronizationSince = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLocally().not()) {
|
||||||
|
accountProperties.publicKey = Base64.encodeBase64String(account.publicKey.encoded)
|
||||||
|
accountProperties.privateKey = Base64.encodeBase64String(account.privateKey.encoded)
|
||||||
|
} else {
|
||||||
|
accountProperties.publicKey = StringUtils.EMPTY
|
||||||
|
accountProperties.privateKey = StringUtils.EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知变化
|
||||||
|
notifyAccountChanged(oldAccount, account)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLocally().not()) {
|
|
||||||
accountProperties.publicKey = Base64.encodeBase64String(account.publicKey.encoded)
|
|
||||||
accountProperties.privateKey = Base64.encodeBase64String(account.privateKey.encoded)
|
|
||||||
} else {
|
|
||||||
accountProperties.publicKey = StringUtils.EMPTY
|
|
||||||
accountProperties.privateKey = StringUtils.EMPTY
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通知变化
|
|
||||||
notifyAccountChanged(oldAccount, account)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyAccountChanged(oldAccount: Account, newAccount: Account) {
|
private fun notifyAccountChanged(oldAccount: Account, newAccount: Account) {
|
||||||
@@ -220,7 +230,7 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
|
|||||||
|
|
||||||
override fun ready() {
|
override fun ready() {
|
||||||
if (isLocally().not()) {
|
if (isLocally().not()) {
|
||||||
swingCoroutineScope.launch(Dispatchers.IO) { refreshToken() }
|
swingCoroutineScope.launch(Dispatchers.IO) { refresh() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,8 +238,34 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
|
|||||||
/**
|
/**
|
||||||
* 刷新用户
|
* 刷新用户
|
||||||
*/
|
*/
|
||||||
fun refresh(accessToken: String = getAccessToken()) {
|
fun refresh() {
|
||||||
|
runCatching { refreshToken() }.onSuccess {
|
||||||
|
refreshAccount()
|
||||||
|
}.onFailure {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(it.message, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshAccount() {
|
||||||
|
try {
|
||||||
|
val me = serverManager.callMe(account.server, getAccessToken())
|
||||||
|
val teams = me.teams.map {
|
||||||
|
Team(
|
||||||
|
id = it.id,
|
||||||
|
name = it.name,
|
||||||
|
secretKey = RSA.decrypt(getPrivateKey(), Base64.decodeBase64(it.secretKey)),
|
||||||
|
role = it.role
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 重新登录
|
||||||
|
login(account.copy(teams = teams, subscriptions = me.subscriptions))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (log.isErrorEnabled) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
|
class AccountApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {
|
||||||
|
|||||||
@@ -75,8 +75,10 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
|
|||||||
val subscription = accountManager.getSubscription()
|
val subscription = accountManager.getSubscription()
|
||||||
val isFreePlan = accountManager.isFreePlan()
|
val isFreePlan = accountManager.isFreePlan()
|
||||||
val isLocally = accountManager.isLocally()
|
val isLocally = accountManager.isLocally()
|
||||||
val validTo = if (isFreePlan) "-" else if (subscription.endAt >= Long.MAX_VALUE)
|
val validTo = if (isFreePlan) "-"
|
||||||
I18n.getString("termora.settings.account.lifetime") else
|
else if (subscription.endAt >= Long.MAX_VALUE)
|
||||||
|
I18n.getString("termora.settings.account.lifetime")
|
||||||
|
else
|
||||||
DateFormatUtils.format(Date(subscription.endAt), I18n.getString("termora.date-format"))
|
DateFormatUtils.format(Date(subscription.endAt), I18n.getString("termora.date-format"))
|
||||||
val lastSynchronizationOn = if (isFreePlan) "-" else
|
val lastSynchronizationOn = if (isFreePlan) "-" else
|
||||||
DateFormatUtils.format(
|
DateFormatUtils.format(
|
||||||
@@ -158,9 +160,19 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
|
|||||||
if (isFreePlan.not()) {
|
if (isFreePlan.not()) {
|
||||||
actions.add(JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.sync-now")) {
|
actions.add(JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.sync-now")) {
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
|
// 全量同步
|
||||||
|
accountProperties.nextSynchronizationSince = 0
|
||||||
|
|
||||||
|
// 拉取
|
||||||
PullService.getInstance().trigger()
|
PullService.getInstance().trigger()
|
||||||
|
|
||||||
|
// 推送
|
||||||
PushService.getInstance().trigger()
|
PushService.getInstance().trigger()
|
||||||
|
|
||||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
|
// 刷新账户
|
||||||
|
accountManager.refreshAccount()
|
||||||
|
|
||||||
withContext(Dispatchers.Swing) {
|
withContext(Dispatchers.Swing) {
|
||||||
isEnabled = false
|
isEnabled = false
|
||||||
lastSynchronizationOnLabel.text = DateFormatUtils.format(
|
lastSynchronizationOnLabel.text = DateFormatUtils.format(
|
||||||
|
|||||||
@@ -2,5 +2,4 @@ package app.termora.account
|
|||||||
|
|
||||||
import app.termora.database.OwnerType
|
import app.termora.database.OwnerType
|
||||||
|
|
||||||
data class AccountOwner(val id: String, val name: String, val type: OwnerType) {
|
data class AccountOwner(val id: String, val name: String, val type: OwnerType)
|
||||||
}
|
|
||||||
@@ -48,6 +48,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app")
|
Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app")
|
||||||
private val chinaServer =
|
private val chinaServer =
|
||||||
Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn")
|
Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn")
|
||||||
|
private val serverManager get() = ServerManager.getInstance()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
isModal = true
|
isModal = true
|
||||||
@@ -359,7 +360,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
|||||||
|
|
||||||
val loginJob = swingCoroutineScope.launch(Dispatchers.IO) {
|
val loginJob = swingCoroutineScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
ServerManager.getInstance().login(
|
serverManager.login(
|
||||||
server, usernameTextField.text,
|
server, usernameTextField.text,
|
||||||
String(passwordField.password), mfaTextField.text.trim()
|
String(passwordField.password), mfaTextField.text.trim()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,6 +45,15 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
|||||||
lastChangeHash = StringUtils.EMPTY
|
lastChangeHash = StringUtils.EMPTY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 团队变了,全量同步
|
||||||
|
if (oldAccount.id == newAccount.id) {
|
||||||
|
if (oldAccount.teams != newAccount.teams) {
|
||||||
|
accountProperties.nextSynchronizationSince = 0
|
||||||
|
trigger()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (oldAccount.isLocally && newAccount.isLocally.not()) {
|
if (oldAccount.isLocally && newAccount.isLocally.not()) {
|
||||||
trigger()
|
trigger()
|
||||||
}
|
}
|
||||||
@@ -213,7 +222,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
|||||||
log.debug("拉取数据: {} 成功, 响应码: {}", id, response.code)
|
log.debug("拉取数据: {} 成功, 响应码: {}", id, response.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(response.isSuccessful.not()){
|
if (response.isSuccessful.not()) {
|
||||||
IOUtils.closeQuietly(response)
|
IOUtils.closeQuietly(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +290,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
|||||||
ownerId = ownerId,
|
ownerId = ownerId,
|
||||||
ownerType = ownerType,
|
ownerType = ownerType,
|
||||||
type = type,
|
type = type,
|
||||||
data = decryptData(id, data),
|
data = decryptData(id, data, ownerId),
|
||||||
version = version,
|
version = version,
|
||||||
// 因为已经是拉取最新版本了,所以这里无需再同步了
|
// 因为已经是拉取最新版本了,所以这里无需再同步了
|
||||||
synced = true,
|
synced = true,
|
||||||
@@ -298,11 +307,8 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
|||||||
if (log.isDebugEnabled) {
|
if (log.isDebugEnabled) {
|
||||||
log.debug("数据: {}, 类型: {} 云端已经删除,本地即将删除", id, type)
|
log.debug("数据: {}, 类型: {} 云端已经删除,本地即将删除", id, type)
|
||||||
}
|
}
|
||||||
databaseManager.delete(
|
|
||||||
id, type,
|
|
||||||
DatabaseChangedExtension.Source.Sync
|
|
||||||
)
|
|
||||||
|
|
||||||
|
databaseManager.delete(id, type, DatabaseChangedExtension.Source.Sync)
|
||||||
if (log.isInfoEnabled) {
|
if (log.isInfoEnabled) {
|
||||||
log.info("数据: {}, 类型: {} 已从本地删除", id, type)
|
log.info("数据: {}, 类型: {} 已从本地删除", id, type)
|
||||||
|
|
||||||
@@ -340,7 +346,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
|||||||
ownerId = ownerId,
|
ownerId = ownerId,
|
||||||
ownerType = ownerType,
|
ownerType = ownerType,
|
||||||
type = type,
|
type = type,
|
||||||
data = decryptData(id, data),
|
data = decryptData(id, data, ownerId),
|
||||||
version = version,
|
version = version,
|
||||||
// 因为已经是拉取最新版本了,所以这里无需再同步了
|
// 因为已经是拉取最新版本了,所以这里无需再同步了
|
||||||
synced = true,
|
synced = true,
|
||||||
@@ -377,7 +383,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
|||||||
pullChanges()
|
pullChanges()
|
||||||
|
|
||||||
// N 秒拉一次
|
// N 秒拉一次
|
||||||
val result = withTimeoutOrNull(Random.nextInt(5, 15).seconds) {
|
val result = withTimeoutOrNull(Random.nextInt(3, 10).seconds) {
|
||||||
channel.receiveCatching()
|
channel.receiveCatching()
|
||||||
} ?: continue
|
} ?: continue
|
||||||
if (result.isFailure) break
|
if (result.isFailure) break
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import app.termora.*
|
|||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
import app.termora.database.Data
|
import app.termora.database.Data
|
||||||
import app.termora.database.DatabaseChangedExtension
|
import app.termora.database.DatabaseChangedExtension
|
||||||
|
import app.termora.database.OwnerType
|
||||||
import app.termora.plugin.DispatchThread
|
import app.termora.plugin.DispatchThread
|
||||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -106,7 +107,20 @@ class PushService private constructor() : SyncService(), Disposable, Application
|
|||||||
.delete()
|
.delete()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
AccountHttp.execute(request = request)
|
try {
|
||||||
|
AccountHttp.execute(request = request)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is ResponseException) {
|
||||||
|
if (e.code == 403) {
|
||||||
|
// 如果是 Team 发现没有权限,那么很有可能是被提出团队
|
||||||
|
if (data.ownerType == OwnerType.Team.name) {
|
||||||
|
// 刷新用户
|
||||||
|
accountManager.refreshAccount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
// 修改为已经同步
|
// 修改为已经同步
|
||||||
updateData(data.id, synced = true)
|
updateData(data.id, synced = true)
|
||||||
@@ -153,6 +167,12 @@ class PushService private constructor() : SyncService(), Disposable, Application
|
|||||||
}
|
}
|
||||||
// 标记为已经同步
|
// 标记为已经同步
|
||||||
updateData(data.id, synced = true, version = data.version)
|
updateData(data.id, synced = true, version = data.version)
|
||||||
|
|
||||||
|
// 如果是 Team 发现没有权限,那么很有可能是被提出团队
|
||||||
|
if (data.ownerType == OwnerType.Team.name) {
|
||||||
|
// 刷新用户
|
||||||
|
accountManager.refreshAccount()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
} else if (response.code == 409) { // 版本冲突,一般来说是云端版本大于本地版本
|
} else if (response.code == 409) { // 版本冲突,一般来说是云端版本大于本地版本
|
||||||
val json = ohMyJson.decodeFromString<JsonObject>(text)
|
val json = ohMyJson.decodeFromString<JsonObject>(text)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class ServerManager private constructor() {
|
|||||||
val loginResponse = callLogin(serverInfo, server, username, password, mfa)
|
val loginResponse = callLogin(serverInfo, server, username, password, mfa)
|
||||||
|
|
||||||
// call me
|
// call me
|
||||||
val meResponse = callMe(server, loginResponse.accessToken)
|
val meResponse = callMe(server.server, loginResponse.accessToken)
|
||||||
|
|
||||||
// 解密
|
// 解密
|
||||||
val salt = "${serverInfo.salt}:${username}".toByteArray()
|
val salt = "${serverInfo.salt}:${username}".toByteArray()
|
||||||
@@ -139,9 +139,9 @@ class ServerManager private constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun callMe(server: Server, accessToken: String): MeResponse {
|
fun callMe(server: String, accessToken: String): MeResponse {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url("${server.server}/v1/users/me")
|
.url("${server}/v1/users/me")
|
||||||
.header("Authorization", "Bearer $accessToken")
|
.header("Authorization", "Bearer $accessToken")
|
||||||
.build()
|
.build()
|
||||||
val text = AccountHttp.execute(request = request)
|
val text = AccountHttp.execute(request = request)
|
||||||
@@ -149,13 +149,13 @@ class ServerManager private constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class ServerInfo(val salt: String)
|
data class ServerInfo(val salt: String)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class LoginResponse(val accessToken: String, val refreshToken: String)
|
data class LoginResponse(val accessToken: String, val refreshToken: String)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class MeResponse(
|
data class MeResponse(
|
||||||
val id: String,
|
val id: String,
|
||||||
val email: String,
|
val email: String,
|
||||||
val publicKey: String,
|
val publicKey: String,
|
||||||
@@ -167,5 +167,5 @@ class ServerManager private constructor() {
|
|||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class MeTeam(val id: String, val name: String, val role: TeamRole, val secretKey: String)
|
data class MeTeam(val id: String, val name: String, val role: TeamRole, val secretKey: String)
|
||||||
}
|
}
|
||||||
@@ -78,28 +78,23 @@ abstract class SyncService {
|
|||||||
|
|
||||||
protected fun encryptData(id: String, data: String, ownerId: String): String {
|
protected fun encryptData(id: String, data: String, ownerId: String): String {
|
||||||
val iv = DigestUtils.sha256(id).copyOf(12)
|
val iv = DigestUtils.sha256(id).copyOf(12)
|
||||||
var secretKey = EMPTY_BYTE_ARRAY
|
val secretKey = getSecretKey(ownerId)
|
||||||
if (ownerId != accountManager.getAccountId()) {
|
|
||||||
val team = accountManager.getTeams().firstOrNull { it.id == ownerId }
|
|
||||||
if (team == null) {
|
|
||||||
return StringUtils.EMPTY
|
|
||||||
} else {
|
|
||||||
secretKey = team.secretKey
|
|
||||||
}
|
|
||||||
} else if (ownerId == accountManager.getAccountId()) {
|
|
||||||
secretKey = accountManager.getSecretKey()
|
|
||||||
}
|
|
||||||
if (secretKey.isEmpty()) return StringUtils.EMPTY
|
if (secretKey.isEmpty()) return StringUtils.EMPTY
|
||||||
return Base64.encodeBase64String(AES.GCM.encrypt(secretKey, iv, data.toByteArray()))
|
return Base64.encodeBase64String(AES.GCM.encrypt(secretKey, iv, data.toByteArray()))
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun decryptData(id: String, data: String): String {
|
protected fun getSecretKey(ownerId: String): ByteArray {
|
||||||
|
if (ownerId == accountManager.getAccountId()) {
|
||||||
|
return accountManager.getSecretKey()
|
||||||
|
}
|
||||||
|
val team = accountManager.getTeams().firstOrNull { it.id == ownerId }
|
||||||
|
return team?.secretKey ?: EMPTY_BYTE_ARRAY
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun decryptData(id: String, data: String, ownerId: String): String {
|
||||||
val iv = DigestUtils.sha256(id).copyOf(12)
|
val iv = DigestUtils.sha256(id).copyOf(12)
|
||||||
return String(
|
val secretKey = getSecretKey(ownerId)
|
||||||
AES.GCM.decrypt(
|
if (secretKey.isEmpty()) throw IllegalStateException("根据 ownerId 无法获取对应密钥")
|
||||||
accountManager.getSecretKey(), iv,
|
return String(AES.GCM.decrypt(secretKey, iv, Base64.decodeBase64(data)))
|
||||||
Base64.decodeBase64(data)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,4 +26,22 @@ class Team(
|
|||||||
* 所属角色
|
* 所属角色
|
||||||
*/
|
*/
|
||||||
val role: TeamRole,
|
val role: TeamRole,
|
||||||
)
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Team
|
||||||
|
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (name != other.name) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id.hashCode()
|
||||||
|
result = 31 * result + name.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.termora.actions
|
package app.termora.actions
|
||||||
|
|
||||||
import app.termora.NewHostDialogV2
|
import app.termora.NewHostDialogV2
|
||||||
|
import app.termora.account.AccountManager
|
||||||
import app.termora.tree.HostTreeNode
|
import app.termora.tree.HostTreeNode
|
||||||
import javax.swing.tree.TreePath
|
import javax.swing.tree.TreePath
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ class NewHostAction : AnAction() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val accountManager get() = AccountManager.getInstance()
|
||||||
|
|
||||||
override fun actionPerformed(evt: AnActionEvent) {
|
override fun actionPerformed(evt: AnActionEvent) {
|
||||||
val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return
|
val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return
|
||||||
var lastNode = (tree.lastSelectedPathComponent ?: tree.model.root) as? HostTreeNode ?: return
|
var lastNode = (tree.lastSelectedPathComponent ?: tree.model.root) as? HostTreeNode ?: return
|
||||||
@@ -27,7 +30,7 @@ class NewHostAction : AnAction() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val lastHost = lastNode.host
|
val lastHost = lastNode.host
|
||||||
val dialog = NewHostDialogV2(evt.window)
|
val dialog = NewHostDialogV2(evt.window, accountOwner = accountManager.getOwners().first { it.id == lastHost.ownerId })
|
||||||
dialog.setLocationRelativeTo(evt.window)
|
dialog.setLocationRelativeTo(evt.window)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val host = (dialog.host ?: return).copy(
|
val host = (dialog.host ?: return).copy(
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import app.termora.highlight.KeywordHighlightManager
|
|||||||
import app.termora.keymap.KeymapManager
|
import app.termora.keymap.KeymapManager
|
||||||
import app.termora.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
import app.termora.macro.MacroManager
|
import app.termora.macro.MacroManager
|
||||||
import app.termora.plugin.ExtensionManager
|
|
||||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||||
import app.termora.snippet.SnippetManager
|
import app.termora.snippet.SnippetManager
|
||||||
import app.termora.terminal.CursorStyle
|
import app.termora.terminal.CursorStyle
|
||||||
@@ -23,6 +22,7 @@ import org.jetbrains.exposed.v1.core.statements.StatementType
|
|||||||
import org.jetbrains.exposed.v1.jdbc.*
|
import org.jetbrains.exposed.v1.jdbc.*
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
@@ -32,11 +32,8 @@ import kotlin.reflect.KProperty
|
|||||||
|
|
||||||
class DatabaseManager private constructor() : Disposable {
|
class DatabaseManager private constructor() : Disposable {
|
||||||
companion object {
|
companion object {
|
||||||
|
val log: Logger = LoggerFactory.getLogger(DatabaseManager::class.java)
|
||||||
|
|
||||||
private const val DB_PASSWORD = "DB_PASSWORD"
|
|
||||||
private const val DB_SALT = "DB_SALT"
|
|
||||||
|
|
||||||
val log = LoggerFactory.getLogger(DatabaseManager::class.java)!!
|
|
||||||
fun getInstance(): DatabaseManager {
|
fun getInstance(): DatabaseManager {
|
||||||
return ApplicationScope.forApplicationScope()
|
return ApplicationScope.forApplicationScope()
|
||||||
.getOrCreate(DatabaseManager::class) { DatabaseManager() }
|
.getOrCreate(DatabaseManager::class) { DatabaseManager() }
|
||||||
@@ -52,14 +49,6 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
val sftp by lazy { SFTP(this) }
|
val sftp by lazy { SFTP(this) }
|
||||||
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
internal var dbPassword = StringUtils.EMPTY
|
|
||||||
private set
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
internal var dbSalt = StringUtils.EMPTY
|
|
||||||
private set
|
|
||||||
|
|
||||||
private val map = Collections.synchronizedMap<String, String?>(mutableMapOf())
|
private val map = Collections.synchronizedMap<String, String?>(mutableMapOf())
|
||||||
private val accountManager get() = AccountManager.getInstance()
|
private val accountManager get() = AccountManager.getInstance()
|
||||||
|
|
||||||
@@ -101,11 +90,6 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
// 注册动态扩展
|
// 注册动态扩展
|
||||||
registerDynamicExtensions()
|
registerDynamicExtensions()
|
||||||
|
|
||||||
for (extension in ExtensionManager.getInstance().getExtensions(DatabaseReadyExtension::class.java)) {
|
|
||||||
extension.ready(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun registerDynamicExtensions() {
|
private fun registerDynamicExtensions() {
|
||||||
@@ -308,6 +292,7 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
DataEntity.update({ DataEntity.id eq id }) {
|
DataEntity.update({ DataEntity.id eq id }) {
|
||||||
it[DataEntity.deleted] = true
|
it[DataEntity.deleted] = true
|
||||||
// 如果是本地用户,那么删除是不需要同步的,云端用户才需要同步
|
// 如果是本地用户,那么删除是不需要同步的,云端用户才需要同步
|
||||||
|
// 云端用户也会判断,如果来源 Sync 那么默认同步了
|
||||||
it[DataEntity.synced] = accountManager.isLocally()
|
it[DataEntity.synced] = accountManager.isLocally()
|
||||||
it[DataEntity.data] = StringUtils.EMPTY
|
it[DataEntity.data] = StringUtils.EMPTY
|
||||||
}
|
}
|
||||||
@@ -367,7 +352,6 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
|
|
||||||
|
|
||||||
private inner class AccountDataTransferExtension : AccountExtension {
|
private inner class AccountDataTransferExtension : AccountExtension {
|
||||||
private val hostManager get() = HostManager.getInstance()
|
|
||||||
override fun onAccountChanged(oldAccount: Account, newAccount: Account) {
|
override fun onAccountChanged(oldAccount: Account, newAccount: Account) {
|
||||||
if (oldAccount.isLocally && newAccount.isLocally) {
|
if (oldAccount.isLocally && newAccount.isLocally) {
|
||||||
return
|
return
|
||||||
@@ -481,19 +465,17 @@ class DatabaseManager private constructor() : Disposable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果团队变更,那么删除所有旧的团队数据,静默删除
|
||||||
if (oldAccount.id == newAccount.id) {
|
if (oldAccount.id == newAccount.id) {
|
||||||
return
|
if (oldAccount.teams != newAccount.teams) {
|
||||||
}
|
for (team in oldAccount.teams) {
|
||||||
|
lock.withLock {
|
||||||
for (team in oldAccount.teams) {
|
transaction(database) {
|
||||||
// 如果被踢出团队,那么移除该团队的所有资产
|
DataEntity.deleteWhere {
|
||||||
if (newAccount.teams.none { it.id == team.id }) {
|
DataEntity.ownerId.eq(team.id) and (DataEntity.ownerType.eq(
|
||||||
lock.withLock {
|
OwnerType.Team.name
|
||||||
transaction(database) {
|
))
|
||||||
DataEntity.deleteWhere {
|
}
|
||||||
DataEntity.ownerId.eq(team.id) and (DataEntity.ownerType.eq(
|
|
||||||
OwnerType.Team.name
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
package app.termora.database
|
|
||||||
|
|
||||||
import app.termora.database.DatabaseManager.Companion.log
|
|
||||||
import app.termora.plugin.DispatchThread
|
|
||||||
import app.termora.plugin.Extension
|
|
||||||
import app.termora.plugin.ExtensionManager
|
|
||||||
import javax.swing.SwingUtilities
|
|
||||||
|
|
||||||
interface DatabaseReadyExtension : Extension {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fireReady(databaseManager: DatabaseManager) {
|
|
||||||
if (SwingUtilities.isEventDispatchThread()) {
|
|
||||||
for (extension in ExtensionManager.getInstance().getExtensions(DatabaseReadyExtension::class.java)) {
|
|
||||||
try {
|
|
||||||
extension.ready(databaseManager)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (log.isErrorEnabled) {
|
|
||||||
log.error(e.message, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SwingUtilities.invokeLater { fireReady(databaseManager) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 数据库初始化完成
|
|
||||||
*/
|
|
||||||
fun ready(databaseManager: DatabaseManager) {}
|
|
||||||
|
|
||||||
override fun getDispatchThread(): DispatchThread {
|
|
||||||
return DispatchThread.BGT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,6 +19,7 @@ class KeyManagerDialog(
|
|||||||
owner: Window,
|
owner: Window,
|
||||||
private val selectMode: Boolean = false,
|
private val selectMode: Boolean = false,
|
||||||
size: Dimension = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height")),
|
size: Dimension = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height")),
|
||||||
|
private val accountOwner: AccountOwner? = null,
|
||||||
) : DialogWrapper(owner) {
|
) : DialogWrapper(owner) {
|
||||||
var ok: Boolean = false
|
var ok: Boolean = false
|
||||||
|
|
||||||
@@ -56,12 +57,40 @@ class KeyManagerDialog(
|
|||||||
tabbed.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
tabbed.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
|
||||||
tabbed.tabPlacement = JTabbedPane.TOP
|
tabbed.tabPlacement = JTabbedPane.TOP
|
||||||
|
|
||||||
|
if (accountOwner == null || accountOwner.type == OwnerType.User) {
|
||||||
|
tabbed.addTab(
|
||||||
|
I18n.getString("termora.keymgr.my-keys"),
|
||||||
|
Icons.user,
|
||||||
|
KeyManagerPanel(
|
||||||
|
AccountOwner(
|
||||||
|
accountManager.getAccountId(),
|
||||||
|
accountManager.getEmail(),
|
||||||
|
OwnerType.User
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accountOwner != null && accountManager.hasTeamFeature()) {
|
||||||
|
for (team in accountManager.getTeams()) {
|
||||||
|
if (team.id == accountOwner.id) {
|
||||||
|
tabbed.addTab(
|
||||||
|
team.name,
|
||||||
|
Icons.cwmUsers,
|
||||||
|
KeyManagerPanel(
|
||||||
|
AccountOwner(
|
||||||
|
team.id,
|
||||||
|
team.name,
|
||||||
|
OwnerType.Team
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return tabbed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
tabbed.addTab(
|
|
||||||
I18n.getString("termora.keymgr.my-keys"),
|
|
||||||
Icons.user,
|
|
||||||
KeyManagerPanel(AccountOwner(accountManager.getAccountId(), accountManager.getEmail(), OwnerType.User))
|
|
||||||
)
|
|
||||||
|
|
||||||
if (accountManager.hasTeamFeature()) {
|
if (accountManager.hasTeamFeature()) {
|
||||||
for (team in accountManager.getTeams()) {
|
for (team in accountManager.getTeams()) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugin.internal.local
|
package app.termora.plugin.internal.local
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -14,7 +15,7 @@ internal class LocalProtocolHostPanelExtension private constructor() : ProtocolH
|
|||||||
return LocalProtocolProvider.instance
|
return LocalProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return LocalProtocolHostPanel()
|
return LocalProtocolHostPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugin.internal.rdp
|
package app.termora.plugin.internal.rdp
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -14,7 +15,7 @@ internal class RDPProtocolHostPanelExtension private constructor() : ProtocolHos
|
|||||||
return RDPProtocolProvider.instance
|
return RDPProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return RDPProtocolHostPanel()
|
return RDPProtocolHostPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugin.internal.serial
|
package app.termora.plugin.internal.serial
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -14,7 +15,7 @@ internal class SerialProtocolHostPanelExtension private constructor() : Protocol
|
|||||||
return SerialProtocolProvider.instance
|
return SerialProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return SerialProtocolHostPanel()
|
return SerialProtocolHostPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.termora.plugin.internal.ssh
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
import app.termora.keymgr.KeyManagerDialog
|
import app.termora.keymgr.KeyManagerDialog
|
||||||
import app.termora.plugin.internal.BasicProxyOption
|
import app.termora.plugin.internal.BasicProxyOption
|
||||||
@@ -29,7 +30,7 @@ import javax.swing.table.DefaultTableModel
|
|||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@Suppress("CascadeIf")
|
@Suppress("CascadeIf")
|
||||||
open class SSHHostOptionsPane : OptionsPane() {
|
open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
|
||||||
protected val tunnelingOption = TunnelingOption()
|
protected val tunnelingOption = TunnelingOption()
|
||||||
protected val generalOption = GeneralOption()
|
protected val generalOption = GeneralOption()
|
||||||
protected val proxyOption = BasicProxyOption()
|
protected val proxyOption = BasicProxyOption()
|
||||||
@@ -375,6 +376,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
val dialog = KeyManagerDialog(
|
val dialog = KeyManagerDialog(
|
||||||
owner,
|
owner,
|
||||||
selectMode = true,
|
selectMode = true,
|
||||||
|
accountOwner = accountOwner,
|
||||||
)
|
)
|
||||||
dialog.pack()
|
dialog.pack()
|
||||||
dialog.setLocationRelativeTo(owner)
|
dialog.setLocationRelativeTo(owner)
|
||||||
@@ -383,7 +385,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
val selectedItem = publicKeyComboBox.selectedItem
|
val selectedItem = publicKeyComboBox.selectedItem
|
||||||
|
|
||||||
publicKeyComboBox.removeAllItems()
|
publicKeyComboBox.removeAllItems()
|
||||||
for (keyPair in KeyManager.getInstance().getOhKeyPairs()) {
|
for (keyPair in KeyManager.getInstance().getOhKeyPairs(accountOwner.id)) {
|
||||||
publicKeyComboBox.addItem(keyPair.id)
|
publicKeyComboBox.addItem(keyPair.id)
|
||||||
}
|
}
|
||||||
publicKeyComboBox.selectedItem = selectedItem
|
publicKeyComboBox.selectedItem = selectedItem
|
||||||
@@ -465,7 +467,7 @@ open class SSHHostOptionsPane : OptionsPane() {
|
|||||||
if (authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
|
if (authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
|
||||||
val selectedItem = publicKeyComboBox.selectedItem
|
val selectedItem = publicKeyComboBox.selectedItem
|
||||||
publicKeyComboBox.removeAllItems()
|
publicKeyComboBox.removeAllItems()
|
||||||
for (pair in KeyManager.getInstance().getOhKeyPairs()) {
|
for (pair in KeyManager.getInstance().getOhKeyPairs(accountOwner.id)) {
|
||||||
publicKeyComboBox.addItem(pair.id)
|
publicKeyComboBox.addItem(pair.id)
|
||||||
}
|
}
|
||||||
publicKeyComboBox.selectedItem = selectedItem
|
publicKeyComboBox.selectedItem = selectedItem
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ package app.termora.plugin.internal.ssh
|
|||||||
|
|
||||||
import app.termora.Disposer
|
import app.termora.Disposer
|
||||||
import app.termora.Host
|
import app.termora.Host
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
|
|
||||||
class SSHProtocolHostPanel : ProtocolHostPanel() {
|
class SSHProtocolHostPanel(accountOwner: AccountOwner) : ProtocolHostPanel() {
|
||||||
|
|
||||||
private val pane = SSHHostOptionsPane()
|
private val pane = SSHHostOptionsPane(accountOwner)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initView()
|
initView()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugin.internal.ssh
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -14,8 +15,8 @@ internal class SSHProtocolHostPanelExtension private constructor() : ProtocolHos
|
|||||||
return SSHProtocolProvider.instance
|
return SSHProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return SSHProtocolHostPanel()
|
return SSHProtocolHostPanel(accountOwner)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugin.internal.wsl
|
package app.termora.plugin.internal.wsl
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.protocol.ProtocolHostPanel
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
import app.termora.protocol.ProtocolProvider
|
import app.termora.protocol.ProtocolProvider
|
||||||
@@ -14,11 +15,11 @@ internal class WSLProtocolHostPanelExtension private constructor() : ProtocolHos
|
|||||||
return WSLProtocolProvider.instance
|
return WSLProtocolProvider.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun canCreateProtocolHostPanel(): Boolean {
|
override fun canCreateProtocolHostPanel(accountOwner: AccountOwner): Boolean {
|
||||||
return WSLSupport.isSupported
|
return WSLSupport.isSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||||
return WSLProtocolHostPanel()
|
return WSLProtocolHostPanel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package app.termora.protocol
|
package app.termora.protocol
|
||||||
|
|
||||||
|
import app.termora.account.AccountOwner
|
||||||
import app.termora.plugin.Extension
|
import app.termora.plugin.Extension
|
||||||
import app.termora.plugin.ExtensionManager
|
import app.termora.plugin.ExtensionManager
|
||||||
|
|
||||||
|
|
||||||
interface ProtocolHostPanelExtension : Extension {
|
interface ProtocolHostPanelExtension : Extension {
|
||||||
companion object {
|
companion object {
|
||||||
val extensions
|
val extensions
|
||||||
@@ -19,11 +21,23 @@ interface ProtocolHostPanelExtension : Extension {
|
|||||||
/**
|
/**
|
||||||
* 是否可以创建协议主机面板
|
* 是否可以创建协议主机面板
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Old stuff")
|
||||||
fun canCreateProtocolHostPanel(): Boolean = true
|
fun canCreateProtocolHostPanel(): Boolean = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否可以创建协议主机面板
|
||||||
|
*/
|
||||||
|
fun canCreateProtocolHostPanel(accountOwner: AccountOwner) = canCreateProtocolHostPanel()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建协议主机面板
|
* 创建协议主机面板
|
||||||
*/
|
*/
|
||||||
fun createProtocolHostPanel(): ProtocolHostPanel
|
@Deprecated("Old stuff")
|
||||||
|
fun createProtocolHostPanel(): ProtocolHostPanel = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建协议主机面板
|
||||||
|
*/
|
||||||
|
fun createProtocolHostPanel(accountOwner: AccountOwner) = createProtocolHostPanel()
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.termora.transfer
|
package app.termora.transfer
|
||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
|
import app.termora.account.AccountManager
|
||||||
import app.termora.actions.AnAction
|
import app.termora.actions.AnAction
|
||||||
import app.termora.actions.AnActionEvent
|
import app.termora.actions.AnActionEvent
|
||||||
import app.termora.database.DatabaseChangedExtension
|
import app.termora.database.DatabaseChangedExtension
|
||||||
@@ -164,9 +165,13 @@ 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 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(window, panel.host)
|
val dialog = NewHostDialogV2(
|
||||||
|
window,
|
||||||
|
panel.host,
|
||||||
|
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
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package app.termora.tree
|
|||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.Application.ohMyJson
|
import app.termora.Application.ohMyJson
|
||||||
|
import app.termora.account.AccountManager
|
||||||
import app.termora.actions.OpenHostAction
|
import app.termora.actions.OpenHostAction
|
||||||
import app.termora.database.DatabaseChangedExtension
|
import app.termora.database.DatabaseChangedExtension
|
||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
@@ -295,8 +296,11 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
newHost.addActionListener(object : ActionListener {
|
newHost.addActionListener(object : ActionListener {
|
||||||
|
private val accountManager get() = AccountManager.getInstance()
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
val dialog = NewHostDialogV2(owner)
|
val dialog = NewHostDialogV2(
|
||||||
|
owner,
|
||||||
|
accountOwner = accountManager.getOwners().first { it.id == lastHost.ownerId })
|
||||||
dialog.setLocationRelativeTo(owner)
|
dialog.setLocationRelativeTo(owner)
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
val host = (dialog.host ?: return).copy(
|
val host = (dialog.host ?: return).copy(
|
||||||
@@ -311,8 +315,12 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
property.addActionListener(object : ActionListener {
|
property.addActionListener(object : ActionListener {
|
||||||
|
private val accountManager get() = AccountManager.getInstance()
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
override fun actionPerformed(e: ActionEvent) {
|
||||||
val dialog = NewHostDialogV2(owner, lastHost)
|
val dialog = NewHostDialogV2(
|
||||||
|
owner,
|
||||||
|
lastHost,
|
||||||
|
accountOwner = accountManager.getOwners().first { it.id == lastHost.ownerId })
|
||||||
dialog.setLocationRelativeTo(owner)
|
dialog.setLocationRelativeTo(owner)
|
||||||
dialog.title = lastHost.name
|
dialog.title = lastHost.name
|
||||||
dialog.isVisible = true
|
dialog.isVisible = true
|
||||||
@@ -639,12 +647,12 @@ class NewHostTree : SimpleTree(), Disposable {
|
|||||||
ownerType = folder.host.ownerType,
|
ownerType = folder.host.ownerType,
|
||||||
ownerId = folder.host.ownerId,
|
ownerId = folder.host.ownerId,
|
||||||
),
|
),
|
||||||
DatabaseChangedExtension.Source.Sync
|
DatabaseChangedExtension.Source.User
|
||||||
)
|
)
|
||||||
for (host in node.getAllChildren().map { it.host }) {
|
for (host in node.getAllChildren().map { it.host }) {
|
||||||
hostManager.addHost(
|
hostManager.addHost(
|
||||||
host.copy(ownerType = folder.host.ownerType, ownerId = folder.host.ownerId),
|
host.copy(ownerType = folder.host.ownerType, ownerId = folder.host.ownerId),
|
||||||
DatabaseChangedExtension.Source.Sync
|
DatabaseChangedExtension.Source.User
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,10 @@ class NewHostTreeModel private constructor() : SimpleTreeModel<Host>(
|
|||||||
}
|
}
|
||||||
hostManager.removeHost(node.id)
|
hostManager.removeHost(node.id)
|
||||||
}
|
}
|
||||||
|
removeNodeFromParent0(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeNodeFromParent0(node: MutableTreeNode?) {
|
||||||
super.removeNodeFromParent(node)
|
super.removeNodeFromParent(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +236,13 @@ class NewHostTreeModel private constructor() : SimpleTreeModel<Host>(
|
|||||||
|
|
||||||
private inner class MyAccountAccountExtension : AccountExtension {
|
private inner class MyAccountAccountExtension : AccountExtension {
|
||||||
override fun onAccountChanged(oldAccount: Account, newAccount: Account) {
|
override fun onAccountChanged(oldAccount: Account, newAccount: Account) {
|
||||||
if (oldAccount.id != newAccount.id) reload(getRoot())
|
if (oldAccount.id != newAccount.id) {
|
||||||
|
reload(getRoot())
|
||||||
|
} else if (oldAccount.teams != newAccount.teams) {
|
||||||
|
val nodes = getRoot().children().toList().filterIsInstance<TeamTreeNode>()
|
||||||
|
nodes.forEach { removeNodeFromParent0(it) }
|
||||||
|
reload(getRoot())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user