mirror of
https://gitlab.com/2009scape/rt4-client.git
synced 2025-12-09 16:45:46 -07:00
Merge branch 'KondoImprovements' into 'master'
Draft: KondoKit V2.1 + ChatboxHelms plugin See merge request 2009scape/rt4-client!30
This commit is contained in:
commit
59cefc5f44
33 changed files with 4474 additions and 1078 deletions
390
plugin-playground/src/main/kotlin/ChatboxHelmets/plugin.kt
Normal file
390
plugin-playground/src/main/kotlin/ChatboxHelmets/plugin.kt
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
package ChatboxHelmets
|
||||
|
||||
import KondoKit.Exposed
|
||||
import plugin.Plugin
|
||||
import plugin.api.API
|
||||
import rt4.Component
|
||||
import rt4.JagString
|
||||
import rt4.Player
|
||||
import java.nio.charset.StandardCharsets
|
||||
import com.google.gson.Gson
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
class plugin : Plugin() {
|
||||
|
||||
private val TARGET_COMPONENT_ID = 8978483
|
||||
private val TARGET_COMPONENT_INDEX = 51
|
||||
private val SETTINGS_KEY = "chat-helms"
|
||||
private val BUBBLE_ICON = "<img=3>"
|
||||
private val gson = Gson()
|
||||
|
||||
val TYPE_ICONS: Map<Int, String> = hashMapOf(
|
||||
0 to "",
|
||||
1 to "<img=4>", // IM
|
||||
2 to "<img=5>", // HCIM
|
||||
3 to "<img=6>", // UIM
|
||||
)
|
||||
|
||||
@Exposed(description = "Username to account type mappings (0=Normal, 1=IM, 2=HCIM, 3=UIM)")
|
||||
private var usernameMatches = HashMap<String, Int>()
|
||||
|
||||
private var ACCOUNT_TYPE = 0
|
||||
private var component: Component? = null
|
||||
|
||||
// See JagString.java parse() in the client, There is special encoding
|
||||
// and this correctly resolves specials.
|
||||
val SPECIAL_ESCAPES: Map<Char, String> = hashMapOf(
|
||||
' ' to "(P",
|
||||
'!' to "(Q",
|
||||
'"' to "(R",
|
||||
'#' to "(S",
|
||||
'$' to "(T",
|
||||
'%' to "(U",
|
||||
'&' to "(V",
|
||||
'\'' to "(W",
|
||||
'(' to "(X",
|
||||
')' to "(Y",
|
||||
'*' to "(Z",
|
||||
'+' to ")0",
|
||||
',' to ")1",
|
||||
'-' to ")2",
|
||||
'.' to ")3",
|
||||
'/' to ")4",
|
||||
':' to "(j",
|
||||
';' to "(k",
|
||||
'=' to ")B",
|
||||
'?' to ")D",
|
||||
'@' to ")E",
|
||||
'[' to "*5",
|
||||
'\\' to "*6",
|
||||
']' to "*7",
|
||||
'^' to "*8",
|
||||
'_' to "*9",
|
||||
'`' to ")e",
|
||||
'{' to "*U",
|
||||
'|' to "*V",
|
||||
'}' to "*W",
|
||||
'~' to "*X",
|
||||
'\u0000' to "(0",
|
||||
'\u0001' to "(1",
|
||||
'\u0002' to "(2",
|
||||
'\u0003' to "(3",
|
||||
'\u0004' to "(4",
|
||||
'\u0005' to "(5",
|
||||
'\u0006' to "(6",
|
||||
'\u0007' to "(7",
|
||||
'\b' to "(8",
|
||||
'\t' to "(9",
|
||||
'\n' to "(:",
|
||||
'\u000B' to "(;",
|
||||
'\u000C' to "(<",
|
||||
'\r' to "(=",
|
||||
'\u000E' to "(>",
|
||||
'\u000F' to "(?",
|
||||
'\u0010' to "(@",
|
||||
'\u0011' to "(A",
|
||||
'\u0012' to "(B",
|
||||
'\u0013' to "(C",
|
||||
'\u0014' to "(D",
|
||||
'\u0015' to "(E",
|
||||
'\u0016' to "(F",
|
||||
'\u0017' to "(G",
|
||||
'\u0018' to "(H",
|
||||
'\u0019' to "(I",
|
||||
'\u001A' to "(J",
|
||||
'\u001B' to "(K",
|
||||
'\u001C' to "(L",
|
||||
'\u001D' to "(M",
|
||||
'\u001E' to "(N",
|
||||
'\u001F' to "(O",
|
||||
'\u007F' to "*Y",
|
||||
'\u0080' to "*Z",
|
||||
'\u0081' to "+0",
|
||||
'\u0082' to "+1",
|
||||
'\u0083' to "+2",
|
||||
'\u0084' to "+3",
|
||||
'\u0085' to "+4",
|
||||
'\u0086' to "+5",
|
||||
'\u0087' to "+6",
|
||||
'\u0088' to "+7",
|
||||
'\u0089' to "+8",
|
||||
'\u008A' to "+9",
|
||||
'\u008B' to "*e",
|
||||
'\u008C' to "*f",
|
||||
'\u008D' to "*g",
|
||||
'\u008E' to "*h",
|
||||
'\u008F' to "*i",
|
||||
'\u0090' to "*j",
|
||||
'\u0091' to "*k",
|
||||
'\u0092' to "+A",
|
||||
'\u0093' to "+B",
|
||||
'\u0094' to "+C",
|
||||
'\u0095' to "+D",
|
||||
'\u0096' to "+E",
|
||||
'\u0097' to "+F",
|
||||
'\u0098' to "+G",
|
||||
'\u0099' to "+H",
|
||||
'\u009A' to "+I",
|
||||
'\u009B' to "+J",
|
||||
'\u009C' to "+K",
|
||||
'\u009D' to "+L",
|
||||
'\u009E' to "+M",
|
||||
'\u009F' to "+N",
|
||||
'\u00A0' to "+O",
|
||||
'\u00A1' to "+P",
|
||||
'\u00A2' to "+Q",
|
||||
'\u00A3' to "+R",
|
||||
'\u00A4' to "+S",
|
||||
'\u00A5' to "+T",
|
||||
'\u00A6' to "+U",
|
||||
'\u00A7' to "+V",
|
||||
'\u00A8' to "+W",
|
||||
'\u00A9' to "+X",
|
||||
'\u00AA' to "+Y",
|
||||
'\u00AB' to "+Z",
|
||||
'\u00AC' to ",0",
|
||||
'\u00AD' to ",1",
|
||||
'\u00AE' to ",2",
|
||||
'\u00AF' to ",3",
|
||||
'\u00B0' to ",4",
|
||||
'\u00B1' to ",5",
|
||||
'\u00B2' to ",6",
|
||||
'\u00B3' to ",7",
|
||||
'\u00B4' to ",8",
|
||||
'\u00B5' to ",9",
|
||||
'\u00B6' to "+e",
|
||||
'\u00B7' to "+f",
|
||||
'\u00B8' to "+g",
|
||||
'\u00B9' to "+h",
|
||||
'\u00BA' to "+i",
|
||||
'\u00BB' to "+j",
|
||||
'\u00BC' to "+k",
|
||||
'\u00BD' to ",A",
|
||||
'\u00BE' to ",B",
|
||||
'\u00BF' to ",C",
|
||||
'\u00C0' to ",D",
|
||||
'\u00C1' to ",E",
|
||||
'\u00C2' to ",F",
|
||||
'\u00C3' to ",G",
|
||||
'\u00C4' to ",H",
|
||||
'\u00C5' to ",I",
|
||||
'\u00C6' to ",J",
|
||||
'\u00C7' to ",K",
|
||||
'\u00C8' to ",L",
|
||||
'\u00C9' to ",M",
|
||||
'\u00CA' to ",N",
|
||||
'\u00CB' to ",O",
|
||||
'\u00CC' to ",P",
|
||||
'\u00CD' to ",Q",
|
||||
'\u00CE' to ",R",
|
||||
'\u00CF' to ",S",
|
||||
'\u00D0' to ",T",
|
||||
'\u00D1' to ",U",
|
||||
'\u00D2' to ",V",
|
||||
'\u00D3' to ",W",
|
||||
'\u00D4' to ",X",
|
||||
'\u00D5' to ",Y",
|
||||
'\u00D6' to ",Z",
|
||||
'\u00D7' to "-0",
|
||||
'\u00D8' to "-1",
|
||||
'\u00D9' to "-2",
|
||||
'\u00DA' to "-3",
|
||||
'\u00DB' to "-4",
|
||||
'\u00DC' to "-5",
|
||||
'\u00DD' to "-6",
|
||||
'\u00DE' to "-7",
|
||||
'\u00DF' to "-8",
|
||||
'\u00E0' to "-9",
|
||||
'\u00E1' to ",e",
|
||||
'\u00E2' to ",f",
|
||||
'\u00E3' to ",g",
|
||||
'\u00E4' to ",h",
|
||||
'\u00E5' to ",i",
|
||||
'\u00E6' to ",j",
|
||||
'\u00E7' to ",k",
|
||||
'\u00E8' to "-A",
|
||||
'\u00E9' to "-B",
|
||||
'\u00EA' to "-C",
|
||||
'\u00EB' to "-D",
|
||||
'\u00EC' to "-E",
|
||||
'\u00ED' to "-F",
|
||||
'\u00EE' to "-G",
|
||||
'\u00EF' to "-H",
|
||||
'\u00F0' to "-I",
|
||||
'\u00F1' to "-J",
|
||||
'\u00F2' to "-K",
|
||||
'\u00F3' to "-L",
|
||||
'\u00F4' to "-M",
|
||||
'\u00F5' to "-N",
|
||||
'\u00F6' to "-O",
|
||||
'\u00F7' to "-P",
|
||||
'\u00F8' to "-Q",
|
||||
'\u00F9' to "-R",
|
||||
'\u00FA' to "-S",
|
||||
'\u00FB' to "-T",
|
||||
'\u00FC' to "-U",
|
||||
'\u00FD' to "-V",
|
||||
'\u00FE' to "-W",
|
||||
'\u00FF' to "-X"
|
||||
)
|
||||
|
||||
override fun Init() {
|
||||
// Load username matches from local storage
|
||||
val storedData = API.GetData(SETTINGS_KEY)
|
||||
usernameMatches = when (storedData) {
|
||||
is String -> {
|
||||
try {
|
||||
gson.fromJson(storedData, HashMap::class.java) as HashMap<String, Int>
|
||||
} catch (e: Exception) {
|
||||
println("Failed to deserialize username matches: ${e.message}")
|
||||
HashMap()
|
||||
}
|
||||
}
|
||||
else -> HashMap()
|
||||
}
|
||||
}
|
||||
|
||||
override fun OnPluginsReloaded(): Boolean {
|
||||
grabConfig()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun Draw(timeDelta: Long) {
|
||||
if (component != null && component?.id == TARGET_COMPONENT_ID) {
|
||||
val modifiedStr = replaceUsernameInBytes(component!!.text.chars, Player.usernameInput.toString())
|
||||
component?.text = JagString.parse(modifiedStr)
|
||||
}
|
||||
}
|
||||
|
||||
override fun ComponentDraw(componentIndex: Int, component: Component?, screenX: Int, screenY: Int) {
|
||||
if (component?.id == TARGET_COMPONENT_ID && componentIndex == TARGET_COMPONENT_INDEX) {
|
||||
this.component = component
|
||||
}
|
||||
}
|
||||
|
||||
override fun OnLogin() {
|
||||
grabConfig()
|
||||
}
|
||||
|
||||
// Allow setting from the chatbox
|
||||
override fun ProcessCommand(commandStr: String?, args: Array<out String>?) {
|
||||
super.ProcessCommand(commandStr, args)
|
||||
when(commandStr) {
|
||||
"::chathelm" -> {
|
||||
if (args != null) {
|
||||
if(args.isEmpty()) return
|
||||
val type = args[0].toIntOrNull() ?: return
|
||||
ACCOUNT_TYPE = type
|
||||
usernameMatches[getCleanUserName()] = ACCOUNT_TYPE
|
||||
storeData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun OnKondoValueUpdated() {
|
||||
storeData()
|
||||
OnPluginsReloaded() //refresh the ui
|
||||
}
|
||||
|
||||
private fun getCleanUserName(): String {
|
||||
return Player.usernameInput.toString().replace(" ", "_")
|
||||
}
|
||||
|
||||
private fun grabConfig() {
|
||||
// Check if we already have the account type for this user
|
||||
val cleanUsername = getCleanUserName()
|
||||
if (usernameMatches.containsKey(cleanUsername.toLowerCase())) {
|
||||
ACCOUNT_TYPE = usernameMatches[cleanUsername]!!
|
||||
return
|
||||
}
|
||||
|
||||
// No match found, check the hiscores (live server)
|
||||
fetchAccountTypeFromAPI()
|
||||
}
|
||||
|
||||
private fun fetchAccountTypeFromAPI(){
|
||||
val cleanUsername = getCleanUserName()
|
||||
val apiUrl = "http://api.2009scape.org:3000/hiscores/playerSkills/1/${cleanUsername.toLowerCase()}"
|
||||
Thread {
|
||||
try {
|
||||
val url = URL(apiUrl)
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
|
||||
// If a request take longer than 5 seconds timeout.
|
||||
connection.connectTimeout = 5000
|
||||
connection.readTimeout = 5000
|
||||
|
||||
val responseCode = connection.responseCode
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
val reader = BufferedReader(InputStreamReader(connection.inputStream))
|
||||
val response = reader.use { it.readText() }
|
||||
reader.close()
|
||||
updatePlayerData(response, cleanUsername)
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun updatePlayerData(jsonResponse: String, username: String) {
|
||||
val hiscoresResponse = gson.fromJson(jsonResponse, HiscoresResponse::class.java)
|
||||
ACCOUNT_TYPE = hiscoresResponse.info.iron_mode.toInt()
|
||||
|
||||
// Store the result in our local cache
|
||||
usernameMatches[username] = ACCOUNT_TYPE
|
||||
storeData()
|
||||
}
|
||||
|
||||
private fun replaceUsernameInBytes(bytes: ByteArray, username: String): String {
|
||||
// Convert bytes to string to work with them
|
||||
val text = String(bytes, StandardCharsets.ISO_8859_1)
|
||||
val colonIndex = text.indexOf(": ")
|
||||
if (colonIndex != -1) {
|
||||
val suffix = text.substring(colonIndex)
|
||||
// modify and add the rest (username is always before the first ':' )
|
||||
val newText = "${TYPE_ICONS.getOrDefault(ACCOUNT_TYPE, "")}$username$BUBBLE_ICON$suffix"
|
||||
return encodeToJagStr(newText)
|
||||
}
|
||||
return encodeToJagStr(text)
|
||||
}
|
||||
|
||||
private fun storeData() {
|
||||
val jsonString = gson.toJson(usernameMatches)
|
||||
API.StoreData(SETTINGS_KEY, jsonString)
|
||||
}
|
||||
|
||||
private fun encodeToJagStr(str: String): String {
|
||||
val result = StringBuilder()
|
||||
for (char in str) {
|
||||
val escaped = SPECIAL_ESCAPES[char]
|
||||
if (escaped != null) {
|
||||
result.append(escaped)
|
||||
} else {
|
||||
result.append(char)
|
||||
}
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
// Lifted from KondoHiscores view
|
||||
data class HiscoresResponse(
|
||||
val info: PlayerInfo,
|
||||
val skills: List<Skill>
|
||||
)
|
||||
|
||||
data class PlayerInfo(
|
||||
val exp_multiplier: String,
|
||||
val iron_mode: String
|
||||
)
|
||||
|
||||
data class Skill(
|
||||
val id: String,
|
||||
val dynamic: String,
|
||||
val experience: String,
|
||||
val static: String
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
AUTHOR='downthecrop'
|
||||
DESCRIPTION='Displays account type icons next to usernames in chat pulled from the live server API. \
|
||||
Can be set with a chat command for single player with ::chathelm 1 (0=Normal, 1=IM, 2=HCIM, 3=UIM) \
|
||||
or via KondoKit settings.'
|
||||
VERSION=1.0
|
||||
BIN
plugin-playground/src/main/kotlin/KondoKit/.DS_Store
vendored
Normal file
BIN
plugin-playground/src/main/kotlin/KondoKit/.DS_Store
vendored
Normal file
Binary file not shown.
|
|
@ -3,6 +3,8 @@ package KondoKit
|
|||
import rt4.GameShell
|
||||
import java.awt.*
|
||||
import java.awt.event.MouseListener
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
|
|
@ -11,6 +13,16 @@ import java.util.Timer
|
|||
import javax.swing.*
|
||||
|
||||
object Helpers {
|
||||
// Convenience helper for loading resources relative to the KondoKit plugin class
|
||||
fun openResource(path: String) = plugin::class.java.getResourceAsStream(path)
|
||||
|
||||
// Read a bundled resource as text using the given charset (UTF-8 by default)
|
||||
fun readResourceText(path: String, charset: Charset = StandardCharsets.UTF_8): String? {
|
||||
val stream = openResource(path) ?: return null
|
||||
stream.use { s ->
|
||||
return s.reader(charset).use { it.readText() }
|
||||
}
|
||||
}
|
||||
|
||||
fun convertValue(type: Class<*>, genericType: Type?, value: String): Any {
|
||||
return when {
|
||||
|
|
@ -233,4 +245,4 @@ object Helpers {
|
|||
else -> Color(128, 128, 128) // Default grey for unhandled skill IDs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,324 +0,0 @@
|
|||
package KondoKit
|
||||
|
||||
import KondoKit.Helpers.convertValue
|
||||
import KondoKit.Helpers.showToast
|
||||
import KondoKit.plugin.Companion.TITLE_BAR_COLOR
|
||||
import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
|
||||
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
|
||||
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||
import KondoKit.plugin.Companion.primaryColor
|
||||
import KondoKit.plugin.Companion.secondaryColor
|
||||
import KondoKit.plugin.StateManager.focusedView
|
||||
import plugin.Plugin
|
||||
import plugin.PluginInfo
|
||||
import plugin.PluginRepository
|
||||
import java.awt.*
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.*
|
||||
import java.util.Timer
|
||||
import javax.swing.*
|
||||
import kotlin.math.ceil
|
||||
|
||||
/*
|
||||
This is used for the runtime editing of plugin variables.
|
||||
To expose fields add the @Exposed annotation.
|
||||
When they are applied this will trigger an invoke of OnKondoValueUpdated()
|
||||
if it is implemented. Check GroundItems plugin for an example.
|
||||
*/
|
||||
|
||||
|
||||
object ReflectiveEditorView {
|
||||
var reflectiveEditorView: JPanel? = null
|
||||
private val loadedPlugins: MutableList<String> = mutableListOf()
|
||||
const val VIEW_NAME = "REFLECTIVE_EDITOR_VIEW"
|
||||
fun createReflectiveEditorView() {
|
||||
val reflectiveEditorPanel = JPanel(BorderLayout())
|
||||
reflectiveEditorPanel.background = VIEW_BACKGROUND_COLOR
|
||||
reflectiveEditorPanel.add(Box.createVerticalStrut(5))
|
||||
reflectiveEditorPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
|
||||
reflectiveEditorView = reflectiveEditorPanel
|
||||
addPlugins(reflectiveEditorView!!)
|
||||
}
|
||||
|
||||
fun addPlugins(reflectiveEditorView: JPanel) {
|
||||
reflectiveEditorView.removeAll() // clear previous
|
||||
loadedPlugins.clear()
|
||||
try {
|
||||
val loadedPluginsField = PluginRepository::class.java.getDeclaredField("loadedPlugins")
|
||||
loadedPluginsField.isAccessible = true
|
||||
val loadedPlugins = loadedPluginsField.get(null) as HashMap<*, *>
|
||||
|
||||
for ((pluginInfo, plugin) in loadedPlugins) {
|
||||
addPluginToEditor(reflectiveEditorView, pluginInfo as PluginInfo, plugin as Plugin)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
// Add a centered box for plugins that have no exposed fields
|
||||
if (loadedPlugins.isNotEmpty()) {
|
||||
val noExposedPanel = JPanel(BorderLayout())
|
||||
noExposedPanel.background = VIEW_BACKGROUND_COLOR
|
||||
noExposedPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
|
||||
|
||||
val label = JLabel("Loaded Plugins without Exposed Fields", SwingConstants.CENTER)
|
||||
label.font = Font("RuneScape Small", Font.PLAIN, 16)
|
||||
label.foreground = primaryColor
|
||||
noExposedPanel.add(label, BorderLayout.NORTH)
|
||||
|
||||
val pluginsList = JList(loadedPlugins.toTypedArray())
|
||||
pluginsList.background = WIDGET_COLOR
|
||||
pluginsList.foreground = secondaryColor
|
||||
pluginsList.font = Font("RuneScape Small", Font.PLAIN, 16)
|
||||
|
||||
// Wrap the JList in a JScrollPane with a fixed height
|
||||
val maxScrollPaneHeight = 200
|
||||
val scrollPane = JScrollPane(pluginsList).apply {
|
||||
verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
|
||||
}
|
||||
|
||||
// Create a wrapper panel with BoxLayout to constrain the scroll pane
|
||||
val scrollPaneWrapper = JPanel().apply {
|
||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
||||
add(scrollPane)
|
||||
}
|
||||
|
||||
noExposedPanel.add(scrollPaneWrapper, BorderLayout.CENTER)
|
||||
|
||||
// Center the panel within the reflectiveEditorView
|
||||
val centeredPanel = JPanel().apply {
|
||||
preferredSize = Dimension(240, maxScrollPaneHeight)
|
||||
maximumSize = preferredSize
|
||||
minimumSize = preferredSize
|
||||
}
|
||||
centeredPanel.layout = BoxLayout(centeredPanel, BoxLayout.Y_AXIS)
|
||||
centeredPanel.add(Box.createVerticalGlue())
|
||||
centeredPanel.add(noExposedPanel)
|
||||
centeredPanel.add(Box.createVerticalGlue())
|
||||
|
||||
reflectiveEditorView.add(Box.createVerticalStrut(10))
|
||||
reflectiveEditorView.add(centeredPanel)
|
||||
}
|
||||
|
||||
|
||||
reflectiveEditorView.revalidate()
|
||||
if(focusedView == VIEW_NAME)
|
||||
reflectiveEditorView.repaint()
|
||||
}
|
||||
|
||||
private fun addPluginToEditor(reflectiveEditorView: JPanel, pluginInfo : PluginInfo, plugin: Plugin) {
|
||||
reflectiveEditorView.layout = BoxLayout(reflectiveEditorView, BoxLayout.Y_AXIS)
|
||||
|
||||
val fieldNotifier = Helpers.FieldNotifier(plugin)
|
||||
val exposedFields = plugin.javaClass.declaredFields.filter { field ->
|
||||
field.annotations.any { annotation ->
|
||||
annotation.annotationClass.simpleName == "Exposed"
|
||||
}
|
||||
}
|
||||
|
||||
if (exposedFields.isNotEmpty()) {
|
||||
|
||||
val packageName = plugin.javaClass.`package`.name
|
||||
val version = pluginInfo.version
|
||||
val labelPanel = JPanel(BorderLayout())
|
||||
labelPanel.maximumSize = Dimension(Int.MAX_VALUE, 30)
|
||||
labelPanel.background = VIEW_BACKGROUND_COLOR
|
||||
labelPanel.border = BorderFactory.createEmptyBorder(5, 0, 0, 0)
|
||||
|
||||
val label = JLabel("$packageName v$version", SwingConstants.CENTER)
|
||||
label.foreground = primaryColor
|
||||
label.font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||
labelPanel.add(label, BorderLayout.CENTER)
|
||||
label.isOpaque = true
|
||||
label.background = TITLE_BAR_COLOR
|
||||
reflectiveEditorView.add(labelPanel)
|
||||
|
||||
for (field in exposedFields) {
|
||||
field.isAccessible = true
|
||||
|
||||
// Get the "Exposed" annotation specifically and retrieve its description, if available
|
||||
val exposedAnnotation = field.annotations.firstOrNull { annotation ->
|
||||
annotation.annotationClass.simpleName == "Exposed"
|
||||
}
|
||||
|
||||
val description = exposedAnnotation?.let { annotation ->
|
||||
try {
|
||||
val descriptionField = annotation.annotationClass.java.getMethod("description")
|
||||
descriptionField.invoke(annotation) as String
|
||||
} catch (e: NoSuchMethodException) {
|
||||
"" // No description method, return empty string
|
||||
}
|
||||
} ?: ""
|
||||
|
||||
val fieldPanel = JPanel()
|
||||
fieldPanel.layout = GridBagLayout()
|
||||
fieldPanel.background = WIDGET_COLOR
|
||||
fieldPanel.foreground = secondaryColor
|
||||
fieldPanel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0)
|
||||
fieldPanel.maximumSize = Dimension(Int.MAX_VALUE, 40)
|
||||
|
||||
val gbc = GridBagConstraints()
|
||||
gbc.insets = Insets(0, 5, 0, 5)
|
||||
|
||||
val label = JLabel(field.name.capitalize())
|
||||
label.foreground = secondaryColor
|
||||
gbc.gridx = 0
|
||||
gbc.gridy = 0
|
||||
gbc.weightx = 0.0
|
||||
label.font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||
gbc.anchor = GridBagConstraints.WEST
|
||||
fieldPanel.add(label, gbc)
|
||||
|
||||
// Create appropriate input component based on field type
|
||||
val inputComponent: JComponent = when {
|
||||
field.type == Boolean::class.javaPrimitiveType || field.type == java.lang.Boolean::class.java -> JCheckBox().apply {
|
||||
isSelected = field.get(plugin) as Boolean
|
||||
}
|
||||
|
||||
field.type.isEnum -> JComboBox((field.type.enumConstants as Array<Enum<*>>)).apply {
|
||||
selectedItem = field.get(plugin)
|
||||
}
|
||||
|
||||
field.type == Int::class.javaPrimitiveType || field.type == Integer::class.java -> JSpinner(SpinnerNumberModel(field.get(plugin) as Int, Int.MIN_VALUE, Int.MAX_VALUE, 1))
|
||||
field.type == Float::class.javaPrimitiveType || field.type == Double::class.javaPrimitiveType || field.type == java.lang.Float::class.java || field.type == java.lang.Double::class.java -> JSpinner(SpinnerNumberModel((field.get(plugin) as Number).toDouble(), -Double.MAX_VALUE, Double.MAX_VALUE, 0.1))
|
||||
else -> JTextField(field.get(plugin)?.toString() ?: "")
|
||||
}
|
||||
|
||||
// Add mouse listener to the label only if a description is available
|
||||
if (description.isNotBlank()) {
|
||||
label.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
label.addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
showCustomToolTip(description, label)
|
||||
}
|
||||
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
customToolTipWindow?.isVisible = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
gbc.gridx = 1
|
||||
gbc.gridy = 0
|
||||
gbc.weightx = 1.0
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL
|
||||
fieldPanel.add(inputComponent, gbc)
|
||||
|
||||
val applyButton = JButton("\u2714").apply {
|
||||
maximumSize = Dimension(Int.MAX_VALUE, 8)
|
||||
}
|
||||
gbc.gridx = 2
|
||||
gbc.gridy = 0
|
||||
gbc.weightx = 0.0
|
||||
gbc.fill = GridBagConstraints.NONE
|
||||
applyButton.addActionListener {
|
||||
try {
|
||||
val newValue = when (inputComponent) {
|
||||
is JCheckBox -> inputComponent.isSelected
|
||||
is JComboBox<*> -> inputComponent.selectedItem
|
||||
is JSpinner -> inputComponent.value
|
||||
is JTextField -> convertValue(field.type, field.genericType, inputComponent.text)
|
||||
else -> throw IllegalArgumentException("Unsupported input component type")
|
||||
}
|
||||
fieldNotifier.setFieldValue(field, newValue)
|
||||
showToast(
|
||||
reflectiveEditorView,
|
||||
"${field.name} updated successfully!"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
showToast(
|
||||
reflectiveEditorView,
|
||||
"Failed to update ${field.name}: ${e.message}",
|
||||
JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fieldPanel.add(applyButton, gbc)
|
||||
reflectiveEditorView.add(fieldPanel)
|
||||
|
||||
// Track field changes in real-time and update UI
|
||||
var previousValue = field.get(plugin)?.toString()
|
||||
val timer = Timer()
|
||||
timer.schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
val currentValue = field.get(plugin)?.toString()
|
||||
if (currentValue != previousValue) {
|
||||
previousValue = currentValue
|
||||
SwingUtilities.invokeLater {
|
||||
// Update the inputComponent based on the new value
|
||||
when (inputComponent) {
|
||||
is JCheckBox -> inputComponent.isSelected = field.get(plugin) as Boolean
|
||||
is JComboBox<*> -> inputComponent.selectedItem = field.get(plugin)
|
||||
is JSpinner -> inputComponent.value = field.get(plugin)
|
||||
is JTextField -> inputComponent.text = field.get(plugin)?.toString() ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0, 1000) // Poll every 1000 milliseconds (1 second)
|
||||
}
|
||||
|
||||
if (exposedFields.isNotEmpty()) {
|
||||
reflectiveEditorView.add(Box.createVerticalStrut(5))
|
||||
}
|
||||
}
|
||||
else {
|
||||
loadedPlugins.add(plugin.javaClass.`package`.name)
|
||||
}
|
||||
}
|
||||
|
||||
var customToolTipWindow: JWindow? = null
|
||||
|
||||
fun showCustomToolTip(text: String, component: JComponent) {
|
||||
val _font = Font("RuneScape Small", Font.PLAIN, 16)
|
||||
val maxWidth = 150
|
||||
val lineHeight = 16
|
||||
|
||||
// Create a dummy JLabel to get FontMetrics for the font used in the tooltip
|
||||
val dummyLabel = JLabel()
|
||||
dummyLabel.font = _font
|
||||
val fontMetrics = dummyLabel.getFontMetrics(_font)
|
||||
|
||||
// Calculate the approximate width of the text
|
||||
val textWidth = fontMetrics.stringWidth(text)
|
||||
|
||||
// Calculate the number of lines required based on the text width and max tooltip width
|
||||
val numberOfLines = ceil(textWidth.toDouble() / maxWidth).toInt()
|
||||
|
||||
// Calculate the required height of the tooltip
|
||||
val requiredHeight = numberOfLines * lineHeight + 6 // Adding some padding
|
||||
|
||||
if (customToolTipWindow == null) {
|
||||
customToolTipWindow = JWindow().apply {
|
||||
val bgColor = Helpers.colorToHex(TOOLTIP_BACKGROUND)
|
||||
val textColor = Helpers.colorToHex(secondaryColor)
|
||||
contentPane = JLabel("<html><div style='color: $textColor; background-color: $bgColor; padding: 3px; word-break: break-all;'>$text</div></html>").apply {
|
||||
border = BorderFactory.createLineBorder(Color.BLACK)
|
||||
isOpaque = true
|
||||
background = TOOLTIP_BACKGROUND
|
||||
foreground = Color.WHITE
|
||||
font = _font
|
||||
maximumSize = Dimension(maxWidth, Int.MAX_VALUE)
|
||||
preferredSize = Dimension(maxWidth, requiredHeight)
|
||||
}
|
||||
pack()
|
||||
}
|
||||
} else {
|
||||
// Update the tooltip text
|
||||
val label = customToolTipWindow!!.contentPane as JLabel
|
||||
val bgColor = Helpers.colorToHex(TOOLTIP_BACKGROUND)
|
||||
val textColor = Helpers.colorToHex(secondaryColor)
|
||||
label.text = "<html><div style='color: $textColor; background-color: $bgColor; padding: 3px; word-break: break-all;'>$text</div></html>"
|
||||
label.preferredSize = Dimension(maxWidth, requiredHeight)
|
||||
customToolTipWindow!!.pack()
|
||||
}
|
||||
|
||||
// Position the tooltip near the component
|
||||
val locationOnScreen = component.locationOnScreen
|
||||
customToolTipWindow!!.setLocation(locationOnScreen.x, locationOnScreen.y + 15)
|
||||
customToolTipWindow!!.isVisible = true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,444 +0,0 @@
|
|||
package KondoKit
|
||||
|
||||
import KondoKit.Helpers.addMouseListenerToAll
|
||||
import KondoKit.Helpers.formatHtmlLabelText
|
||||
import KondoKit.Helpers.formatNumber
|
||||
import KondoKit.Helpers.getProgressBarColor
|
||||
import KondoKit.Helpers.getSpriteId
|
||||
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
|
||||
import KondoKit.plugin.Companion.IMAGE_SIZE
|
||||
import KondoKit.plugin.Companion.LVL_ICON
|
||||
import KondoKit.plugin.Companion.POPUP_BACKGROUND
|
||||
import KondoKit.plugin.Companion.POPUP_FOREGROUND
|
||||
import KondoKit.plugin.Companion.TOTAL_XP_WIDGET_SIZE
|
||||
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
|
||||
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||
import KondoKit.plugin.Companion.WIDGET_SIZE
|
||||
import KondoKit.plugin.Companion.playerXPMultiplier
|
||||
import KondoKit.plugin.Companion.primaryColor
|
||||
import KondoKit.plugin.Companion.secondaryColor
|
||||
import KondoKit.plugin.StateManager.focusedView
|
||||
import plugin.api.API
|
||||
import java.awt.*
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.charset.StandardCharsets
|
||||
import javax.swing.*
|
||||
|
||||
object XPTrackerView {
|
||||
private val COMBAT_SKILLS = intArrayOf(0,1,2,3,4)
|
||||
val xpWidgets: MutableMap<Int, XPWidget> = HashMap()
|
||||
var totalXPWidget: XPWidget? = null
|
||||
val initialXP: MutableMap<Int, Int> = HashMap()
|
||||
var xpTrackerView: JPanel? = null
|
||||
const val VIEW_NAME = "XP_TRACKER_VIEW"
|
||||
|
||||
|
||||
val npcHitpointsMap: Map<Int, Int> = try {
|
||||
BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("res/npc_hitpoints_map.json"), StandardCharsets.UTF_8))
|
||||
.useLines { lines ->
|
||||
val json = lines.joinToString("\n")
|
||||
val pairs = json.trim().removeSurrounding("{", "}").split(",")
|
||||
val map = mutableMapOf<Int, Int>()
|
||||
|
||||
for (pair in pairs) {
|
||||
val keyValue = pair.split(":")
|
||||
val id = keyValue[0].trim().trim('\"').toIntOrNull()
|
||||
val hitpoints = keyValue[1].trim()
|
||||
|
||||
if (id != null && hitpoints.isNotEmpty()) {
|
||||
map[id] = hitpoints.toIntOrNull() ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("XPTracker Error parsing NPC HP: ${e.message}")
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
fun updateWidget(xpWidget: XPWidget, xp: Int) {
|
||||
val (currentLevel, xpGainedSinceLastLevel) = XPTable.getLevelForXp(xp)
|
||||
|
||||
var xpGainedSinceLastUpdate = xp - xpWidget.previousXp
|
||||
xpWidget.totalXpGained += xpGainedSinceLastUpdate
|
||||
updateTotalXPWidget(xpGainedSinceLastUpdate)
|
||||
|
||||
val progress: Double
|
||||
if (currentLevel >= 99) {
|
||||
progress = 100.0 // Set progress to 100% if the level is 99 or above
|
||||
xpWidget.xpLeftLabel.text = "" // Hide XP Left when level is 99
|
||||
xpWidget.actionsRemainingLabel.text = ""
|
||||
} else {
|
||||
val nextLevelXp = XPTable.getXpRequiredForLevel(currentLevel + 1)
|
||||
val xpLeft = nextLevelXp - xp
|
||||
progress = xpGainedSinceLastLevel.toDouble() / (nextLevelXp - XPTable.getXpRequiredForLevel(currentLevel)) * 100
|
||||
val xpLeftstr = formatNumber(xpLeft)
|
||||
xpWidget.xpLeftLabel.text = formatHtmlLabelText("XP Left: ", primaryColor, xpLeftstr, secondaryColor)
|
||||
if(COMBAT_SKILLS.contains(xpWidget.skillId)) {
|
||||
if(LootTrackerView.lastConfirmedKillNpcId != -1 && npcHitpointsMap.isNotEmpty()) {
|
||||
val npcHP = npcHitpointsMap[LootTrackerView.lastConfirmedKillNpcId]
|
||||
val xpPerKill = when (xpWidget.skillId) {
|
||||
3 -> playerXPMultiplier * (npcHP ?: 1) // Hitpoints
|
||||
else -> playerXPMultiplier * (npcHP ?: 1) * 4 // Combat XP for other skills
|
||||
}
|
||||
val remainingKills = xpLeft / xpPerKill
|
||||
xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Kills: ", primaryColor, remainingKills.toString(), secondaryColor)
|
||||
}
|
||||
} else {
|
||||
if(xpGainedSinceLastUpdate == 0)
|
||||
xpGainedSinceLastUpdate = 1 // Avoid possible divide by 0
|
||||
val remainingActions = (xpLeft / xpGainedSinceLastUpdate).coerceAtLeast(1)
|
||||
xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Actions: ", primaryColor, remainingActions.toString(), secondaryColor)
|
||||
}
|
||||
}
|
||||
|
||||
val formattedXp = formatNumber(xpWidget.totalXpGained)
|
||||
xpWidget.xpGainedLabel.text = formatHtmlLabelText("XP Gained: ", primaryColor, formattedXp, secondaryColor)
|
||||
|
||||
// Update the progress bar with current level, progress, and next level
|
||||
xpWidget.progressBar.updateProgress(progress, currentLevel, if (currentLevel < 99) currentLevel + 1 else 99, focusedView == VIEW_NAME)
|
||||
|
||||
xpWidget.previousXp = xp
|
||||
if (focusedView == VIEW_NAME)
|
||||
xpWidget.container.repaint()
|
||||
}
|
||||
|
||||
|
||||
private fun updateTotalXPWidget(xpGainedSinceLastUpdate: Int) {
|
||||
val totalXPWidget = totalXPWidget ?: return
|
||||
totalXPWidget.totalXpGained += xpGainedSinceLastUpdate
|
||||
val formattedXp = formatNumber(totalXPWidget.totalXpGained)
|
||||
totalXPWidget.xpGainedLabel.text = formatHtmlLabelText("Gained: ", primaryColor, formattedXp, secondaryColor)
|
||||
if (focusedView == VIEW_NAME)
|
||||
totalXPWidget.container.repaint()
|
||||
}
|
||||
|
||||
|
||||
fun resetXPTracker(xpTrackerView : JPanel){
|
||||
|
||||
// Redo logic here
|
||||
xpTrackerView.removeAll()
|
||||
val popupMenu = createResetMenu()
|
||||
|
||||
// Create a custom MouseListener
|
||||
val rightClickListener = object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (e.isPopupTrigger) {
|
||||
popupMenu.show(e.component, e.x, e.y)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseReleased(e: MouseEvent) {
|
||||
if (e.isPopupTrigger) {
|
||||
popupMenu.show(e.component, e.x, e.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create the XP widget
|
||||
totalXPWidget = createTotalXPWidget()
|
||||
val wrapped = wrappedWidget(totalXPWidget!!.container)
|
||||
addMouseListenerToAll(wrapped,rightClickListener)
|
||||
wrapped.addMouseListener(rightClickListener)
|
||||
xpTrackerView.add(Box.createVerticalStrut(5))
|
||||
xpTrackerView.add(wrapped)
|
||||
xpTrackerView.add(Box.createVerticalStrut(5))
|
||||
|
||||
initialXP.clear()
|
||||
xpWidgets.clear()
|
||||
|
||||
xpTrackerView.revalidate()
|
||||
if (focusedView == VIEW_NAME)
|
||||
xpTrackerView.repaint()
|
||||
}
|
||||
|
||||
fun createTotalXPWidget(): XPWidget {
|
||||
val widgetPanel = Panel().apply {
|
||||
layout = BorderLayout(5, 5)
|
||||
background = WIDGET_COLOR
|
||||
preferredSize = TOTAL_XP_WIDGET_SIZE
|
||||
maximumSize = TOTAL_XP_WIDGET_SIZE
|
||||
minimumSize = TOTAL_XP_WIDGET_SIZE
|
||||
}
|
||||
|
||||
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(LVL_ICON))
|
||||
|
||||
val imageContainer = Panel(FlowLayout()).apply {
|
||||
preferredSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
|
||||
maximumSize = preferredSize
|
||||
minimumSize = preferredSize
|
||||
size = preferredSize
|
||||
}
|
||||
|
||||
bufferedImageSprite.let { image ->
|
||||
val imageCanvas = ImageCanvas(image).apply {
|
||||
preferredSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
|
||||
maximumSize = preferredSize
|
||||
minimumSize = preferredSize
|
||||
size = preferredSize
|
||||
}
|
||||
|
||||
imageContainer.add(imageCanvas)
|
||||
imageContainer.size = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
|
||||
|
||||
imageContainer.revalidate()
|
||||
if(focusedView == VIEW_NAME)
|
||||
imageContainer.repaint()
|
||||
}
|
||||
|
||||
val textPanel = Panel().apply {
|
||||
layout = GridLayout(2, 1, 5, 0)
|
||||
}
|
||||
|
||||
val font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||
|
||||
val xpGainedLabel = JLabel(
|
||||
formatHtmlLabelText("Gained: ", primaryColor, "0", secondaryColor)
|
||||
).apply {
|
||||
this.horizontalAlignment = JLabel.LEFT
|
||||
this.font = font
|
||||
}
|
||||
|
||||
val xpPerHourLabel = JLabel(
|
||||
formatHtmlLabelText("XP /hr: ", primaryColor, "0", secondaryColor)
|
||||
).apply {
|
||||
this.horizontalAlignment = JLabel.LEFT
|
||||
this.font = font
|
||||
}
|
||||
|
||||
textPanel.add(xpGainedLabel)
|
||||
textPanel.add(xpPerHourLabel)
|
||||
|
||||
widgetPanel.add(imageContainer, BorderLayout.WEST)
|
||||
widgetPanel.add(textPanel, BorderLayout.CENTER)
|
||||
|
||||
return XPWidget(
|
||||
skillId = -1,
|
||||
container = widgetPanel,
|
||||
xpGainedLabel = xpGainedLabel,
|
||||
xpLeftLabel = JLabel(formatHtmlLabelText("XP Left: ", primaryColor, "0", secondaryColor)).apply {
|
||||
this.horizontalAlignment = JLabel.LEFT
|
||||
this.font = font
|
||||
},
|
||||
xpPerHourLabel = xpPerHourLabel,
|
||||
progressBar = ProgressBar(0.0, Color.BLACK), // Unused
|
||||
totalXpGained = 0,
|
||||
startTime = System.currentTimeMillis(),
|
||||
previousXp = 0,
|
||||
actionsRemainingLabel = JLabel(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun createXPTrackerView(){
|
||||
val widgetViewPanel = JPanel().apply {
|
||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
||||
background = VIEW_BACKGROUND_COLOR
|
||||
}
|
||||
|
||||
val popupMenu = createResetMenu()
|
||||
|
||||
// Create a custom MouseListener
|
||||
val rightClickListener = object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (e.isPopupTrigger) {
|
||||
popupMenu.show(e.component, e.x, e.y)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseReleased(e: MouseEvent) {
|
||||
if (e.isPopupTrigger) {
|
||||
popupMenu.show(e.component, e.x, e.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create the XP widget
|
||||
totalXPWidget = createTotalXPWidget()
|
||||
val wrapped = wrappedWidget(totalXPWidget!!.container)
|
||||
addMouseListenerToAll(wrapped,rightClickListener)
|
||||
wrapped.addMouseListener(rightClickListener)
|
||||
widgetViewPanel.add(Box.createVerticalStrut(5))
|
||||
widgetViewPanel.add(wrapped)
|
||||
widgetViewPanel.add(Box.createVerticalStrut(5))
|
||||
|
||||
xpTrackerView = widgetViewPanel
|
||||
}
|
||||
|
||||
|
||||
fun createResetMenu(): JPopupMenu {
|
||||
// Create a popup menu
|
||||
val popupMenu = JPopupMenu()
|
||||
|
||||
val rFont = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||
|
||||
popupMenu.background = POPUP_BACKGROUND
|
||||
|
||||
// Create menu items with custom font and colors
|
||||
val menuItem1 = JMenuItem("Reset Tracker").apply {
|
||||
font = rFont // Set custom font
|
||||
background = POPUP_BACKGROUND // Dark background for item
|
||||
foreground = POPUP_FOREGROUND // Light text color for item
|
||||
}
|
||||
|
||||
// Add menu items to the popup menu
|
||||
popupMenu.add(menuItem1)
|
||||
|
||||
// Add action listeners to each menu item (optional)
|
||||
menuItem1.addActionListener { plugin.registerDrawAction { resetXPTracker(xpTrackerView!!) } }
|
||||
return popupMenu
|
||||
}
|
||||
|
||||
|
||||
fun createXPWidget(skillId: Int, previousXp: Int): XPWidget {
|
||||
val widgetPanel = Panel().apply {
|
||||
layout = BorderLayout(5, 5)
|
||||
background = WIDGET_COLOR
|
||||
preferredSize = WIDGET_SIZE
|
||||
maximumSize = WIDGET_SIZE
|
||||
minimumSize = WIDGET_SIZE
|
||||
}
|
||||
|
||||
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(getSpriteId(skillId)))
|
||||
val imageContainer = Panel(FlowLayout()).apply {
|
||||
background = WIDGET_COLOR
|
||||
preferredSize = IMAGE_SIZE
|
||||
maximumSize = IMAGE_SIZE
|
||||
minimumSize = IMAGE_SIZE
|
||||
size = IMAGE_SIZE
|
||||
}
|
||||
|
||||
bufferedImageSprite.let { image ->
|
||||
val imageCanvas = ImageCanvas(image).apply {
|
||||
background = WIDGET_COLOR
|
||||
preferredSize = Dimension(image.width, image.height)
|
||||
maximumSize = Dimension(image.width, image.height)
|
||||
minimumSize = Dimension(image.width, image.height)
|
||||
size = Dimension(image.width, image.height) // Explicitly set the size
|
||||
}
|
||||
|
||||
imageContainer.add(imageCanvas)
|
||||
imageContainer.size = Dimension(image.width, image.height) // Ensure container respects the image size
|
||||
|
||||
imageContainer.revalidate()
|
||||
if(focusedView == VIEW_NAME)
|
||||
imageContainer.repaint()
|
||||
}
|
||||
|
||||
val textPanel = Panel().apply {
|
||||
layout = GridLayout(2, 2, 5, 0)
|
||||
background = WIDGET_COLOR
|
||||
}
|
||||
|
||||
val font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||
|
||||
val xpGainedLabel = JLabel(
|
||||
formatHtmlLabelText("XP Gained: ", primaryColor, "0", secondaryColor)
|
||||
).apply {
|
||||
this.horizontalAlignment = JLabel.LEFT
|
||||
this.font = font
|
||||
}
|
||||
|
||||
val xpLeftLabel = JLabel(
|
||||
formatHtmlLabelText("XP Left: ", primaryColor, "0K", secondaryColor)
|
||||
).apply {
|
||||
this.horizontalAlignment = JLabel.LEFT
|
||||
this.font = font
|
||||
}
|
||||
|
||||
val xpPerHourLabel = JLabel(
|
||||
formatHtmlLabelText("XP /hr: ", primaryColor, "0", secondaryColor)
|
||||
).apply {
|
||||
this.horizontalAlignment = JLabel.LEFT
|
||||
this.font = font
|
||||
}
|
||||
|
||||
val killsLabel = JLabel(
|
||||
formatHtmlLabelText("Kills: ", primaryColor, "0", secondaryColor)
|
||||
).apply {
|
||||
this.horizontalAlignment = JLabel.LEFT
|
||||
this.font = font
|
||||
}
|
||||
|
||||
val levelPanel = Panel().apply {
|
||||
layout = BorderLayout(5, 0)
|
||||
background = WIDGET_COLOR
|
||||
}
|
||||
|
||||
val progressBarPanel = ProgressBar(0.0, getProgressBarColor(skillId)).apply {
|
||||
preferredSize = Dimension(160, 22)
|
||||
}
|
||||
|
||||
levelPanel.add(progressBarPanel, BorderLayout.CENTER)
|
||||
|
||||
textPanel.add(xpGainedLabel)
|
||||
textPanel.add(xpLeftLabel)
|
||||
textPanel.add(xpPerHourLabel)
|
||||
textPanel.add(killsLabel)
|
||||
|
||||
widgetPanel.add(imageContainer, BorderLayout.WEST)
|
||||
widgetPanel.add(textPanel, BorderLayout.CENTER)
|
||||
widgetPanel.add(levelPanel, BorderLayout.SOUTH)
|
||||
|
||||
widgetPanel.revalidate()
|
||||
if(focusedView == VIEW_NAME)
|
||||
widgetPanel.repaint()
|
||||
|
||||
return XPWidget(
|
||||
skillId = skillId,
|
||||
container = widgetPanel,
|
||||
xpGainedLabel = xpGainedLabel,
|
||||
xpLeftLabel = xpLeftLabel,
|
||||
xpPerHourLabel = xpPerHourLabel,
|
||||
progressBar = progressBarPanel,
|
||||
totalXpGained = 0,
|
||||
actionsRemainingLabel = killsLabel,
|
||||
startTime = System.currentTimeMillis(),
|
||||
previousXp = previousXp
|
||||
)
|
||||
}
|
||||
|
||||
fun wrappedWidget(component: Component, padding: Int = 7): Container {
|
||||
val outerPanelSize = Dimension(
|
||||
component.preferredSize.width + 2 * padding,
|
||||
component.preferredSize.height + 2 * padding
|
||||
)
|
||||
val outerPanel = JPanel(GridBagLayout()).apply {
|
||||
background = WIDGET_COLOR
|
||||
preferredSize = outerPanelSize
|
||||
maximumSize = outerPanelSize
|
||||
minimumSize = outerPanelSize
|
||||
}
|
||||
val innerPanel = JPanel(BorderLayout()).apply {
|
||||
background = WIDGET_COLOR
|
||||
preferredSize = component.preferredSize
|
||||
maximumSize = component.preferredSize
|
||||
minimumSize = component.preferredSize
|
||||
add(component, BorderLayout.CENTER)
|
||||
}
|
||||
val gbc = GridBagConstraints().apply {
|
||||
anchor = GridBagConstraints.CENTER
|
||||
}
|
||||
outerPanel.add(innerPanel, gbc)
|
||||
return outerPanel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
data class XPWidget(
|
||||
val container: Container,
|
||||
val skillId: Int,
|
||||
val xpGainedLabel: JLabel,
|
||||
val xpLeftLabel: JLabel,
|
||||
val xpPerHourLabel: JLabel,
|
||||
val actionsRemainingLabel: JLabel,
|
||||
val progressBar: ProgressBar,
|
||||
var totalXpGained: Int = 0,
|
||||
var startTime: Long = System.currentTimeMillis(),
|
||||
var previousXp: Int = 0
|
||||
)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package KondoKit.components
|
||||
|
||||
import KondoKit.plugin.Companion.TITLE_BAR_COLOR
|
||||
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||
import KondoKit.plugin.Companion.secondaryColor
|
||||
import java.awt.*
|
||||
import javax.swing.*
|
||||
|
||||
class ButtonPanel(
|
||||
private val alignment: Int = FlowLayout.CENTER,
|
||||
private val hgap: Int = 5,
|
||||
private val vgap: Int = 0
|
||||
) : JPanel() {
|
||||
|
||||
init {
|
||||
layout = FlowLayout(alignment, hgap, vgap)
|
||||
background = WIDGET_COLOR
|
||||
}
|
||||
|
||||
fun addButton(text: String, action: () -> Unit): JButton {
|
||||
val button = JButton(text).apply {
|
||||
background = TITLE_BAR_COLOR
|
||||
foreground = secondaryColor
|
||||
font = Font("RuneScape Small", Font.PLAIN, 14)
|
||||
addActionListener {
|
||||
action()
|
||||
}
|
||||
}
|
||||
add(button)
|
||||
return button
|
||||
}
|
||||
|
||||
fun addIcon(icon: Icon, action: () -> Unit): JButton {
|
||||
val button = JButton(icon).apply {
|
||||
background = TITLE_BAR_COLOR
|
||||
foreground = secondaryColor
|
||||
font = Font("RuneScape Small", Font.PLAIN, 14)
|
||||
addActionListener {
|
||||
action()
|
||||
}
|
||||
}
|
||||
add(button)
|
||||
return button
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package KondoKit.components
|
||||
|
||||
import KondoKit.ImageCanvas
|
||||
import KondoKit.SpriteToBufferedImage
|
||||
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||
import KondoKit.plugin.Companion.primaryColor
|
||||
import KondoKit.plugin.Companion.secondaryColor
|
||||
import plugin.api.API
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Color
|
||||
import java.awt.Dimension
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JPanel
|
||||
|
||||
class IconComponent(
|
||||
spriteId: Int,
|
||||
private val iconWidth: Int = 25,
|
||||
private val iconHeight: Int = 23,
|
||||
private val useThemeColors: Boolean = true,
|
||||
private val tint: Color? = null,
|
||||
private val grayscale: Boolean = false,
|
||||
private val brightnessBoost: Float = 1.0f
|
||||
) : JPanel() {
|
||||
|
||||
private val imageCanvas: ImageCanvas
|
||||
|
||||
init {
|
||||
layout = BorderLayout()
|
||||
background = if (useThemeColors) WIDGET_COLOR else Color.WHITE
|
||||
|
||||
val bufferedImageSprite = SpriteToBufferedImage.getBufferedImageFromSprite(
|
||||
API.GetSprite(spriteId),
|
||||
tint,
|
||||
grayscale,
|
||||
brightnessBoost
|
||||
)
|
||||
|
||||
imageCanvas = ImageCanvas(bufferedImageSprite).apply {
|
||||
val size = Dimension(iconWidth, iconHeight)
|
||||
preferredSize = size
|
||||
maximumSize = size
|
||||
minimumSize = size
|
||||
background = if (useThemeColors) WIDGET_COLOR else Color.WHITE
|
||||
}
|
||||
|
||||
add(imageCanvas, BorderLayout.CENTER)
|
||||
border = BorderFactory.createEmptyBorder(2, 2, 2, 2)
|
||||
}
|
||||
|
||||
fun updateFillColor(color: Color) {
|
||||
imageCanvas.fillColor = color
|
||||
background = color
|
||||
imageCanvas.repaint()
|
||||
repaint()
|
||||
}
|
||||
|
||||
fun applyThemeColors() {
|
||||
updateFillColor(WIDGET_COLOR)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package KondoKit.components
|
||||
|
||||
import KondoKit.Helpers.formatHtmlLabelText
|
||||
import KondoKit.plugin.Companion.primaryColor
|
||||
import KondoKit.plugin.Companion.secondaryColor
|
||||
import java.awt.Font
|
||||
import java.awt.Color
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
class LabelComponent(
|
||||
text: String = "",
|
||||
private val isHtml: Boolean = false,
|
||||
private val alignment: Int = SwingConstants.LEFT
|
||||
) : JLabel() {
|
||||
|
||||
init {
|
||||
this.text = text
|
||||
this.horizontalAlignment = alignment
|
||||
this.font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||
}
|
||||
|
||||
fun updateText(plainText: String, color: Color = secondaryColor) {
|
||||
this.text = plainText
|
||||
this.foreground = color
|
||||
}
|
||||
|
||||
fun updateHtmlText(text1: String, color1: Color = primaryColor, text2: String = "", color2: Color = secondaryColor) {
|
||||
this.text = formatHtmlLabelText(text1, color1, text2, color2)
|
||||
}
|
||||
|
||||
fun setAsHeader() {
|
||||
font = Font("RuneScape Small", Font.BOLD, 16)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package KondoKit.components
|
||||
|
||||
import KondoKit.plugin.Companion.POPUP_BACKGROUND
|
||||
import KondoKit.plugin.Companion.POPUP_FOREGROUND
|
||||
import java.awt.Font
|
||||
import javax.swing.JMenuItem
|
||||
import javax.swing.JPopupMenu
|
||||
|
||||
class PopupMenuComponent : JPopupMenu() {
|
||||
|
||||
init {
|
||||
background = POPUP_BACKGROUND
|
||||
}
|
||||
|
||||
fun addMenuItem(text: String, action: () -> Unit): JMenuItem {
|
||||
val menuItem = JMenuItem(text).apply {
|
||||
font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||
background = POPUP_BACKGROUND
|
||||
foreground = POPUP_FOREGROUND
|
||||
addActionListener {
|
||||
action()
|
||||
}
|
||||
}
|
||||
add(menuItem)
|
||||
return menuItem
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package KondoKit
|
||||
package KondoKit.components
|
||||
|
||||
import KondoKit.plugin.Companion.PROGRESS_BAR_FILL
|
||||
import KondoKit.plugin.Companion.secondaryColor
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
package KondoKit.components.ReflectiveEditorComponents
|
||||
|
||||
import KondoKit.ImageCanvas
|
||||
import KondoKit.plugin
|
||||
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||
import KondoKit.plugin.Companion.secondaryColor
|
||||
import plugin.api.API
|
||||
import java.awt.*
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.ActionListener
|
||||
import java.awt.event.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.image.BufferedImage
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.*
|
||||
|
||||
class CustomSearchField(private val parentPanel: JPanel, private val onSearch: (String) -> Unit) : Canvas() {
|
||||
private var cursorVisible: Boolean = true
|
||||
private var text: String = ""
|
||||
|
||||
private val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(1423)) // MAG_SPRITE
|
||||
private val imageCanvas = bufferedImageSprite.let {
|
||||
ImageCanvas(it).apply {
|
||||
preferredSize = Dimension(12, 12) // ICON_DIMENSION_SMALL
|
||||
size = preferredSize
|
||||
minimumSize = preferredSize
|
||||
maximumSize = preferredSize
|
||||
fillColor = WIDGET_COLOR
|
||||
}
|
||||
}
|
||||
|
||||
// Method to set the text programmatically
|
||||
fun setText(newText: String) {
|
||||
text = newText
|
||||
SwingUtilities.invokeLater {
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
|
||||
// Method to get the current text
|
||||
fun getText(): String {
|
||||
return text
|
||||
}
|
||||
|
||||
init {
|
||||
preferredSize = Dimension(230, 30) // SEARCH_FIELD_DIMENSION
|
||||
background = WIDGET_COLOR // COLOR_BACKGROUND_DARK
|
||||
foreground = secondaryColor // COLOR_FOREGROUND_LIGHT
|
||||
font = Font("Arial", Font.PLAIN, 14) // FONT_ARIAL_PLAIN_14
|
||||
minimumSize = preferredSize
|
||||
maximumSize = preferredSize
|
||||
|
||||
addKeyListener(object : KeyAdapter() {
|
||||
override fun keyTyped(e: KeyEvent) {
|
||||
// Prevent null character from being typed on Ctrl+A & Ctrl+V
|
||||
if (e.isControlDown && (e.keyChar == '\u0001' || e.keyChar == '\u0016')) {
|
||||
e.consume()
|
||||
return
|
||||
}
|
||||
if (e.keyChar == '\b') {
|
||||
if (text.isNotEmpty()) {
|
||||
text = text.dropLast(1)
|
||||
}
|
||||
} else if (e.keyChar == '\n') {
|
||||
onSearch(text)
|
||||
} else {
|
||||
text += e.keyChar
|
||||
}
|
||||
SwingUtilities.invokeLater {
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
if (e.isControlDown) {
|
||||
when (e.keyCode) {
|
||||
KeyEvent.VK_A -> {
|
||||
text = ""
|
||||
SwingUtilities.invokeLater {
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
KeyEvent.VK_V -> {
|
||||
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
|
||||
val pasteText = clipboard.getData(DataFlavor.stringFlavor) as String
|
||||
text += pasteText
|
||||
SwingUtilities.invokeLater {
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (e.x > width - 20 && e.y < 20) {
|
||||
text = ""
|
||||
onSearch(text)
|
||||
SwingUtilities.invokeLater {
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
javax.swing.Timer(500, ActionListener { _ ->
|
||||
cursorVisible = !cursorVisible
|
||||
if (plugin.StateManager.focusedView == "REFLECTIVE_EDITOR_VIEW")
|
||||
SwingUtilities.invokeLater {
|
||||
repaint()
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
override fun paint(g: Graphics) {
|
||||
super.paint(g)
|
||||
g.color = foreground
|
||||
g.font = font
|
||||
|
||||
val fm = g.fontMetrics
|
||||
val cursorX = fm.stringWidth(text) + 30
|
||||
|
||||
// Draw magnifying glass icon
|
||||
imageCanvas.let { canvas ->
|
||||
val imgG = g.create(5, 5, canvas.width, canvas.height)
|
||||
canvas.paint(imgG)
|
||||
imgG.dispose()
|
||||
}
|
||||
|
||||
// Use a local copy of the text to avoid threading issues
|
||||
val currentText = text
|
||||
|
||||
// Draw placeholder text if field is empty, otherwise draw actual text
|
||||
if (currentText == "") {
|
||||
g.color = Color.GRAY // Use a lighter color for placeholder text
|
||||
g.drawString("Search plugins...", 30, 20)
|
||||
} else {
|
||||
g.color = foreground // Use normal color for actual text
|
||||
g.drawString(currentText, 30, 20)
|
||||
}
|
||||
|
||||
if (cursorVisible && hasFocus()) {
|
||||
g.color = foreground
|
||||
g.drawLine(cursorX, 5, cursorX, 25)
|
||||
}
|
||||
|
||||
// Only draw the "x" button if there's text
|
||||
if (currentText != "") {
|
||||
g.color = Color.RED
|
||||
g.drawString("x", width - 20, 20)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBufferedImageFromSprite(sprite: Any?): BufferedImage {
|
||||
// This is a simplified version - you might need to adjust based on your actual implementation
|
||||
// For now, let's just return a placeholder image
|
||||
val image = BufferedImage(12, 12, BufferedImage.TYPE_INT_ARGB)
|
||||
val g2d = image.createGraphics()
|
||||
g2d.color = Color.GRAY
|
||||
g2d.fillOval(2, 2, 8, 8)
|
||||
g2d.drawLine(8, 8, 11, 11)
|
||||
g2d.dispose()
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package KondoKit.components.ReflectiveEditorComponents
|
||||
|
||||
data class GitLabPlugin(
|
||||
val id: String,
|
||||
val path: String,
|
||||
val pluginProperties: PluginProperties?,
|
||||
val pluginError: String?
|
||||
)
|
||||
|
||||
data class PluginProperties(
|
||||
val author: String,
|
||||
val version: String,
|
||||
val description: String
|
||||
)
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
package KondoKit.components.ReflectiveEditorComponents
|
||||
|
||||
import KondoKit.views.ReflectiveEditorView
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonObject
|
||||
import java.awt.EventQueue
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.concurrent.*
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
object GitLabPluginFetcher {
|
||||
private const val GITLAB_PROJECT_ID = "38297322"
|
||||
private const val GITLAB_BRANCH = "master"
|
||||
private const val CHROME_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||
private const val DEBUG = true // Set to false to disable debug logging
|
||||
|
||||
// Thread pool for concurrent plugin property fetching
|
||||
private val executorService = Executors.newFixedThreadPool(5)
|
||||
|
||||
// Debug logging function
|
||||
private fun debugLog(message: String) {
|
||||
if (DEBUG) {
|
||||
println("[GitLabPluginFetcher] $message")
|
||||
}
|
||||
}
|
||||
|
||||
// Function to fetch plugins from GitLab
|
||||
fun fetchGitLabPlugins(onComplete: (List<GitLabPlugin>) -> Unit) {
|
||||
Thread {
|
||||
try {
|
||||
debugLog("Starting to fetch GitLab plugins...")
|
||||
val plugins = mutableListOf<GitLabPlugin>()
|
||||
val apiUrl = "https://gitlab.com/api/v4/projects/${GITLAB_PROJECT_ID}/repository/tree?ref=${GITLAB_BRANCH}&per_page=100"
|
||||
debugLog("API URL: $apiUrl")
|
||||
|
||||
// Create URL connection
|
||||
val url = URL(apiUrl)
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.setRequestProperty("User-Agent", CHROME_USER_AGENT)
|
||||
debugLog("Set User-Agent header to: $CHROME_USER_AGENT")
|
||||
|
||||
// Read response
|
||||
val responseCode = connection.responseCode
|
||||
debugLog("Response code: $responseCode")
|
||||
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
val reader = BufferedReader(InputStreamReader(connection.inputStream))
|
||||
val response = reader.use { it.readText() }
|
||||
debugLog("Response length: ${response.length} characters")
|
||||
debugLog("Response preview: ${response.take(200)}...")
|
||||
|
||||
// Parse JSON response
|
||||
val gson = Gson()
|
||||
val treeItems = gson.fromJson(response, Array<JsonObject>::class.java)
|
||||
debugLog("Parsed ${treeItems.size} items from JSON")
|
||||
|
||||
// Filter for directories (trees) with mode "040000"
|
||||
val pluginDirectories = mutableListOf<Pair<String, String>>()
|
||||
for (jsonObject in treeItems) {
|
||||
if (jsonObject["type"].asString == "tree" && jsonObject["mode"].asString == "040000") {
|
||||
val folderId = jsonObject["id"].asString
|
||||
val folderPath = jsonObject["path"].asString
|
||||
pluginDirectories.add(folderId to folderPath)
|
||||
debugLog("Found directory: $folderPath (ID: $folderId)")
|
||||
}
|
||||
}
|
||||
debugLog("Found ${pluginDirectories.size} plugin directories")
|
||||
|
||||
// Fetch plugin properties in parallel
|
||||
val pluginFutures = mutableListOf<Future<GitLabPlugin>>()
|
||||
for ((folderId, folderPath) in pluginDirectories) {
|
||||
val future = executorService.submit(Callable<GitLabPlugin> {
|
||||
try {
|
||||
val pluginProperties = fetchPluginProperties(folderPath)
|
||||
debugLog("Successfully fetched properties for: $folderPath")
|
||||
GitLabPlugin(folderId, folderPath, pluginProperties, null)
|
||||
} catch (e: Exception) {
|
||||
debugLog("Error fetching plugin.properties for $folderPath: ${e.message}")
|
||||
GitLabPlugin(folderId, folderPath, null, "Error fetching plugin.properties")
|
||||
}
|
||||
})
|
||||
pluginFutures.add(future)
|
||||
}
|
||||
|
||||
// Collect results
|
||||
for (future in pluginFutures) {
|
||||
try {
|
||||
plugins.add(future.get())
|
||||
} catch (e: Exception) {
|
||||
debugLog("Error getting plugin result: ${e.message}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugLog("HTTP error: $responseCode")
|
||||
val errorReader = BufferedReader(InputStreamReader(connection.errorStream))
|
||||
val errorResponse = errorReader.use { it.readText() }
|
||||
debugLog("Error response: $errorResponse")
|
||||
}
|
||||
|
||||
debugLog("Completed fetching plugins. Total plugins: ${plugins.size}")
|
||||
// Update on UI thread
|
||||
SwingUtilities.invokeLater {
|
||||
onComplete(plugins)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
debugLog("Exception occurred: ${e.message}")
|
||||
e.printStackTrace()
|
||||
SwingUtilities.invokeLater {
|
||||
onComplete(emptyList())
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
// Function to fetch plugin.properties from a specific folder
|
||||
private fun fetchPluginProperties(folderPath: String): PluginProperties {
|
||||
val pluginFilePath = "$folderPath/plugin.properties"
|
||||
val pluginUrl = "https://gitlab.com/api/v4/projects/${GITLAB_PROJECT_ID}/repository/files/${pluginFilePath.replace("/", "%2F")}/raw?ref=${GITLAB_BRANCH}"
|
||||
debugLog("Fetching plugin.properties from: $pluginUrl")
|
||||
|
||||
// Create URL connection
|
||||
val url = URL(pluginUrl)
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.setRequestProperty("User-Agent", CHROME_USER_AGENT)
|
||||
|
||||
// Read response
|
||||
val responseCode = connection.responseCode
|
||||
debugLog("plugin.properties response code: $responseCode")
|
||||
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
val reader = BufferedReader(InputStreamReader(connection.inputStream))
|
||||
val rawContent = reader.use { it.readText() }
|
||||
debugLog("plugin.properties content length: ${rawContent.length} characters")
|
||||
debugLog("plugin.properties content preview: ${rawContent.take(200)}...")
|
||||
|
||||
return parseProperties(rawContent)
|
||||
} else {
|
||||
debugLog("Failed to fetch plugin.properties. Response code: $responseCode")
|
||||
val errorReader = BufferedReader(InputStreamReader(connection.errorStream))
|
||||
val errorResponse = errorReader.use { it.readText() }
|
||||
debugLog("Error response for plugin.properties: $errorResponse")
|
||||
throw Exception("Plugin file not found for folder: $folderPath")
|
||||
}
|
||||
}
|
||||
|
||||
// Function to parse plugin.properties content
|
||||
private fun parseProperties(content: String): PluginProperties {
|
||||
debugLog("Parsing plugin.properties content")
|
||||
val lines = content.split("\n")
|
||||
var author = "Unknown"
|
||||
var version = "Unknown"
|
||||
var description = "No description available"
|
||||
|
||||
for (line in lines) {
|
||||
val parts = line.split("=")
|
||||
if (parts.size == 2) {
|
||||
val key = parts[0].trim()
|
||||
val value = parts[1].replace("\"", "").replace("'", "").trim()
|
||||
|
||||
when {
|
||||
key.startsWith("AUTHOR", ignoreCase = true) -> {
|
||||
author = value
|
||||
debugLog("Found author: $value")
|
||||
}
|
||||
key.startsWith("VERSION", ignoreCase = true) -> {
|
||||
version = value
|
||||
debugLog("Found version: $value")
|
||||
}
|
||||
key.startsWith("DESCRIPTION", ignoreCase = true) -> {
|
||||
description = value
|
||||
debugLog("Found description: $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugLog("Parsed properties - Author: $author, Version: $version, Description: $description")
|
||||
return PluginProperties(author, version, description)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package KondoKit.components.ReflectiveEditorComponents
|
||||
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Simple test to verify GitLabPluginFetcher debug logging works
|
||||
*/
|
||||
fun main() {
|
||||
println("Testing GitLabPluginFetcher with debug logging...")
|
||||
|
||||
GitLabPluginFetcher.fetchGitLabPlugins { plugins ->
|
||||
println("Fetched ${plugins.size} plugins")
|
||||
plugins.forEach { plugin ->
|
||||
println("- ${plugin.path}: ${plugin.pluginProperties?.description ?: plugin.pluginError}")
|
||||
}
|
||||
|
||||
// Shutdown the executor service
|
||||
try {
|
||||
val field = GitLabPluginFetcher::class.java.getDeclaredField("executorService")
|
||||
field.isAccessible = true
|
||||
val executorService = field.get(GitLabPluginFetcher) as ExecutorService
|
||||
executorService.shutdown()
|
||||
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
executorService.shutdownNow()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
// Give the async fetch some time to complete
|
||||
Thread.sleep(10000)
|
||||
println("Test completed")
|
||||
}
|
||||
|
|
@ -0,0 +1,413 @@
|
|||
package KondoKit.components.ReflectiveEditorComponents
|
||||
|
||||
import KondoKit.views.ReflectiveEditorView
|
||||
import plugin.PluginRepository
|
||||
import java.awt.EventQueue
|
||||
import java.io.*
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.concurrent.*
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
/**
|
||||
* Manages downloading and installing plugins from GitLab with concurrent support
|
||||
*/
|
||||
object PluginDownloadManager {
|
||||
private const val GITLAB_PROJECT_PATH = "2009scape/tools/client-plugins"
|
||||
private const val GITLAB_BRANCH = "master"
|
||||
private const val CHROME_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||
private const val MAX_CONCURRENT_DOWNLOADS = 3
|
||||
private const val DEBUG = true // Set to false to disable debug logging
|
||||
|
||||
// Thread pool for concurrent downloads
|
||||
private val downloadExecutor = Executors.newFixedThreadPool(MAX_CONCURRENT_DOWNLOADS)
|
||||
|
||||
// Callback for download progress updates
|
||||
interface DownloadProgressCallback {
|
||||
fun onProgress(pluginName: String, progress: Int)
|
||||
fun onComplete(pluginName: String, success: Boolean, errorMessage: String? = null)
|
||||
}
|
||||
|
||||
// Debug logging function
|
||||
private fun debugLog(message: String) {
|
||||
if (DEBUG) {
|
||||
println("[PluginDownloadManager] $message")
|
||||
}
|
||||
}
|
||||
|
||||
// Test URL method for debugging
|
||||
fun testDownloadUrl(plugin: GitLabPlugin): String {
|
||||
return "https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip?path=${plugin.path}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a single plugin
|
||||
*/
|
||||
fun downloadPlugin(plugin: GitLabPlugin, callback: (String, Boolean, String?) -> Unit) {
|
||||
downloadExecutor.submit {
|
||||
try {
|
||||
debugLog("Starting download for plugin: ${plugin.path}")
|
||||
|
||||
// Download the plugin as a ZIP archive
|
||||
val success = downloadAndExtractPlugin(plugin, object : DownloadProgressCallback {
|
||||
override fun onProgress(pluginName: String, progress: Int) {
|
||||
// We don't need to do anything here for the simple callback
|
||||
}
|
||||
|
||||
override fun onComplete(pluginName: String, success: Boolean, errorMessage: String?) {
|
||||
callback(pluginName, success, errorMessage)
|
||||
}
|
||||
})
|
||||
|
||||
if (success) {
|
||||
debugLog("Successfully downloaded and extracted plugin: ${plugin.path}")
|
||||
SwingUtilities.invokeLater {
|
||||
callback(plugin.path, true, null)
|
||||
}
|
||||
} else {
|
||||
debugLog("Failed to download plugin: ${plugin.path}")
|
||||
SwingUtilities.invokeLater {
|
||||
callback(plugin.path, false, "Failed to download plugin")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
debugLog("Exception during download of ${plugin.path}: ${e.message}")
|
||||
e.printStackTrace()
|
||||
SwingUtilities.invokeLater {
|
||||
callback(plugin.path, false, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a single plugin with progress updates
|
||||
*/
|
||||
fun downloadPlugin(plugin: GitLabPlugin, callback: DownloadProgressCallback) {
|
||||
downloadExecutor.submit {
|
||||
try {
|
||||
debugLog("Starting download for plugin: ${plugin.path}")
|
||||
callback.onProgress(plugin.path, 0)
|
||||
|
||||
// Download the plugin as a ZIP archive
|
||||
val success = downloadAndExtractPlugin(plugin, callback)
|
||||
|
||||
if (success) {
|
||||
debugLog("Successfully downloaded and extracted plugin: ${plugin.path}")
|
||||
SwingUtilities.invokeLater {
|
||||
callback.onComplete(plugin.path, true)
|
||||
}
|
||||
} else {
|
||||
debugLog("Failed to download plugin: ${plugin.path}")
|
||||
SwingUtilities.invokeLater {
|
||||
callback.onComplete(plugin.path, false, "Failed to download plugin")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
debugLog("Exception during download of ${plugin.path}: ${e.message}")
|
||||
e.printStackTrace()
|
||||
SwingUtilities.invokeLater {
|
||||
callback.onComplete(plugin.path, false, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download multiple plugins concurrently
|
||||
*/
|
||||
fun downloadPlugins(plugins: List<GitLabPlugin>, callback: (String, Boolean, String?) -> Unit) {
|
||||
debugLog("Starting concurrent download of ${plugins.size} plugins")
|
||||
|
||||
// Submit all downloads to the executor
|
||||
for (plugin in plugins) {
|
||||
downloadPlugin(plugin, object : DownloadProgressCallback {
|
||||
override fun onProgress(pluginName: String, progress: Int) {
|
||||
// We don't need to do anything here for the simple callback
|
||||
}
|
||||
|
||||
override fun onComplete(pluginName: String, success: Boolean, errorMessage: String?) {
|
||||
callback(pluginName, success, errorMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and extract a plugin from GitLab
|
||||
*/
|
||||
private fun downloadAndExtractPlugin(plugin: GitLabPlugin, callback: DownloadProgressCallback): Boolean {
|
||||
try {
|
||||
// Validate plugin path
|
||||
if (plugin.path.isBlank()) {
|
||||
debugLog("Plugin path is blank for plugin: ${plugin.path}")
|
||||
return false
|
||||
}
|
||||
|
||||
debugLog("Starting download for plugin: ${plugin.path}")
|
||||
|
||||
// Try multiple URL formats since GitLab has changed their API
|
||||
val downloadUrls = listOf(
|
||||
// Format 1: Using ref_type parameter
|
||||
"https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip?path=${plugin.path}&ref_type=heads",
|
||||
// Format 2: Using ref parameter
|
||||
"https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip?path=${plugin.path}",
|
||||
// Format 3: Direct archive URL without path parameter (we'll filter later)
|
||||
"https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip"
|
||||
)
|
||||
|
||||
for (downloadUrl in downloadUrls) {
|
||||
debugLog("Trying download URL: $downloadUrl")
|
||||
|
||||
// Validate URL
|
||||
val url = try {
|
||||
URL(downloadUrl)
|
||||
} catch (e: Exception) {
|
||||
debugLog("Invalid URL: $downloadUrl for plugin: ${plugin.path}. Error: ${e.message}")
|
||||
continue
|
||||
}
|
||||
|
||||
// Create URL connection
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.setRequestProperty("User-Agent", CHROME_USER_AGENT)
|
||||
// Add Accept header to avoid 406 errors
|
||||
connection.setRequestProperty("Accept", "*/*")
|
||||
|
||||
debugLog("Connection opened for plugin: ${plugin.path}")
|
||||
debugLog("Request headers - User-Agent: ${connection.getRequestProperty("User-Agent")}")
|
||||
debugLog("Request headers - Accept: ${connection.getRequestProperty("Accept")}")
|
||||
|
||||
// Get content length for progress tracking
|
||||
val contentLength = connection.contentLength
|
||||
debugLog("Content length: $contentLength bytes for plugin: ${plugin.path}")
|
||||
|
||||
// Read response
|
||||
val responseCode = connection.responseCode
|
||||
debugLog("Response code: $responseCode for plugin: ${plugin.path}")
|
||||
|
||||
// Log response message
|
||||
val responseMessage = connection.responseMessage
|
||||
debugLog("Response message: $responseMessage for plugin: ${plugin.path}")
|
||||
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
// Check if input stream is available
|
||||
val inputStream = connection.inputStream
|
||||
if (inputStream == null) {
|
||||
debugLog("Input stream is null for plugin: ${plugin.path}")
|
||||
continue
|
||||
}
|
||||
|
||||
debugLog("Input stream available for plugin: ${plugin.path}")
|
||||
|
||||
// Create output directory
|
||||
val pluginsDir = ReflectiveEditorView.pluginsDirectory
|
||||
debugLog("Plugins directory: ${pluginsDir.absolutePath}")
|
||||
|
||||
// Validate plugins directory
|
||||
if (!pluginsDir.exists()) {
|
||||
debugLog("Plugins directory does not exist: ${pluginsDir.absolutePath}")
|
||||
if (!pluginsDir.mkdirs()) {
|
||||
debugLog("Failed to create plugins directory: ${pluginsDir.absolutePath}")
|
||||
return false
|
||||
}
|
||||
debugLog("Created plugins directory: ${pluginsDir.absolutePath}")
|
||||
}
|
||||
|
||||
if (!pluginsDir.isDirectory) {
|
||||
debugLog("Plugins path is not a directory: ${pluginsDir.absolutePath}")
|
||||
return false
|
||||
}
|
||||
|
||||
val pluginDir = File(pluginsDir, plugin.path)
|
||||
debugLog("Plugin directory: ${pluginDir.absolutePath}")
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (!pluginDir.exists()) {
|
||||
pluginDir.mkdirs()
|
||||
debugLog("Created plugin directory: ${pluginDir.absolutePath}")
|
||||
}
|
||||
|
||||
// Download the ZIP file
|
||||
val tempZipFile = File.createTempFile("plugin_", ".zip")
|
||||
tempZipFile.deleteOnExit()
|
||||
debugLog("Created temp file: ${tempZipFile.absolutePath}")
|
||||
|
||||
var totalBytesRead = 0L
|
||||
inputStream.use { input ->
|
||||
FileOutputStream(tempZipFile).use { outputStream ->
|
||||
val buffer = ByteArray(8192)
|
||||
var bytesRead: Int
|
||||
|
||||
debugLog("Starting to read data for plugin: ${plugin.path}")
|
||||
|
||||
while (input.read(buffer).also { bytesRead = it } != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead)
|
||||
totalBytesRead += bytesRead
|
||||
|
||||
// Report progress if content length is known
|
||||
if (contentLength > 0) {
|
||||
val progress = (totalBytesRead * 100 / contentLength).toInt()
|
||||
// Limit progress updates to avoid flooding the UI
|
||||
if (progress % 5 == 0) {
|
||||
SwingUtilities.invokeLater {
|
||||
callback.onProgress(plugin.path, progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugLog("Finished reading data for plugin: ${plugin.path}. Total bytes: $totalBytesRead")
|
||||
}
|
||||
}
|
||||
|
||||
debugLog("Downloaded ${totalBytesRead} bytes to ${tempZipFile.absolutePath}")
|
||||
|
||||
// Extract the ZIP file
|
||||
if (extractZipFile(tempZipFile, pluginDir, plugin.path)) {
|
||||
debugLog("Successfully extracted plugin to ${pluginDir.absolutePath}")
|
||||
// Clean up temp file
|
||||
tempZipFile.delete()
|
||||
return true
|
||||
} else {
|
||||
debugLog("Failed to extract plugin")
|
||||
tempZipFile.delete()
|
||||
continue // Try next URL format
|
||||
}
|
||||
} else {
|
||||
debugLog("HTTP error: $responseCode for plugin: ${plugin.path} with URL: $downloadUrl")
|
||||
|
||||
// Try to read error stream for more details
|
||||
try {
|
||||
val errorStream = connection.errorStream
|
||||
if (errorStream != null) {
|
||||
val errorReader = BufferedReader(InputStreamReader(errorStream))
|
||||
val errorResponse = errorReader.use { it.readText() }
|
||||
debugLog("Error response: $errorResponse for plugin: ${plugin.path}")
|
||||
} else {
|
||||
debugLog("Error stream is null for plugin: ${plugin.path}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
debugLog("Exception while reading error stream: ${e.message} for plugin: ${plugin.path}")
|
||||
}
|
||||
|
||||
continue // Try next URL format
|
||||
}
|
||||
}
|
||||
|
||||
debugLog("All URL formats failed for plugin: ${plugin.path}")
|
||||
return false
|
||||
} catch (e: Exception) {
|
||||
debugLog("Exception during download and extraction for plugin ${plugin.path}: ${e.message}")
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a ZIP file to the specified directory
|
||||
*/
|
||||
private fun extractZipFile(zipFile: File, targetDir: File, pluginPath: String): Boolean {
|
||||
try {
|
||||
debugLog("Extracting ZIP file: ${zipFile.absolutePath} to ${targetDir.absolutePath}")
|
||||
|
||||
// Validate inputs
|
||||
if (!zipFile.exists()) {
|
||||
debugLog("ZIP file does not exist: ${zipFile.absolutePath}")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!targetDir.exists() && !targetDir.mkdirs()) {
|
||||
debugLog("Failed to create target directory: ${targetDir.absolutePath}")
|
||||
return false
|
||||
}
|
||||
|
||||
ZipInputStream(FileInputStream(zipFile)).use { zis ->
|
||||
var entry: ZipEntry?
|
||||
var entryCount = 0
|
||||
|
||||
while (zis.nextEntry.also { entry = it } != null) {
|
||||
entryCount++
|
||||
val entryName = entry!!.name
|
||||
|
||||
debugLog("Processing ZIP entry $entryCount: $entryName")
|
||||
|
||||
// Skip the top-level directory in the ZIP (GitLab adds a project-branch-hash directory)
|
||||
// We want to extract the contents of the plugin directory directly
|
||||
val relativePath = if (entryName.contains("/")) {
|
||||
entryName.substring(entryName.indexOf("/") + 1)
|
||||
} else {
|
||||
entryName
|
||||
}
|
||||
|
||||
debugLog("Relative path for entry: $relativePath")
|
||||
|
||||
// Only extract files that are part of this specific plugin
|
||||
if (relativePath.startsWith(pluginPath) && relativePath != pluginPath) {
|
||||
val fileName = relativePath.substring(pluginPath.length + 1) // +1 for the trailing slash
|
||||
|
||||
debugLog("File name to extract: $fileName")
|
||||
|
||||
if (fileName.isNotEmpty()) {
|
||||
val file = File(targetDir, fileName)
|
||||
|
||||
debugLog("Target file path: ${file.absolutePath}")
|
||||
|
||||
// Create parent directories if needed
|
||||
val parent = file.parentFile
|
||||
if (parent != null && !parent.exists()) {
|
||||
if (parent.mkdirs()) {
|
||||
debugLog("Created parent directories: ${parent.absolutePath}")
|
||||
} else {
|
||||
debugLog("Failed to create parent directories: ${parent.absolutePath}")
|
||||
zis.closeEntry()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Extract file or directory
|
||||
if (entry!!.isDirectory) {
|
||||
if (!file.exists()) {
|
||||
if (file.mkdirs()) {
|
||||
debugLog("Created directory: ${file.absolutePath}")
|
||||
} else {
|
||||
debugLog("Failed to create directory: ${file.absolutePath}")
|
||||
}
|
||||
} else {
|
||||
debugLog("Directory already exists: ${file.absolutePath}")
|
||||
}
|
||||
} else {
|
||||
// Create file
|
||||
try {
|
||||
FileOutputStream(file).use { fos ->
|
||||
zis.copyTo(fos)
|
||||
}
|
||||
debugLog("Extracted file: ${file.absolutePath}")
|
||||
} catch (e: Exception) {
|
||||
debugLog("Failed to extract file ${file.absolutePath}: ${e.message}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugLog("Skipping empty file name")
|
||||
}
|
||||
} else {
|
||||
debugLog("Skipping entry (not part of plugin): $relativePath")
|
||||
}
|
||||
|
||||
zis.closeEntry()
|
||||
}
|
||||
|
||||
debugLog("Processed $entryCount entries from ZIP file")
|
||||
}
|
||||
|
||||
debugLog("Successfully extracted ZIP file")
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
debugLog("Exception during ZIP extraction: ${e.message}")
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package KondoKit.components.ReflectiveEditorComponents
|
||||
|
||||
/**
|
||||
* Data class representing a plugin with its installation status and update information
|
||||
*/
|
||||
data class PluginStatus(
|
||||
val name: String,
|
||||
val installedVersion: String?,
|
||||
val remoteVersion: String?,
|
||||
val description: String?,
|
||||
val author: String?,
|
||||
val gitLabPlugin: GitLabPlugin?,
|
||||
val isInstalled: Boolean,
|
||||
val needsUpdate: Boolean,
|
||||
val isDownloading: Boolean = false,
|
||||
val downloadProgress: Int = 0
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package KondoKit
|
||||
package KondoKit.components
|
||||
|
||||
import KondoKit.plugin.Companion.SCROLL_BAR_COLOR
|
||||
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
package KondoKit.components
|
||||
|
||||
import KondoKit.ImageCanvas
|
||||
import KondoKit.SpriteToBufferedImage
|
||||
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||
import KondoKit.plugin.Companion.secondaryColor
|
||||
import KondoKit.plugin.StateManager.focusedView
|
||||
import plugin.api.API
|
||||
import java.awt.*
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.event.*
|
||||
import javax.swing.*
|
||||
|
||||
open class SearchField(
|
||||
protected val onSearch: (String) -> Unit,
|
||||
private val placeholderText: String = "Search...",
|
||||
private val fieldWidth: Int = 230,
|
||||
private val fieldHeight: Int = 30,
|
||||
private val viewName: String? = null
|
||||
) : Canvas() {
|
||||
|
||||
private var cursorVisible: Boolean = true
|
||||
private var text: String = ""
|
||||
|
||||
private val bufferedImageSprite = SpriteToBufferedImage.getBufferedImageFromSprite(API.GetSprite(1423)) // MAG_SPRITE
|
||||
private val imageCanvas = bufferedImageSprite.let {
|
||||
ImageCanvas(it).apply {
|
||||
preferredSize = Dimension(12, 12)
|
||||
size = preferredSize
|
||||
minimumSize = preferredSize
|
||||
maximumSize = preferredSize
|
||||
fillColor = WIDGET_COLOR
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val dimension = Dimension(fieldWidth, fieldHeight)
|
||||
preferredSize = dimension
|
||||
background = WIDGET_COLOR
|
||||
foreground = secondaryColor
|
||||
font = Font("Arial", Font.PLAIN, 14)
|
||||
minimumSize = dimension
|
||||
maximumSize = dimension
|
||||
|
||||
addKeyListener(object : KeyAdapter() {
|
||||
override fun keyTyped(e: KeyEvent) {
|
||||
// Prevent null character from being typed on Ctrl+A & Ctrl+V
|
||||
if (e.isControlDown && (e.keyChar == '\u0001' || e.keyChar == '\u0016')) {
|
||||
e.consume()
|
||||
return
|
||||
}
|
||||
if (e.keyChar == '\b') {
|
||||
if (text.isNotEmpty()) {
|
||||
text = text.dropLast(1)
|
||||
}
|
||||
} else if (e.keyChar == '\n') {
|
||||
triggerSearch()
|
||||
} else {
|
||||
text += e.keyChar
|
||||
}
|
||||
SwingUtilities.invokeLater {
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
if (e.isControlDown) {
|
||||
when (e.keyCode) {
|
||||
KeyEvent.VK_A -> {
|
||||
text = ""
|
||||
SwingUtilities.invokeLater {
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
KeyEvent.VK_V -> {
|
||||
try {
|
||||
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
|
||||
val pasteText = clipboard.getData(DataFlavor.stringFlavor) as String
|
||||
text += pasteText
|
||||
SwingUtilities.invokeLater {
|
||||
repaint()
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
// Ignore clipboard errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (e.x > width - 20 && e.y < 20) {
|
||||
text = ""
|
||||
SwingUtilities.invokeLater {
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Timer(500) { _ ->
|
||||
cursorVisible = !cursorVisible
|
||||
// Only repaint if the view is active or if viewName is not specified
|
||||
if (viewName == null || focusedView == viewName) {
|
||||
SwingUtilities.invokeLater {
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun paint(g: Graphics) {
|
||||
super.paint(g)
|
||||
g.color = foreground
|
||||
g.font = font
|
||||
|
||||
val fm = g.fontMetrics
|
||||
val cursorX = fm.stringWidth(text) + 30
|
||||
|
||||
// Draw magnifying glass icon
|
||||
imageCanvas.let { canvas ->
|
||||
val imgG = g.create(5, 5, canvas.width, canvas.height)
|
||||
canvas.paint(imgG)
|
||||
imgG.dispose()
|
||||
}
|
||||
|
||||
// Use a local copy of the text to avoid threading issues
|
||||
val currentText = text
|
||||
|
||||
// Draw placeholder text if field is empty, otherwise draw actual text
|
||||
if (currentText.isEmpty()) {
|
||||
g.color = Color.GRAY // Use a lighter color for placeholder text
|
||||
g.drawString(placeholderText, 30, 20)
|
||||
} else {
|
||||
g.color = foreground // Use normal color for actual text
|
||||
g.drawString(currentText, 30, 20)
|
||||
}
|
||||
|
||||
if (cursorVisible && hasFocus()) {
|
||||
g.color = foreground
|
||||
g.drawLine(cursorX, 5, cursorX, 25)
|
||||
}
|
||||
|
||||
// Only draw the "x" button if there's text
|
||||
if (currentText.isNotEmpty()) {
|
||||
g.color = Color.RED
|
||||
g.drawString("x", width - 20, 20)
|
||||
}
|
||||
}
|
||||
|
||||
fun setText(newText: String) {
|
||||
text = newText
|
||||
repaint()
|
||||
}
|
||||
|
||||
fun getText(): String = text
|
||||
|
||||
private fun triggerSearch() {
|
||||
val query = text.trim()
|
||||
if (query.isNotEmpty()) {
|
||||
text = query
|
||||
repaint()
|
||||
onSearch(query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,503 @@
|
|||
package KondoKit.components
|
||||
|
||||
import KondoKit.Helpers
|
||||
import KondoKit.Helpers.FieldNotifier
|
||||
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||
import KondoKit.plugin.Companion.primaryColor
|
||||
import KondoKit.plugin.Companion.secondaryColor
|
||||
import plugin.Plugin
|
||||
import java.awt.*
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.*
|
||||
import java.util.Timer
|
||||
import javax.swing.*
|
||||
import javax.swing.table.DefaultTableModel
|
||||
|
||||
class SettingsPanel(private val plugin: Plugin) : JPanel() {
|
||||
|
||||
init {
|
||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
||||
background = WIDGET_COLOR
|
||||
border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
|
||||
}
|
||||
|
||||
fun addSettingsFromPlugin() {
|
||||
val fieldNotifier = FieldNotifier(plugin)
|
||||
val exposedFields = plugin.javaClass.declaredFields.filter { field ->
|
||||
field.annotations.any { annotation ->
|
||||
annotation.annotationClass.simpleName == "Exposed"
|
||||
}
|
||||
}
|
||||
|
||||
if (exposedFields.isNotEmpty()) {
|
||||
for (field in exposedFields) {
|
||||
field.isAccessible = true
|
||||
|
||||
// Get the "Exposed" annotation specifically and retrieve its description, if available
|
||||
val exposedAnnotation = field.annotations.firstOrNull { annotation ->
|
||||
annotation.annotationClass.simpleName == "Exposed"
|
||||
}
|
||||
|
||||
val description = exposedAnnotation?.let { annotation ->
|
||||
try {
|
||||
val descriptionField = annotation.annotationClass.java.getMethod("description")
|
||||
descriptionField.invoke(annotation) as String
|
||||
} catch (e: NoSuchMethodException) {
|
||||
"" // No description method, return empty string
|
||||
}
|
||||
} ?: ""
|
||||
|
||||
val fieldPanel = JPanel().apply {
|
||||
layout = GridBagLayout()
|
||||
background = WIDGET_COLOR
|
||||
foreground = secondaryColor
|
||||
border = BorderFactory.createEmptyBorder(5, 0, 5, 0)
|
||||
maximumSize = Dimension(Int.MAX_VALUE, 50)
|
||||
}
|
||||
|
||||
val gbc = GridBagConstraints().apply {
|
||||
insets = Insets(0, 5, 0, 5)
|
||||
}
|
||||
|
||||
val label = JLabel(field.name.capitalize()).apply {
|
||||
foreground = secondaryColor
|
||||
font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||
}
|
||||
gbc.gridx = 0
|
||||
gbc.gridy = 0
|
||||
gbc.weightx = 0.0
|
||||
gbc.anchor = GridBagConstraints.WEST
|
||||
fieldPanel.add(label, gbc)
|
||||
|
||||
// Create appropriate input component based on field type
|
||||
val inputComponent: JComponent
|
||||
val isHashMapEditor: Boolean
|
||||
when {
|
||||
field.type == Boolean::class.javaPrimitiveType || field.type == java.lang.Boolean::class.java -> {
|
||||
inputComponent = JCheckBox().apply {
|
||||
isSelected = field.get(plugin) as Boolean
|
||||
}
|
||||
isHashMapEditor = false
|
||||
}
|
||||
|
||||
field.type.isEnum -> {
|
||||
val enumConstants = field.type.enumConstants
|
||||
inputComponent = JComboBox(enumConstants as Array<out Enum<*>>).apply {
|
||||
selectedItem = field.get(plugin)
|
||||
}
|
||||
isHashMapEditor = false
|
||||
}
|
||||
|
||||
field.type == Int::class.javaPrimitiveType || field.type == Integer::class.java -> {
|
||||
inputComponent = JSpinner(SpinnerNumberModel(field.get(plugin) as Int, Int.MIN_VALUE, Int.MAX_VALUE, 1))
|
||||
isHashMapEditor = false
|
||||
}
|
||||
|
||||
field.type == Float::class.javaPrimitiveType ||
|
||||
field.type == Double::class.javaPrimitiveType ||
|
||||
field.type == java.lang.Float::class.java ||
|
||||
field.type == java.lang.Double::class.java -> {
|
||||
inputComponent = JSpinner(SpinnerNumberModel((field.get(plugin) as Number).toDouble(), -Double.MAX_VALUE, Double.MAX_VALUE, 0.1))
|
||||
isHashMapEditor = false
|
||||
}
|
||||
|
||||
// Check if the field is a HashMap
|
||||
field.type == HashMap::class.java || field.type.simpleName == "HashMap" -> {
|
||||
inputComponent = createHashMapEditor(field, plugin, fieldNotifier)
|
||||
isHashMapEditor = true
|
||||
}
|
||||
|
||||
else -> {
|
||||
inputComponent = JTextField(field.get(plugin)?.toString() ?: "")
|
||||
isHashMapEditor = false
|
||||
}
|
||||
}
|
||||
|
||||
// Handle HashMap editors differently - they don't use the fieldPanel layout
|
||||
if (isHashMapEditor) {
|
||||
// Add the HashMap editor directly to the settings panel
|
||||
add(inputComponent)
|
||||
} else {
|
||||
// Add mouse listener to the label only if a description is available
|
||||
if (description.isNotBlank()) {
|
||||
label.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
label.addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
showCustomToolTip(description, label)
|
||||
}
|
||||
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
customToolTipWindow?.isVisible = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
gbc.gridx = 1
|
||||
gbc.gridy = 0
|
||||
gbc.weightx = 1.0
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL
|
||||
fieldPanel.add(inputComponent, gbc)
|
||||
|
||||
// Add apply button for non-HashMap editors
|
||||
val applyButton = JButton("\u2714").apply {
|
||||
maximumSize = Dimension(Int.MAX_VALUE, 8)
|
||||
}
|
||||
gbc.gridx = 2
|
||||
gbc.gridy = 0
|
||||
gbc.weightx = 0.0
|
||||
gbc.fill = GridBagConstraints.NONE
|
||||
applyButton.addActionListener {
|
||||
try {
|
||||
val newValue = when (inputComponent) {
|
||||
is JCheckBox -> inputComponent.isSelected
|
||||
is JComboBox<*> -> inputComponent.selectedItem
|
||||
is JSpinner -> inputComponent.value
|
||||
is JTextField -> Helpers.convertValue(field.type, field.genericType, inputComponent.text)
|
||||
else -> throw IllegalArgumentException("Unsupported input component type")
|
||||
}
|
||||
fieldNotifier.setFieldValue(field, newValue)
|
||||
Helpers.showToast(
|
||||
this@SettingsPanel,
|
||||
"${field.name} updated successfully!"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Helpers.showToast(
|
||||
this@SettingsPanel,
|
||||
"Failed to update ${field.name}: ${e.message}",
|
||||
JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fieldPanel.add(applyButton, gbc)
|
||||
add(fieldPanel)
|
||||
}
|
||||
|
||||
// Track field changes in real-time and update UI (skip HashMap fields)
|
||||
if (field.type != HashMap::class.java && field.type.simpleName != "HashMap") {
|
||||
var previousValue = field.get(plugin)?.toString()
|
||||
val timer = Timer()
|
||||
timer.schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
val currentValue = field.get(plugin)?.toString()
|
||||
if (currentValue != previousValue) {
|
||||
previousValue = currentValue
|
||||
SwingUtilities.invokeLater {
|
||||
// Update the inputComponent based on the new value
|
||||
when (inputComponent) {
|
||||
is JCheckBox -> inputComponent.isSelected = field.get(plugin) as Boolean
|
||||
is JComboBox<*> -> inputComponent.selectedItem = field.get(plugin)
|
||||
is JSpinner -> inputComponent.value = field.get(plugin)
|
||||
is JTextField -> inputComponent.text = field.get(plugin)?.toString() ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0, 1000) // Poll every 1000 milliseconds (1 second)
|
||||
}
|
||||
}
|
||||
|
||||
if (exposedFields.isNotEmpty()) {
|
||||
add(Box.createVerticalStrut(5))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createHashMapEditor(field: java.lang.reflect.Field, plugin: Plugin, fieldNotifier: FieldNotifier): JComponent {
|
||||
// Create a panel to hold the table and buttons
|
||||
val editorPanel = JPanel()
|
||||
editorPanel.layout = BoxLayout(editorPanel, BoxLayout.Y_AXIS)
|
||||
editorPanel.background = WIDGET_COLOR
|
||||
editorPanel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0)
|
||||
editorPanel.maximumSize = Dimension(Int.MAX_VALUE, 250)
|
||||
|
||||
// Get the "Exposed" annotation specifically and retrieve its description, if available
|
||||
val exposedAnnotation = field.annotations.firstOrNull { annotation ->
|
||||
annotation.annotationClass.simpleName == "Exposed"
|
||||
}
|
||||
|
||||
val description = exposedAnnotation?.let { annotation ->
|
||||
try {
|
||||
val descriptionField = annotation.annotationClass.java.getMethod("description")
|
||||
descriptionField.invoke(annotation) as String
|
||||
} catch (e: NoSuchMethodException) {
|
||||
"" // No description method, return empty string
|
||||
}
|
||||
} ?: ""
|
||||
|
||||
// Create title label
|
||||
val titleLabel = JLabel("${field.name} (Key-Value Pairs)").apply {
|
||||
foreground = secondaryColor
|
||||
font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||
alignmentX = Component.LEFT_ALIGNMENT
|
||||
|
||||
// Add mouse listener to the label only if a description is available
|
||||
if (description.isNotBlank()) {
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseEntered(e: MouseEvent) {
|
||||
showCustomToolTip(description, this@apply)
|
||||
}
|
||||
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
SettingsPanel.customToolTipWindow?.isVisible = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Get the current HashMap value
|
||||
val hashMap = field.get(plugin) as? HashMap<*, *> ?: HashMap<Any, Any>()
|
||||
|
||||
// Create a table model for the HashMap
|
||||
val tableModel = object : DefaultTableModel() {
|
||||
init {
|
||||
val columnVector = java.util.Vector<String>()
|
||||
columnVector.add("Key")
|
||||
columnVector.add("Value")
|
||||
columnIdentifiers = columnVector
|
||||
hashMap.forEach { (key, value) ->
|
||||
val vector = java.util.Vector<Any>()
|
||||
vector.add(key.toString())
|
||||
vector.add(value.toString())
|
||||
addRow(vector)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isCellEditable(row: Int, column: Int): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getColumnClass(columnIndex: Int): Class<*> {
|
||||
return String::class.java
|
||||
}
|
||||
}
|
||||
|
||||
// Create the table
|
||||
val table = JTable(tableModel).apply {
|
||||
background = WIDGET_COLOR
|
||||
foreground = secondaryColor
|
||||
gridColor = primaryColor
|
||||
tableHeader.background = WIDGET_COLOR
|
||||
tableHeader.foreground = secondaryColor
|
||||
preferredScrollableViewportSize = Dimension(Int.MAX_VALUE, 150)
|
||||
setFillsViewportHeight(true)
|
||||
}
|
||||
|
||||
// Create a scroll pane for the table with fixed size
|
||||
val scrollPane = JScrollPane(table).apply {
|
||||
preferredSize = Dimension(Int.MAX_VALUE, 150)
|
||||
maximumSize = preferredSize
|
||||
minimumSize = preferredSize
|
||||
background = WIDGET_COLOR
|
||||
verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED
|
||||
horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
|
||||
alignmentX = Component.LEFT_ALIGNMENT
|
||||
}
|
||||
|
||||
// Create buttons panel
|
||||
val buttonsPanel = JPanel(FlowLayout(FlowLayout.LEFT, 5, 5)).apply {
|
||||
background = WIDGET_COLOR
|
||||
alignmentX = Component.LEFT_ALIGNMENT
|
||||
}
|
||||
|
||||
// Add button
|
||||
val addButton = JButton("+").apply {
|
||||
maximumSize = Dimension(40, 30)
|
||||
addActionListener {
|
||||
val vector = java.util.Vector<Any>()
|
||||
vector.add("")
|
||||
vector.add("")
|
||||
tableModel.addRow(vector)
|
||||
// Select the new row for editing
|
||||
val newRow = tableModel.rowCount - 1
|
||||
table.setRowSelectionInterval(newRow, newRow)
|
||||
table.editCellAt(newRow, 0)
|
||||
table.editorComponent?.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
// Remove button
|
||||
val removeButton = JButton("-").apply {
|
||||
maximumSize = Dimension(40, 30)
|
||||
addActionListener {
|
||||
val selectedRow = table.selectedRow
|
||||
if (selectedRow >= 0) {
|
||||
tableModel.removeRow(selectedRow)
|
||||
} else {
|
||||
Helpers.showToast(
|
||||
this@SettingsPanel,
|
||||
"Please select a row to remove",
|
||||
JOptionPane.WARNING_MESSAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply button
|
||||
val applyButton = JButton("Apply Changes").apply {
|
||||
maximumSize = Dimension(120, 30)
|
||||
addActionListener {
|
||||
try {
|
||||
// Commit any active cell editing before reading values
|
||||
if (table.isEditing) {
|
||||
table.cellEditor.stopCellEditing()
|
||||
}
|
||||
|
||||
// Get the current HashMap from the field and modify it in place
|
||||
val currentHashMap = field.get(plugin) as? HashMap<Any, Any> ?: HashMap<Any, Any>()
|
||||
|
||||
// Clear the current HashMap
|
||||
currentHashMap.clear()
|
||||
|
||||
// Add the new entries from the table to the existing HashMap
|
||||
for (i in 0 until tableModel.rowCount) {
|
||||
val key = tableModel.getValueAt(i, 0).toString()
|
||||
val value = tableModel.getValueAt(i, 1).toString()
|
||||
// Only add non-empty keys
|
||||
if (key.isNotBlank()) {
|
||||
// Skip empty values
|
||||
if (value.isBlank()) {
|
||||
Helpers.showToast(
|
||||
this@SettingsPanel,
|
||||
"Skipping entry with empty value for key '$key'",
|
||||
JOptionPane.WARNING_MESSAGE
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to convert the value to the appropriate type based on the field's generic type
|
||||
val convertedValue = try {
|
||||
// For HashMap<String, Int> - treat all HashMap<String, Int> fields the same way regardless of plugin
|
||||
val fieldTypeName = field.genericType.toString()
|
||||
if (fieldTypeName.contains("java.util.HashMap<java.lang.String, java.lang.Integer>") ||
|
||||
fieldTypeName.contains("HashMap<String, Int>") ||
|
||||
fieldTypeName.contains("HashMap<String, Integer>")) {
|
||||
try {
|
||||
val intValue = value.toInt()
|
||||
intValue
|
||||
} catch (e: NumberFormatException) {
|
||||
Helpers.showToast(
|
||||
this@SettingsPanel,
|
||||
"Invalid number format for key '$key': '$value'. Using 0 as default.",
|
||||
JOptionPane.WARNING_MESSAGE
|
||||
)
|
||||
0
|
||||
}
|
||||
}
|
||||
// For other numeric types
|
||||
else if (fieldTypeName.contains("java.lang.Integer") ||
|
||||
fieldTypeName.contains("int")) {
|
||||
value.toInt()
|
||||
}
|
||||
else if (fieldTypeName.contains("java.lang.Double") ||
|
||||
fieldTypeName.contains("double")) {
|
||||
value.toDouble()
|
||||
}
|
||||
else if (fieldTypeName.contains("java.lang.Float") ||
|
||||
fieldTypeName.contains("float")) {
|
||||
value.toFloat()
|
||||
}
|
||||
else if (fieldTypeName.contains("java.lang.Boolean") ||
|
||||
fieldTypeName.contains("boolean")) {
|
||||
value.toBoolean()
|
||||
}
|
||||
// Default to string for other types
|
||||
else {
|
||||
value
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// If conversion fails, keep as string
|
||||
value
|
||||
}
|
||||
currentHashMap[key] = convertedValue
|
||||
}
|
||||
}
|
||||
|
||||
// Update the field to trigger notifications (even though the reference is the same)
|
||||
// This ensures OnKondoValueUpdated() gets called if it exists
|
||||
fieldNotifier.setFieldValue(field, currentHashMap)
|
||||
Helpers.showToast(
|
||||
this@SettingsPanel,
|
||||
"${field.name} updated successfully!"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Helpers.showToast(
|
||||
this@SettingsPanel,
|
||||
"Failed to update ${field.name}: ${e.message}",
|
||||
JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buttonsPanel.add(addButton)
|
||||
buttonsPanel.add(removeButton)
|
||||
buttonsPanel.add(Box.createHorizontalStrut(20))
|
||||
buttonsPanel.add(applyButton)
|
||||
|
||||
// Add components to the editor panel in vertical stack
|
||||
editorPanel.add(titleLabel)
|
||||
editorPanel.add(Box.createVerticalStrut(5))
|
||||
editorPanel.add(scrollPane)
|
||||
editorPanel.add(Box.createVerticalStrut(5))
|
||||
editorPanel.add(buttonsPanel)
|
||||
|
||||
return editorPanel
|
||||
}
|
||||
|
||||
companion object {
|
||||
var customToolTipWindow: JWindow? = null
|
||||
|
||||
fun showCustomToolTip(text: String, component: JComponent) {
|
||||
val _font = Font("RuneScape Small", Font.PLAIN, 16)
|
||||
val maxWidth = 150
|
||||
val lineHeight = 16
|
||||
|
||||
// Create a dummy JLabel to get FontMetrics for the font used in the tooltip
|
||||
val dummyLabel = JLabel()
|
||||
dummyLabel.font = _font
|
||||
val fontMetrics = dummyLabel.getFontMetrics(_font)
|
||||
|
||||
// Calculate the approximate width of the text
|
||||
val textWidth = fontMetrics.stringWidth(text)
|
||||
|
||||
// Calculate the number of lines required based on the text width and max tooltip width
|
||||
val numberOfLines = Math.ceil(textWidth.toDouble() / maxWidth).toInt()
|
||||
|
||||
// Calculate the required height of the tooltip
|
||||
val requiredHeight = numberOfLines * lineHeight + 6 // Adding some padding
|
||||
|
||||
if (customToolTipWindow == null) {
|
||||
customToolTipWindow = JWindow().apply {
|
||||
val bgColor = Helpers.colorToHex(KondoKit.plugin.Companion.TOOLTIP_BACKGROUND)
|
||||
val textColor = Helpers.colorToHex(KondoKit.plugin.Companion.secondaryColor)
|
||||
contentPane = JLabel("<html><div style='color: $textColor; background-color: $bgColor; padding: 3px; word-break: break-all;'>$text</div></html>").apply {
|
||||
border = BorderFactory.createLineBorder(Color.BLACK)
|
||||
isOpaque = true
|
||||
background = KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
|
||||
foreground = Color.WHITE
|
||||
font = _font
|
||||
maximumSize = Dimension(maxWidth, Int.MAX_VALUE)
|
||||
preferredSize = Dimension(maxWidth, requiredHeight)
|
||||
}
|
||||
pack()
|
||||
}
|
||||
} else {
|
||||
// Update the tooltip text
|
||||
val label = customToolTipWindow!!.contentPane as JLabel
|
||||
val bgColor = Helpers.colorToHex(KondoKit.plugin.Companion.TOOLTIP_BACKGROUND)
|
||||
val textColor = Helpers.colorToHex(KondoKit.plugin.Companion.secondaryColor)
|
||||
label.text = "<html><div style='color: $textColor; background-color: $bgColor; padding: 3px; word-break: break-all;'>$text</div></html>"
|
||||
label.preferredSize = Dimension(maxWidth, requiredHeight)
|
||||
customToolTipWindow!!.pack()
|
||||
}
|
||||
|
||||
// Position the tooltip near the component
|
||||
val locationOnScreen = component.locationOnScreen
|
||||
customToolTipWindow!!.setLocation(locationOnScreen.x, locationOnScreen.y + 15)
|
||||
customToolTipWindow!!.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
package KondoKit.components
|
||||
|
||||
import java.awt.*
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.image.BufferedImage
|
||||
import javax.swing.JPanel
|
||||
|
||||
class ToggleSwitch : JPanel() {
|
||||
private var activated = false
|
||||
private var switchColor = Color(200, 200, 200)
|
||||
private var buttonColor = Color(255, 255, 255)
|
||||
private var borderColor = Color(50, 50, 50)
|
||||
private var activeSwitch = Color(0, 125, 255)
|
||||
private var puffer: BufferedImage? = null
|
||||
private var g: Graphics2D? = null
|
||||
|
||||
var onToggleListener: ((Boolean) -> Unit)? = null
|
||||
|
||||
init {
|
||||
isVisible = true
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseReleased(arg0: MouseEvent) {
|
||||
activated = !activated
|
||||
onToggleListener?.invoke(activated)
|
||||
repaint()
|
||||
}
|
||||
})
|
||||
cursor = Cursor(Cursor.HAND_CURSOR)
|
||||
preferredSize = Dimension(32, 20)
|
||||
maximumSize = Dimension(32, 20)
|
||||
minimumSize = Dimension(32, 20)
|
||||
isOpaque = false // Make the panel background transparent
|
||||
}
|
||||
|
||||
override fun paint(gr: Graphics) {
|
||||
if (g == null || puffer?.width != width || puffer?.height != height) {
|
||||
puffer = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
|
||||
g = puffer?.createGraphics()
|
||||
val rh = RenderingHints(
|
||||
RenderingHints.KEY_ANTIALIASING,
|
||||
RenderingHints.VALUE_ANTIALIAS_ON
|
||||
)
|
||||
g?.setRenderingHints(rh)
|
||||
}
|
||||
|
||||
// Clear the buffer with transparent background
|
||||
g?.color = Color(0, 0, 0, 0)
|
||||
g?.fillRect(0, 0, width, height)
|
||||
|
||||
// Draw the track with circular ends (rounded rectangle)
|
||||
val trackHeight = height - 2
|
||||
val trackWidth = width - 2
|
||||
val trackArc = trackHeight // Makes it fully rounded at the ends
|
||||
|
||||
g?.color = if (activated) activeSwitch else switchColor
|
||||
g?.fillRoundRect(1, 1, trackWidth, trackHeight, trackArc, trackArc)
|
||||
g?.color = borderColor
|
||||
g?.drawRoundRect(1, 1, trackWidth, trackHeight, trackArc, trackArc)
|
||||
|
||||
// Draw the thumb (circular button)
|
||||
val thumbDiameter = trackHeight - 4
|
||||
val thumbX = if (activated) {
|
||||
width - thumbDiameter - 3 // Right side when activated
|
||||
} else {
|
||||
3 // Left side when not activated
|
||||
}
|
||||
val thumbY = (height - thumbDiameter) / 2
|
||||
|
||||
g?.color = buttonColor
|
||||
g?.fillOval(thumbX, thumbY, thumbDiameter, thumbDiameter)
|
||||
g?.color = borderColor
|
||||
g?.drawOval(thumbX, thumbY, thumbDiameter, thumbDiameter)
|
||||
|
||||
gr.drawImage(puffer, 0, 0, null)
|
||||
}
|
||||
|
||||
fun isActivated(): Boolean {
|
||||
return activated
|
||||
}
|
||||
|
||||
fun setActivated(activated: Boolean) {
|
||||
this.activated = activated
|
||||
repaint()
|
||||
}
|
||||
|
||||
fun getSwitchColor(): Color {
|
||||
return switchColor
|
||||
}
|
||||
|
||||
/**
|
||||
* Unactivated Background Color of switch
|
||||
*/
|
||||
fun setSwitchColor(switchColor: Color) {
|
||||
this.switchColor = switchColor
|
||||
}
|
||||
|
||||
fun getButtonColor(): Color {
|
||||
return buttonColor
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch-Button color
|
||||
*/
|
||||
fun setButtonColor(buttonColor: Color) {
|
||||
this.buttonColor = buttonColor
|
||||
}
|
||||
|
||||
fun getBorderColor(): Color {
|
||||
return borderColor
|
||||
}
|
||||
|
||||
/**
|
||||
* Border-color of whole switch and switch-button
|
||||
*/
|
||||
fun setBorderColor(borderColor: Color) {
|
||||
this.borderColor = borderColor
|
||||
}
|
||||
|
||||
fun getActiveSwitch(): Color {
|
||||
return activeSwitch
|
||||
}
|
||||
|
||||
fun setActiveSwitch(activeSwitch: Color) {
|
||||
this.activeSwitch = activeSwitch
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package KondoKit.components
|
||||
|
||||
import KondoKit.plugin.Companion.TITLE_BAR_COLOR
|
||||
import KondoKit.plugin.Companion.secondaryColor
|
||||
import java.awt.*
|
||||
import javax.swing.*
|
||||
|
||||
class ViewHeader(
|
||||
title: String,
|
||||
private val headerHeight: Int = 40
|
||||
) : JPanel() {
|
||||
|
||||
private val titleLabel = JLabel(title)
|
||||
|
||||
init {
|
||||
background = TITLE_BAR_COLOR
|
||||
preferredSize = Dimension(Int.MAX_VALUE, headerHeight)
|
||||
border = BorderFactory.createEmptyBorder(5, 10, 5, 10)
|
||||
layout = BorderLayout()
|
||||
|
||||
titleLabel.foreground = secondaryColor
|
||||
titleLabel.font = Font("RuneScape Small", Font.PLAIN, 16)
|
||||
titleLabel.horizontalAlignment = SwingConstants.CENTER
|
||||
|
||||
add(titleLabel, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
fun setTitle(title: String) {
|
||||
titleLabel.text = title
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package KondoKit.components
|
||||
|
||||
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.JPanel
|
||||
|
||||
class WidgetPanel(
|
||||
private val widgetWidth: Int = 220,
|
||||
private val widgetHeight: Int = 50,
|
||||
private val addDefaultPadding: Boolean = true
|
||||
) : JPanel() {
|
||||
|
||||
init {
|
||||
layout = BorderLayout(5, 5)
|
||||
background = WIDGET_COLOR
|
||||
|
||||
val size = Dimension(widgetWidth, widgetHeight)
|
||||
preferredSize = size
|
||||
maximumSize = size
|
||||
minimumSize = size
|
||||
|
||||
if (addDefaultPadding) {
|
||||
border = BorderFactory.createEmptyBorder(5, 5, 5, 5)
|
||||
}
|
||||
}
|
||||
|
||||
fun setFixedSize(width: Int, height: Int) {
|
||||
val size = Dimension(width, height)
|
||||
preferredSize = size
|
||||
maximumSize = size
|
||||
minimumSize = size
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +1,18 @@
|
|||
package KondoKit
|
||||
|
||||
import KondoKit.Constants.COMBAT_LVL_SPRITE
|
||||
import KondoKit.Helpers.formatHtmlLabelText
|
||||
import KondoKit.Helpers.formatNumber
|
||||
import KondoKit.Helpers.getSpriteId
|
||||
import KondoKit.Helpers.showAlert
|
||||
import KondoKit.HiscoresView.createHiscoreSearchView
|
||||
import KondoKit.HiscoresView.hiScoreView
|
||||
import KondoKit.LootTrackerView.BAG_ICON
|
||||
import KondoKit.LootTrackerView.createLootTrackerView
|
||||
import KondoKit.LootTrackerView.lootTrackerView
|
||||
import KondoKit.LootTrackerView.npcDeathSnapshots
|
||||
import KondoKit.LootTrackerView.onPostClientTick
|
||||
import KondoKit.LootTrackerView.takeGroundSnapshot
|
||||
import KondoKit.ReflectiveEditorView.addPlugins
|
||||
import KondoKit.ReflectiveEditorView.createReflectiveEditorView
|
||||
import KondoKit.ReflectiveEditorView.reflectiveEditorView
|
||||
import KondoKit.views.*
|
||||
import KondoKit.views.OnUpdateCallback
|
||||
import KondoKit.views.OnDrawCallback
|
||||
import KondoKit.views.OnXPUpdateCallback
|
||||
import KondoKit.views.OnKillingBlowNPCCallback
|
||||
import KondoKit.views.OnPostClientTickCallback
|
||||
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
|
||||
import KondoKit.Themes.Theme
|
||||
import KondoKit.Themes.ThemeType
|
||||
import KondoKit.Themes.getTheme
|
||||
import KondoKit.XPTrackerView.createXPTrackerView
|
||||
import KondoKit.XPTrackerView.createXPWidget
|
||||
import KondoKit.XPTrackerView.initialXP
|
||||
import KondoKit.XPTrackerView.resetXPTracker
|
||||
import KondoKit.XPTrackerView.totalXPWidget
|
||||
import KondoKit.XPTrackerView.updateWidget
|
||||
import KondoKit.XPTrackerView.wrappedWidget
|
||||
import KondoKit.XPTrackerView.xpTrackerView
|
||||
import KondoKit.XPTrackerView.xpWidgets
|
||||
import KondoKit.plugin.StateManager.focusedView
|
||||
import KondoKit.components.ScrollablePanel
|
||||
import plugin.Plugin
|
||||
import plugin.api.*
|
||||
import plugin.api.API.*
|
||||
|
|
@ -98,9 +81,9 @@ class plugin : Plugin() {
|
|||
const val FIXED_HEIGHT = 503
|
||||
private const val NAVBAR_WIDTH = 30
|
||||
private const val MAIN_CONTENT_WIDTH = 242
|
||||
private const val WRENCH_ICON = 907
|
||||
private const val LOOT_ICON = 777
|
||||
private const val MAG_SPRITE = 1423
|
||||
const val WRENCH_ICON = 907
|
||||
const val LOOT_ICON = 777
|
||||
const val MAG_SPRITE = 1423
|
||||
const val LVL_ICON = 898
|
||||
private lateinit var cardLayout: CardLayout
|
||||
private lateinit var mainContentPanel: JPanel
|
||||
|
|
@ -118,12 +101,38 @@ class plugin : Plugin() {
|
|||
private const val HIDDEN_VIEW = "HIDDEN"
|
||||
private var altCanvas: AltCanvas? = null
|
||||
private val drawActions = mutableListOf<() -> Unit>()
|
||||
private val views = mutableListOf<View>()
|
||||
private val updateCallbacks = mutableListOf<OnUpdateCallback>()
|
||||
private val drawCallbacks = mutableListOf<OnDrawCallback>()
|
||||
private val xpUpdateCallbacks = mutableListOf<OnXPUpdateCallback>()
|
||||
private val killingBlowNPCCallbacks = mutableListOf<OnKillingBlowNPCCallback>()
|
||||
private val postClientTickCallbacks = mutableListOf<OnPostClientTickCallback>()
|
||||
|
||||
fun registerDrawAction(action: () -> Unit) {
|
||||
synchronized(drawActions) {
|
||||
drawActions.add(action)
|
||||
}
|
||||
}
|
||||
|
||||
fun registerUpdateCallback(callback: OnUpdateCallback) {
|
||||
updateCallbacks.add(callback)
|
||||
}
|
||||
|
||||
fun registerDrawCallback(callback: OnDrawCallback) {
|
||||
drawCallbacks.add(callback)
|
||||
}
|
||||
|
||||
fun registerXPUpdateCallback(callback: OnXPUpdateCallback) {
|
||||
xpUpdateCallbacks.add(callback)
|
||||
}
|
||||
|
||||
fun registerKillingBlowNPCCallback(callback: OnKillingBlowNPCCallback) {
|
||||
killingBlowNPCCallbacks.add(callback)
|
||||
}
|
||||
|
||||
fun registerPostClientTickCallback(callback: OnPostClientTickCallback) {
|
||||
postClientTickCallbacks.add(callback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun Init() {
|
||||
|
|
@ -137,7 +146,7 @@ class plugin : Plugin() {
|
|||
if (lastLogin != "" && lastLogin != Player.usernameInput.toString()) {
|
||||
// if we logged in with a new character
|
||||
// we need to reset the trackers
|
||||
xpTrackerView?.let { resetXPTracker(it) }
|
||||
XPTrackerView.xpTrackerView?.let { XPTrackerView.resetXPTracker(it) }
|
||||
}
|
||||
lastLogin = Player.usernameInput.toString()
|
||||
}
|
||||
|
|
@ -164,41 +173,28 @@ class plugin : Plugin() {
|
|||
|
||||
override fun OnPluginsReloaded(): Boolean {
|
||||
if (!initialized) return true
|
||||
updateDisplaySettings()
|
||||
frame.remove(rightPanelWrapper)
|
||||
frame.layout = BorderLayout()
|
||||
rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) }
|
||||
frame.revalidate()
|
||||
// Ensure Swing updates happen on the EDT to avoid flicker
|
||||
SwingUtilities.invokeLater {
|
||||
updateDisplaySettings()
|
||||
frame.remove(rightPanelWrapper)
|
||||
frame.layout = BorderLayout()
|
||||
rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) }
|
||||
frame.revalidate()
|
||||
frame.repaint()
|
||||
|
||||
// Rebuild the reflective editor UI on the EDT and in one batch
|
||||
ReflectiveEditorView.addPlugins(ReflectiveEditorView.panel)
|
||||
}
|
||||
pluginsReloaded = true
|
||||
reloadInterfaces = true
|
||||
return true
|
||||
}
|
||||
|
||||
override fun OnXPUpdate(skillId: Int, xp: Int) {
|
||||
if (!initialXP.containsKey(skillId)) {
|
||||
initialXP[skillId] = xp
|
||||
return
|
||||
}
|
||||
var xpWidget = xpWidgets[skillId]
|
||||
if (xpWidget != null) {
|
||||
updateWidget(xpWidget, xp)
|
||||
} else {
|
||||
val previousXp = initialXP[skillId] ?: xp
|
||||
if (xp == initialXP[skillId]) return
|
||||
|
||||
xpWidget = createXPWidget(skillId, previousXp)
|
||||
xpWidgets[skillId] = xpWidget
|
||||
|
||||
xpTrackerView?.add(wrappedWidget(xpWidget.container))
|
||||
xpTrackerView?.add(Box.createVerticalStrut(5))
|
||||
|
||||
if(focusedView == XPTrackerView.VIEW_NAME) {
|
||||
xpTrackerView?.revalidate()
|
||||
xpTrackerView?.repaint()
|
||||
}
|
||||
|
||||
updateWidget(xpWidget, xp)
|
||||
}
|
||||
// Call registered XP update callbacks
|
||||
xpUpdateCallbacks.forEach { callback ->
|
||||
callback.onXPUpdate(skillId, xp)
|
||||
}
|
||||
}
|
||||
|
||||
override fun Draw(timeDelta: Long) {
|
||||
|
|
@ -208,7 +204,10 @@ class plugin : Plugin() {
|
|||
}
|
||||
|
||||
if (pluginsReloaded) {
|
||||
reflectiveEditorView?.let { addPlugins(it) }
|
||||
// Rebuild the reflective editor UI on the EDT and in one batch
|
||||
SwingUtilities.invokeLater {
|
||||
ReflectiveEditorView.addPlugins(ReflectiveEditorView.panel)
|
||||
}
|
||||
pluginsReloaded = false
|
||||
}
|
||||
|
||||
|
|
@ -219,10 +218,18 @@ class plugin : Plugin() {
|
|||
|
||||
accumulatedTime += timeDelta
|
||||
if (accumulatedTime >= TICK_INTERVAL) {
|
||||
lootTrackerView?.let { onPostClientTick(it) }
|
||||
// Call registered post client tick callbacks
|
||||
postClientTickCallbacks.forEach { callback ->
|
||||
callback.onPostClientTick()
|
||||
}
|
||||
accumulatedTime = 0L
|
||||
}
|
||||
|
||||
// Call registered draw callbacks
|
||||
drawCallbacks.forEach { callback ->
|
||||
callback.onDraw(timeDelta)
|
||||
}
|
||||
|
||||
// Draw synced actions (that require to be done between glBegin and glEnd)
|
||||
if (drawActions.isNotEmpty()) {
|
||||
synchronized(drawActions) {
|
||||
|
|
@ -260,32 +267,17 @@ class plugin : Plugin() {
|
|||
}
|
||||
|
||||
override fun Update() {
|
||||
|
||||
val widgets = xpWidgets.values
|
||||
val totalXP = totalXPWidget
|
||||
|
||||
widgets.forEach { xpWidget ->
|
||||
val elapsedTime = (System.currentTimeMillis() - xpWidget.startTime) / 1000.0 / 60.0 / 60.0
|
||||
val xpPerHour = if (elapsedTime > 0) (xpWidget.totalXpGained / elapsedTime).toInt() else 0
|
||||
val formattedXpPerHour = formatNumber(xpPerHour)
|
||||
xpWidget.xpPerHourLabel.text =
|
||||
formatHtmlLabelText("XP /hr: ", primaryColor, formattedXpPerHour, secondaryColor)
|
||||
xpWidget.container.repaint()
|
||||
}
|
||||
|
||||
totalXP?.let { totalXPWidget ->
|
||||
val elapsedTime = (System.currentTimeMillis() - totalXPWidget.startTime) / 1000.0 / 60.0 / 60.0
|
||||
val totalXPPerHour = if (elapsedTime > 0) (totalXPWidget.totalXpGained / elapsedTime).toInt() else 0
|
||||
val formattedTotalXpPerHour = formatNumber(totalXPPerHour)
|
||||
totalXPWidget.xpPerHourLabel.text =
|
||||
formatHtmlLabelText("XP /hr: ", primaryColor, formattedTotalXpPerHour, secondaryColor)
|
||||
totalXPWidget.container.repaint()
|
||||
// Call registered update callbacks
|
||||
updateCallbacks.forEach { callback ->
|
||||
callback.onUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun OnKillingBlowNPC(npcID: Int, x: Int, z: Int) {
|
||||
val preDeathSnapshot = takeGroundSnapshot(Pair(x,z))
|
||||
npcDeathSnapshots[npcID] = LootTrackerView.GroundSnapshot(preDeathSnapshot, Pair(x, z), 0)
|
||||
// Call registered killing blow NPC callbacks
|
||||
killingBlowNPCCallbacks.forEach { callback ->
|
||||
callback.onKillingBlowNPC(npcID, x, z)
|
||||
}
|
||||
}
|
||||
|
||||
private fun allSpritesLoaded() : Boolean {
|
||||
|
|
@ -296,7 +288,7 @@ class plugin : Plugin() {
|
|||
return false
|
||||
}
|
||||
}
|
||||
val otherIcons = arrayOf(LVL_ICON, MAG_SPRITE, LOOT_ICON, WRENCH_ICON, COMBAT_LVL_SPRITE, BAG_ICON)
|
||||
val otherIcons = arrayOf(LVL_ICON, MAG_SPRITE, LOOT_ICON, WRENCH_ICON, Constants.COMBAT_LVL_SPRITE, LootTrackerView.BAG_ICON)
|
||||
for (icon in otherIcons) {
|
||||
if(!js5Archive8.isFileReady(icon)){
|
||||
return false
|
||||
|
|
@ -317,6 +309,11 @@ class plugin : Plugin() {
|
|||
destroyAltCanvas()
|
||||
} else if (useScaledFixed && altCanvas == null) {
|
||||
initAltCanvas()
|
||||
} else if (!useScaledFixed && altCanvas != null) {
|
||||
// Was using scaled fixed but toggled the setting
|
||||
// restore the original canvas
|
||||
moveCanvasToFront()
|
||||
destroyAltCanvas()
|
||||
}
|
||||
|
||||
when (mode) {
|
||||
|
|
@ -409,7 +406,7 @@ class plugin : Plugin() {
|
|||
private fun searchHiscore(username: String): Runnable {
|
||||
return Runnable {
|
||||
setActiveView(HiscoresView.VIEW_NAME)
|
||||
val customSearchField = hiScoreView?.let { HiscoresView.CustomSearchField(it) }
|
||||
val customSearchField = HiscoresView.hiScoreView?.let { HiscoresView.CustomSearchField(it) }
|
||||
|
||||
customSearchField?.searchPlayer(username) ?: run {
|
||||
println("searchView is null or CustomSearchField creation failed.")
|
||||
|
|
@ -447,15 +444,33 @@ class plugin : Plugin() {
|
|||
}
|
||||
|
||||
// Register Views
|
||||
createXPTrackerView()
|
||||
createHiscoreSearchView()
|
||||
createLootTrackerView()
|
||||
createReflectiveEditorView()
|
||||
val xpTrackerView = XPTrackerView
|
||||
val hiscoresView = HiscoresView
|
||||
val lootTrackerView = LootTrackerView
|
||||
val reflectiveEditorView = ReflectiveEditorView
|
||||
|
||||
// Create views
|
||||
xpTrackerView.createView()
|
||||
hiscoresView.createView()
|
||||
lootTrackerView.createView()
|
||||
reflectiveEditorView.createView()
|
||||
|
||||
// Register views
|
||||
views.add(xpTrackerView)
|
||||
views.add(hiscoresView)
|
||||
views.add(lootTrackerView)
|
||||
views.add(reflectiveEditorView)
|
||||
|
||||
// Register view functions
|
||||
xpTrackerView.registerFunctions()
|
||||
hiscoresView.registerFunctions()
|
||||
lootTrackerView.registerFunctions()
|
||||
reflectiveEditorView.registerFunctions()
|
||||
|
||||
mainContentPanel.add(ScrollablePanel(xpTrackerView!!), XPTrackerView.VIEW_NAME)
|
||||
mainContentPanel.add(ScrollablePanel(hiScoreView!!), HiscoresView.VIEW_NAME)
|
||||
mainContentPanel.add(ScrollablePanel(lootTrackerView!!), LootTrackerView.VIEW_NAME)
|
||||
mainContentPanel.add(ScrollablePanel(reflectiveEditorView!!), ReflectiveEditorView.VIEW_NAME)
|
||||
mainContentPanel.add(ScrollablePanel(xpTrackerView.panel), xpTrackerView.name)
|
||||
mainContentPanel.add(ScrollablePanel(hiscoresView.panel), hiscoresView.name)
|
||||
mainContentPanel.add(ScrollablePanel(lootTrackerView.panel), lootTrackerView.name)
|
||||
mainContentPanel.add(ScrollablePanel(reflectiveEditorView.panel), reflectiveEditorView.name)
|
||||
|
||||
val navPanel = Panel().apply {
|
||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
||||
|
|
@ -463,10 +478,10 @@ class plugin : Plugin() {
|
|||
preferredSize = Dimension(NAVBAR_WIDTH, frame.height)
|
||||
}
|
||||
|
||||
navPanel.add(createNavButton(LVL_ICON, XPTrackerView.VIEW_NAME))
|
||||
navPanel.add(createNavButton(MAG_SPRITE, HiscoresView.VIEW_NAME))
|
||||
navPanel.add(createNavButton(LOOT_ICON, LootTrackerView.VIEW_NAME))
|
||||
navPanel.add(createNavButton(WRENCH_ICON, ReflectiveEditorView.VIEW_NAME))
|
||||
navPanel.add(createNavButton(xpTrackerView.iconSpriteId, xpTrackerView.name))
|
||||
navPanel.add(createNavButton(hiscoresView.iconSpriteId, hiscoresView.name))
|
||||
navPanel.add(createNavButton(lootTrackerView.iconSpriteId, lootTrackerView.name))
|
||||
navPanel.add(createNavButton(reflectiveEditorView.iconSpriteId, reflectiveEditorView.name))
|
||||
|
||||
val rightPanel = Panel(BorderLayout()).apply {
|
||||
add(mainContentPanel, BorderLayout.CENTER)
|
||||
|
|
@ -481,45 +496,80 @@ class plugin : Plugin() {
|
|||
verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_NEVER
|
||||
}
|
||||
|
||||
frame.layout = BorderLayout()
|
||||
rightPanelWrapper?.let {
|
||||
frame.add(it, BorderLayout.EAST)
|
||||
val desiredView = if (launchMinimized) HIDDEN_VIEW else xpTrackerView.name
|
||||
// Commit layout synchronously on the EDT to avoid initial misplacement
|
||||
val commit = Runnable {
|
||||
frame.layout = BorderLayout()
|
||||
rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) }
|
||||
setActiveView(desiredView)
|
||||
frame.validate()
|
||||
frame.repaint()
|
||||
}
|
||||
|
||||
if(launchMinimized){
|
||||
setActiveView(HIDDEN_VIEW)
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
commit.run()
|
||||
} else {
|
||||
setActiveView(XPTrackerView.VIEW_NAME)
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(commit)
|
||||
} catch (e: Exception) {
|
||||
// Fallback to async if invokeAndWait fails for any reason
|
||||
SwingUtilities.invokeLater(commit)
|
||||
}
|
||||
}
|
||||
initialized = true
|
||||
pluginsReloaded = true
|
||||
updateDisplaySettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setActiveView(viewName: String) {
|
||||
// Handle the visibility of the main content panel
|
||||
if (viewName == HIDDEN_VIEW) {
|
||||
mainContentPanel.isVisible = false
|
||||
} else {
|
||||
if (!mainContentPanel.isVisible) {
|
||||
mainContentPanel.isVisible = true
|
||||
val runUpdate: () -> Unit = {
|
||||
// Track visibility change to decide if we need to resize/reload interfaces
|
||||
val wasVisible = mainContentPanel.isVisible
|
||||
|
||||
// Handle the visibility of the main content panel and card switch
|
||||
if (viewName == HIDDEN_VIEW) {
|
||||
mainContentPanel.isVisible = false
|
||||
} else {
|
||||
if (!mainContentPanel.isVisible) {
|
||||
mainContentPanel.isVisible = true
|
||||
}
|
||||
cardLayout.show(mainContentPanel, viewName)
|
||||
}
|
||||
cardLayout.show(mainContentPanel, viewName)
|
||||
|
||||
val visibilityChanged = wasVisible != mainContentPanel.isVisible
|
||||
|
||||
// Batch painting to avoid intermediate repaints
|
||||
rightPanelWrapper?.ignoreRepaint = true
|
||||
try {
|
||||
if (visibilityChanged) {
|
||||
// Only touch layout and client interfaces if width actually changes
|
||||
updateDisplaySettings()
|
||||
reloadInterfaces = true
|
||||
rightPanelWrapper?.revalidate()
|
||||
frame?.validate()
|
||||
} else {
|
||||
// Just a card switch; avoid full frame revalidate
|
||||
mainContentPanel.revalidate()
|
||||
}
|
||||
} finally {
|
||||
rightPanelWrapper?.ignoreRepaint = false
|
||||
}
|
||||
|
||||
// Targeted repaint for snappy feedback
|
||||
if (visibilityChanged) {
|
||||
rightPanelWrapper?.repaint()
|
||||
frame?.repaint()
|
||||
} else {
|
||||
mainContentPanel.repaint()
|
||||
}
|
||||
StateManager.focusedView = viewName
|
||||
}
|
||||
|
||||
reloadInterfaces = true
|
||||
updateDisplaySettings()
|
||||
|
||||
// Revalidate and repaint necessary panels
|
||||
mainContentPanel.revalidate()
|
||||
rightPanelWrapper?.revalidate()
|
||||
frame?.revalidate()
|
||||
|
||||
mainContentPanel.repaint()
|
||||
rightPanelWrapper?.repaint()
|
||||
frame?.repaint()
|
||||
|
||||
focusedView = viewName
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
runUpdate()
|
||||
} else {
|
||||
SwingUtilities.invokeLater { runUpdate() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNavButton(spriteId: Int, viewName: String): JPanel {
|
||||
|
|
@ -535,7 +585,7 @@ class plugin : Plugin() {
|
|||
}
|
||||
lastClickTime = currentTime
|
||||
|
||||
if (focusedView == viewName) {
|
||||
if (StateManager.focusedView == viewName) {
|
||||
setActiveView("HIDDEN")
|
||||
} else {
|
||||
setActiveView(viewName)
|
||||
|
|
@ -666,7 +716,7 @@ class plugin : Plugin() {
|
|||
}
|
||||
|
||||
private fun loadFont(): Font? {
|
||||
val fontStream = plugin::class.java.getResourceAsStream("res/runescape_small.ttf")
|
||||
val fontStream = Helpers.openResource("res/runescape_small.ttf")
|
||||
return if (fontStream != null) {
|
||||
try {
|
||||
val font = Font.createFont(Font.TRUETYPE_FONT, fontStream)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
AUTHOR='downthecrop'
|
||||
DESCRIPTION='A plugin that adds a right-side panel with custom widgets and navigation.'
|
||||
VERSION=2.0
|
||||
VERSION=2.1
|
||||
BIN
plugin-playground/src/main/kotlin/KondoKit/res/cog.png
Normal file
BIN
plugin-playground/src/main/kotlin/KondoKit/res/cog.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 893 B |
33
plugin-playground/src/main/kotlin/KondoKit/views/BaseView.kt
Normal file
33
plugin-playground/src/main/kotlin/KondoKit/views/BaseView.kt
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package KondoKit.views
|
||||
|
||||
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
|
||||
import java.awt.Dimension
|
||||
import javax.swing.BorderFactory
|
||||
import javax.swing.Box
|
||||
import javax.swing.BoxLayout
|
||||
import javax.swing.JPanel
|
||||
|
||||
open class BaseView(
|
||||
private val viewName: String,
|
||||
private val preferredWidth: Int = 242,
|
||||
private val addDefaultSpacing: Boolean = true
|
||||
) : JPanel() {
|
||||
|
||||
init {
|
||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
||||
name = viewName
|
||||
background = VIEW_BACKGROUND_COLOR
|
||||
border = BorderFactory.createEmptyBorder(0, 0, 0, 0)
|
||||
|
||||
if (addDefaultSpacing) {
|
||||
add(Box.createVerticalStrut(5))
|
||||
}
|
||||
}
|
||||
|
||||
fun setViewSize(height: Int) {
|
||||
val dimension = Dimension(preferredWidth, height)
|
||||
preferredSize = dimension
|
||||
maximumSize = dimension
|
||||
minimumSize = dimension
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,21 @@
|
|||
package KondoKit
|
||||
package KondoKit.views
|
||||
|
||||
import KondoKit.Constants.COLOR_BACKGROUND_DARK
|
||||
import KondoKit.Constants.SKILL_DISPLAY_ORDER
|
||||
import KondoKit.Constants.SKILL_SPRITE_DIMENSION
|
||||
import KondoKit.Helpers.formatHtmlLabelText
|
||||
import KondoKit.Helpers.getSpriteId
|
||||
import KondoKit.Helpers.showToast
|
||||
import KondoKit.ImageCanvas
|
||||
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
|
||||
import KondoKit.plugin.Companion.POPUP_FOREGROUND
|
||||
import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
|
||||
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
|
||||
import KondoKit.components.LabelComponent
|
||||
import KondoKit.components.SearchField
|
||||
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||
import KondoKit.plugin.Companion.primaryColor
|
||||
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
|
||||
import KondoKit.plugin.Companion.POPUP_FOREGROUND
|
||||
import KondoKit.plugin.Companion.secondaryColor
|
||||
import KondoKit.plugin.StateManager.focusedView
|
||||
import KondoKit.plugin.Companion.primaryColor
|
||||
import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
|
||||
import com.google.gson.Gson
|
||||
import plugin.api.API
|
||||
import rt4.Sprites
|
||||
import java.awt.*
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.event.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
|
|
@ -64,125 +57,41 @@ object Constants {
|
|||
val SKILL_DISPLAY_ORDER = arrayOf(0,3,14,2,16,13,1,15,10,4,17,7,5,12,11,6,9,8,20,18,19,22,21,23)
|
||||
}
|
||||
|
||||
var text: String = ""
|
||||
|
||||
object HiscoresView {
|
||||
object HiscoresView : View {
|
||||
|
||||
const val VIEW_NAME = "HISCORE_SEARCH_VIEW"
|
||||
var hiScoreView: JPanel? = null
|
||||
class CustomSearchField(private val hiscoresPanel: JPanel) : Canvas() {
|
||||
override val name: String = VIEW_NAME
|
||||
override val iconSpriteId: Int = Constants.MAG_SPRITE
|
||||
|
||||
override val panel: JPanel
|
||||
get() = hiScoreView ?: JPanel()
|
||||
|
||||
private var cursorVisible: Boolean = true
|
||||
override fun createView() {
|
||||
createHiscoreSearchView()
|
||||
}
|
||||
|
||||
override fun registerFunctions() {
|
||||
// Hiscores functions are handled within the view itself
|
||||
}
|
||||
|
||||
class CustomSearchField(private val hiscoresPanel: JPanel) : SearchField(
|
||||
onSearch = { _ -> }, // Placeholder, will be replaced
|
||||
viewName = VIEW_NAME
|
||||
) {
|
||||
private val gson = Gson()
|
||||
|
||||
private val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(Constants.MAG_SPRITE))
|
||||
private val imageCanvas = bufferedImageSprite.let {
|
||||
ImageCanvas(it).apply {
|
||||
preferredSize = Constants.ICON_DIMENSION_SMALL
|
||||
size = preferredSize
|
||||
minimumSize = preferredSize
|
||||
maximumSize = preferredSize
|
||||
fillColor = COLOR_BACKGROUND_DARK
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
preferredSize = Constants.SEARCH_FIELD_DIMENSION
|
||||
background = Constants.COLOR_BACKGROUND_DARK
|
||||
foreground = Constants.COLOR_FOREGROUND_LIGHT
|
||||
font = Constants.FONT_ARIAL_PLAIN_14
|
||||
minimumSize = preferredSize
|
||||
maximumSize = preferredSize
|
||||
|
||||
addKeyListener(object : KeyAdapter() {
|
||||
override fun keyTyped(e: KeyEvent) {
|
||||
// Prevent null character from being typed on Ctrl+A & Ctrl+V
|
||||
if (e.isControlDown && (e.keyChar == '\u0001' || e.keyChar == '\u0016')) {
|
||||
e.consume()
|
||||
return
|
||||
}
|
||||
if (e.keyChar == '\b') {
|
||||
if (text.isNotEmpty()) {
|
||||
text = text.dropLast(1)
|
||||
}
|
||||
} else if (e.keyChar == '\n') {
|
||||
searchPlayer(text)
|
||||
} else {
|
||||
text += e.keyChar
|
||||
}
|
||||
SwingUtilities.invokeLater {
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
if (e.isControlDown) {
|
||||
when (e.keyCode) {
|
||||
KeyEvent.VK_A -> {
|
||||
text = ""
|
||||
repaint()
|
||||
}
|
||||
KeyEvent.VK_V -> {
|
||||
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
|
||||
val pasteText = clipboard.getData(DataFlavor.stringFlavor) as String
|
||||
text += pasteText
|
||||
SwingUtilities.invokeLater {
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (e.x > width - 20 && e.y < 20) {
|
||||
text = ""
|
||||
SwingUtilities.invokeLater {
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Timer(500) {
|
||||
cursorVisible = !cursorVisible
|
||||
if(focusedView == VIEW_NAME)
|
||||
SwingUtilities.invokeLater {
|
||||
repaint()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun paint(g: Graphics) {
|
||||
super.paint(g)
|
||||
g.color = foreground
|
||||
g.font = font
|
||||
|
||||
val fm = g.fontMetrics
|
||||
val cursorX = fm.stringWidth(text) + 30
|
||||
|
||||
imageCanvas.let { canvas ->
|
||||
val imgG = g.create(5, 5, canvas.width, canvas.height)
|
||||
canvas.paint(imgG)
|
||||
imgG.dispose()
|
||||
}
|
||||
|
||||
g.drawString(text, 30, 20)
|
||||
|
||||
if (cursorVisible && hasFocus()) {
|
||||
g.drawLine(cursorX, 5, cursorX, 25)
|
||||
}
|
||||
|
||||
if (text.isNotEmpty()) {
|
||||
g.color = Color.RED
|
||||
g.drawString("x", width - 20, 20)
|
||||
}
|
||||
// This is a workaround to set the onSearch callback after the class is fully initialized
|
||||
val onSearchField = javaClass.superclass.getDeclaredField("onSearch")
|
||||
onSearchField.isAccessible = true
|
||||
onSearchField.set(this, { username: String -> searchPlayer(username) })
|
||||
}
|
||||
|
||||
fun searchPlayer(username: String) {
|
||||
text = username.replace(" ", "_")
|
||||
val apiUrl = "http://api.2009scape.org:3000/hiscores/playerSkills/1/${text.toLowerCase()}"
|
||||
val cleanUsername = username.replace(" ", "_")
|
||||
setText(cleanUsername)
|
||||
val apiUrl = "http://api.2009scape.org:3000/hiscores/playerSkills/1/${cleanUsername.toLowerCase()}"
|
||||
|
||||
updateHiscoresView(null, "Searching...")
|
||||
|
||||
|
|
@ -223,7 +132,6 @@ object HiscoresView {
|
|||
}.start()
|
||||
}
|
||||
|
||||
|
||||
private fun updatePlayerData(jsonResponse: String, username: String) {
|
||||
val hiscoresResponse = gson.fromJson(jsonResponse, HiscoresResponse::class.java)
|
||||
updateHiscoresView(hiscoresResponse, username)
|
||||
|
|
@ -232,10 +140,12 @@ object HiscoresView {
|
|||
private fun updateHiscoresView(data: HiscoresResponse?, username: String) {
|
||||
val playerNameLabel = findComponentByName(hiscoresPanel, "playerNameLabel") as? JPanel
|
||||
playerNameLabel?.removeAll() // Clear previous components
|
||||
var nameLabel = JLabel(formatHtmlLabelText(username, secondaryColor, "", primaryColor), JLabel.CENTER).apply {
|
||||
var nameLabel = LabelComponent().apply {
|
||||
updateHtmlText(username, secondaryColor, "", primaryColor)
|
||||
font = Constants.FONT_ARIAL_BOLD_12
|
||||
foreground = Constants.COLOR_FOREGROUND_LIGHT
|
||||
border = BorderFactory.createEmptyBorder(0, 6, 0, 0) // Top, Left, Bottom, Right padding
|
||||
horizontalAlignment = JLabel.CENTER
|
||||
}
|
||||
playerNameLabel?.add(nameLabel)
|
||||
playerNameLabel?.revalidate()
|
||||
|
|
@ -260,10 +170,12 @@ object HiscoresView {
|
|||
}
|
||||
|
||||
val exp_multiplier = data.info.exp_multiplier
|
||||
nameLabel = JLabel(formatHtmlLabelText(username, secondaryColor, " (${exp_multiplier}x)", primaryColor), JLabel.CENTER).apply {
|
||||
nameLabel = LabelComponent().apply {
|
||||
updateHtmlText(username, secondaryColor, " (${exp_multiplier}x)", primaryColor)
|
||||
font = Constants.FONT_ARIAL_BOLD_12
|
||||
foreground = Constants.COLOR_FOREGROUND_LIGHT
|
||||
border = BorderFactory.createEmptyBorder(0, 6, 0, 0) // Top, Left, Bottom, Right padding
|
||||
horizontalAlignment = JLabel.CENTER
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -342,13 +254,9 @@ object HiscoresView {
|
|||
}
|
||||
|
||||
fun createHiscoreSearchView() {
|
||||
val hiscorePanel = JPanel().apply {
|
||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
||||
name = VIEW_NAME
|
||||
val hiscorePanel = BaseView(VIEW_NAME, Constants.HISCORE_PANEL_DIMENSION.width).apply {
|
||||
background = Constants.COLOR_BACKGROUND_MEDIUM
|
||||
preferredSize = Constants.HISCORE_PANEL_DIMENSION
|
||||
maximumSize = preferredSize
|
||||
minimumSize = preferredSize
|
||||
setViewSize(Constants.HISCORE_PANEL_DIMENSION.height)
|
||||
}
|
||||
|
||||
val customSearchField = CustomSearchField(hiscorePanel)
|
||||
|
|
@ -392,23 +300,23 @@ object HiscoresView {
|
|||
minimumSize = preferredSize
|
||||
}
|
||||
|
||||
for (i in SKILL_DISPLAY_ORDER) {
|
||||
for (i in Constants.SKILL_DISPLAY_ORDER) {
|
||||
val skillPanel = JPanel().apply {
|
||||
layout = BorderLayout()
|
||||
background = COLOR_BACKGROUND_DARK
|
||||
background = Constants.COLOR_BACKGROUND_DARK
|
||||
preferredSize = Constants.SKILL_PANEL_DIMENSION
|
||||
maximumSize = preferredSize
|
||||
minimumSize = preferredSize
|
||||
border = MatteBorder(5, 0, 0, 0, COLOR_BACKGROUND_DARK)
|
||||
border = MatteBorder(5, 0, 0, 0, Constants.COLOR_BACKGROUND_DARK)
|
||||
}
|
||||
|
||||
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(getSpriteId(i)))
|
||||
|
||||
val imageCanvas = bufferedImageSprite.let {
|
||||
ImageCanvas(it).apply {
|
||||
preferredSize = SKILL_SPRITE_DIMENSION
|
||||
size = SKILL_SPRITE_DIMENSION
|
||||
fillColor = COLOR_BACKGROUND_DARK
|
||||
preferredSize = Constants.SKILL_SPRITE_DIMENSION
|
||||
size = Constants.SKILL_SPRITE_DIMENSION
|
||||
fillColor = Constants.COLOR_BACKGROUND_DARK
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -421,7 +329,7 @@ object HiscoresView {
|
|||
}
|
||||
|
||||
val imageContainer = JPanel(FlowLayout(FlowLayout.CENTER, 5, 0)).apply {
|
||||
background = COLOR_BACKGROUND_DARK
|
||||
background = Constants.COLOR_BACKGROUND_DARK
|
||||
add(imageCanvas)
|
||||
add(numberLabel)
|
||||
}
|
||||
|
|
@ -433,7 +341,7 @@ object HiscoresView {
|
|||
hiscorePanel.add(skillsPanel)
|
||||
|
||||
val totalCombatPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 0)).apply {
|
||||
background = COLOR_BACKGROUND_DARK
|
||||
background = Constants.COLOR_BACKGROUND_DARK
|
||||
preferredSize = Constants.TOTAL_COMBAT_PANEL_DIMENSION
|
||||
maximumSize = preferredSize
|
||||
minimumSize = preferredSize
|
||||
|
|
@ -442,7 +350,7 @@ object HiscoresView {
|
|||
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(Constants.LVL_BAR_SPRITE))
|
||||
|
||||
val totalLevelIcon = ImageCanvas(bufferedImageSprite).apply {
|
||||
fillColor = COLOR_BACKGROUND_DARK
|
||||
fillColor = Constants.COLOR_BACKGROUND_DARK
|
||||
preferredSize = Constants.ICON_DIMENSION_LARGE
|
||||
size = Constants.ICON_DIMENSION_LARGE
|
||||
}
|
||||
|
|
@ -464,7 +372,7 @@ object HiscoresView {
|
|||
val bufferedImageSprite2 = getBufferedImageFromSprite(API.GetSprite(Constants.COMBAT_LVL_SPRITE))
|
||||
|
||||
val combatLevelIcon = ImageCanvas(bufferedImageSprite2).apply {
|
||||
fillColor = COLOR_BACKGROUND_DARK
|
||||
fillColor = Constants.COLOR_BACKGROUND_DARK
|
||||
preferredSize = Constants.ICON_DIMENSION_LARGE
|
||||
size = Constants.ICON_DIMENSION_LARGE
|
||||
}
|
||||
|
|
@ -478,7 +386,7 @@ object HiscoresView {
|
|||
}
|
||||
|
||||
val combatLevelPanel = JPanel(FlowLayout(FlowLayout.LEFT)).apply {
|
||||
background = COLOR_BACKGROUND_DARK
|
||||
background = Constants.COLOR_BACKGROUND_DARK
|
||||
add(combatLevelIcon)
|
||||
add(combatLevelLabel)
|
||||
}
|
||||
|
|
@ -1,18 +1,23 @@
|
|||
package KondoKit
|
||||
package KondoKit.views
|
||||
|
||||
import KondoKit.Helpers
|
||||
import KondoKit.Helpers.addMouseListenerToAll
|
||||
import KondoKit.Helpers.formatHtmlLabelText
|
||||
import KondoKit.ImageCanvas
|
||||
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
|
||||
import KondoKit.XPTrackerView.wrappedWidget
|
||||
import KondoKit.views.XPTrackerView.wrappedWidget
|
||||
import KondoKit.components.PopupMenuComponent
|
||||
import KondoKit.components.ProgressBar
|
||||
import KondoKit.plugin.Companion.POPUP_BACKGROUND
|
||||
import KondoKit.plugin.Companion.POPUP_FOREGROUND
|
||||
import KondoKit.plugin.Companion.TITLE_BAR_COLOR
|
||||
import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
|
||||
import KondoKit.plugin.Companion.TOTAL_XP_WIDGET_SIZE
|
||||
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
|
||||
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||
import KondoKit.plugin.Companion.primaryColor
|
||||
import KondoKit.plugin.Companion.registerDrawAction
|
||||
import KondoKit.plugin.Companion.secondaryColor
|
||||
import KondoKit.plugin.Companion.useLiveGEPrices
|
||||
import KondoKit.plugin.StateManager.focusedView
|
||||
import plugin.api.API
|
||||
import rt4.*
|
||||
|
|
@ -22,17 +27,16 @@ import java.awt.event.MouseAdapter
|
|||
import java.awt.event.MouseEvent
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.text.DecimalFormat
|
||||
import javax.swing.*
|
||||
import kotlin.math.ceil
|
||||
|
||||
object LootTrackerView {
|
||||
object LootTrackerView : View, OnPostClientTickCallback, OnKillingBlowNPCCallback {
|
||||
private const val SNAPSHOT_LIFESPAN = 10
|
||||
const val BAG_ICON = 900
|
||||
const val OPEN_BAG = 777
|
||||
val npcDeathSnapshots = mutableMapOf<Int, GroundSnapshot>()
|
||||
var gePriceMap = loadGEPrices()
|
||||
const val VIEW_NAME = "LOOT_TRACKER_VIEW"
|
||||
|
|
@ -42,9 +46,33 @@ object LootTrackerView {
|
|||
var lastConfirmedKillNpcId = -1
|
||||
private var customToolTipWindow: JWindow? = null
|
||||
var lootTrackerView: JPanel? = null
|
||||
override val name: String = VIEW_NAME
|
||||
override val iconSpriteId: Int = OPEN_BAG
|
||||
|
||||
override val panel: JPanel
|
||||
get() = lootTrackerView ?: JPanel()
|
||||
|
||||
override fun createView() {
|
||||
createLootTrackerView()
|
||||
}
|
||||
|
||||
override fun registerFunctions() {
|
||||
// Register callbacks with the plugin
|
||||
KondoKit.plugin.registerPostClientTickCallback(this)
|
||||
KondoKit.plugin.registerKillingBlowNPCCallback(this)
|
||||
}
|
||||
|
||||
override fun onPostClientTick() {
|
||||
lootTrackerView?.let { onPostClientTick(it) }
|
||||
}
|
||||
|
||||
override fun onKillingBlowNPC(npcID: Int, x: Int, z: Int) {
|
||||
val preDeathSnapshot = takeGroundSnapshot(Pair(x,z))
|
||||
npcDeathSnapshots[npcID] = GroundSnapshot(preDeathSnapshot, Pair(x, z), 0)
|
||||
}
|
||||
|
||||
fun loadGEPrices(): Map<String, String> {
|
||||
return if (plugin.useLiveGEPrices) {
|
||||
return if (useLiveGEPrices) {
|
||||
try {
|
||||
println("LootTracker: Loading Remote GE Prices")
|
||||
val url = URL("https://cdn.2009scape.org/gedata/latest.json")
|
||||
|
|
@ -79,23 +107,21 @@ object LootTrackerView {
|
|||
} else {
|
||||
try {
|
||||
println("LootTracker: Loading Local GE Prices")
|
||||
BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("res/item_configs.json"), StandardCharsets.UTF_8))
|
||||
.useLines { lines ->
|
||||
val json = lines.joinToString("\n")
|
||||
val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }
|
||||
val gePrices = mutableMapOf<String, String>()
|
||||
Helpers.readResourceText("res/item_configs.json")?.let { json ->
|
||||
val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }
|
||||
val gePrices = mutableMapOf<String, String>()
|
||||
|
||||
for (item in items) {
|
||||
val pairs = item.removeSurrounding("{", "}").split(",")
|
||||
val id = pairs.find { it.trim().startsWith("\"id\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
|
||||
val grandExchangePrice = pairs.find { it.trim().startsWith("\"grand_exchange_price\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
|
||||
if (id != null && grandExchangePrice != null) {
|
||||
gePrices[id] = grandExchangePrice
|
||||
}
|
||||
for (item in items) {
|
||||
val pairs = item.removeSurrounding("{", "}").split(",")
|
||||
val id = pairs.find { it.trim().startsWith("\"id\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
|
||||
val grandExchangePrice = pairs.find { it.trim().startsWith("\"grand_exchange_price\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
|
||||
if (id != null && grandExchangePrice != null) {
|
||||
gePrices[id] = grandExchangePrice
|
||||
}
|
||||
|
||||
gePrices
|
||||
}
|
||||
|
||||
gePrices
|
||||
} ?: emptyMap()
|
||||
} catch (e: Exception) {
|
||||
emptyMap()
|
||||
}
|
||||
|
|
@ -105,9 +131,7 @@ object LootTrackerView {
|
|||
|
||||
|
||||
fun createLootTrackerView() {
|
||||
lootTrackerView = JPanel().apply {
|
||||
layout = BoxLayout(this, BoxLayout.Y_AXIS) // Use BoxLayout on Y axis to stack widgets vertically
|
||||
background = VIEW_BACKGROUND_COLOR
|
||||
lootTrackerView = BaseView(VIEW_NAME, addDefaultSpacing = false).apply {
|
||||
add(Box.createVerticalStrut(5))
|
||||
totalTrackerWidget = createTotalLootWidget()
|
||||
|
||||
|
|
@ -376,7 +400,8 @@ object LootTrackerView {
|
|||
fun onPostClientTick(lootTrackerView: JPanel) {
|
||||
val toRemove = mutableListOf<Int>()
|
||||
|
||||
npcDeathSnapshots.entries.forEach { (npcId, snapshot) ->
|
||||
npcDeathSnapshots.entries.forEach { entry ->
|
||||
val (npcId, snapshot) = entry
|
||||
val postDeathSnapshot = takeGroundSnapshot(Pair(snapshot.location.first, snapshot.location.second))
|
||||
val newDrops = postDeathSnapshot.subtract(snapshot.items)
|
||||
|
||||
|
|
@ -392,7 +417,9 @@ object LootTrackerView {
|
|||
}
|
||||
}
|
||||
|
||||
toRemove.forEach { npcDeathSnapshots.remove(it) }
|
||||
toRemove.forEach { npcId ->
|
||||
npcDeathSnapshots.remove(npcId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -427,7 +454,7 @@ object LootTrackerView {
|
|||
newDrops.forEach { drop ->
|
||||
val geValue = (gePriceMap[drop.id.toString()]?.toInt() ?: 0) * drop.quantity
|
||||
updateValueLabel(lootTrackerView, geValue.toString(), npcName)
|
||||
plugin.registerDrawAction { addItemToLootPanel(lootTrackerView, drop, npcName) }
|
||||
registerDrawAction { addItemToLootPanel(lootTrackerView, drop, npcName) }
|
||||
updateTotalValue(geValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -503,7 +530,7 @@ object LootTrackerView {
|
|||
|
||||
private fun removeLootFrameMenu(toRemove: JPanel, npcName: String): JPopupMenu {
|
||||
// Create a popup menu
|
||||
val popupMenu = JPopupMenu()
|
||||
val popupMenu = PopupMenuComponent()
|
||||
val rFont = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||
|
||||
popupMenu.background = POPUP_BACKGROUND
|
||||
|
|
@ -540,20 +567,17 @@ object LootTrackerView {
|
|||
|
||||
private fun resetLootTrackerMenu(): JPopupMenu {
|
||||
// Create a popup menu
|
||||
val popupMenu = JPopupMenu()
|
||||
val rFont = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||
|
||||
popupMenu.background = POPUP_BACKGROUND
|
||||
val popupMenu = PopupMenuComponent()
|
||||
|
||||
// Create menu items with custom font and colors
|
||||
val menuItem1 = JMenuItem("Reset Loot Tracker").apply {
|
||||
font = rFont // Set custom font
|
||||
background = POPUP_BACKGROUND // Dark background for item
|
||||
foreground = POPUP_FOREGROUND // Light text color for item
|
||||
font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||
background = POPUP_BACKGROUND
|
||||
foreground = POPUP_FOREGROUND
|
||||
}
|
||||
popupMenu.add(menuItem1)
|
||||
menuItem1.addActionListener {
|
||||
plugin.registerDrawAction {
|
||||
registerDrawAction {
|
||||
resetLootTracker()
|
||||
}
|
||||
}
|
||||
|
|
@ -608,4 +632,18 @@ object LootTrackerView {
|
|||
|
||||
data class GroundSnapshot(val items: Set<Item>, val location: Pair<Int, Int>, var age: Int)
|
||||
data class Item(val id: Int, val quantity: Int)
|
||||
}
|
||||
|
||||
// XPWidget data class for loot tracking
|
||||
data class XPWidget(
|
||||
val container: Container,
|
||||
val skillId: Int,
|
||||
val xpGainedLabel: JLabel,
|
||||
val xpLeftLabel: JLabel,
|
||||
val xpPerHourLabel: JLabel,
|
||||
val actionsRemainingLabel: JLabel,
|
||||
val progressBar: ProgressBar,
|
||||
var totalXpGained: Int = 0,
|
||||
var startTime: Long = System.currentTimeMillis(),
|
||||
var previousXp: Int = 0
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
12
plugin-playground/src/main/kotlin/KondoKit/views/View.kt
Normal file
12
plugin-playground/src/main/kotlin/KondoKit/views/View.kt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package KondoKit.views
|
||||
|
||||
import javax.swing.JPanel
|
||||
|
||||
interface View {
|
||||
val name: String
|
||||
val iconSpriteId: Int
|
||||
val panel: JPanel
|
||||
|
||||
fun createView()
|
||||
fun registerFunctions()
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package KondoKit.views
|
||||
|
||||
interface OnUpdateCallback {
|
||||
fun onUpdate()
|
||||
}
|
||||
|
||||
interface OnDrawCallback {
|
||||
fun onDraw(timeDelta: Long)
|
||||
}
|
||||
|
||||
interface OnXPUpdateCallback {
|
||||
fun onXPUpdate(skillId: Int, xp: Int)
|
||||
}
|
||||
|
||||
interface OnKillingBlowNPCCallback {
|
||||
fun onKillingBlowNPC(npcID: Int, x: Int, z: Int)
|
||||
}
|
||||
|
||||
interface OnPostClientTickCallback {
|
||||
fun onPostClientTick()
|
||||
}
|
||||
|
|
@ -0,0 +1,497 @@
|
|||
package KondoKit.views
|
||||
|
||||
import KondoKit.Helpers
|
||||
import KondoKit.Helpers.addMouseListenerToAll
|
||||
import KondoKit.Helpers.formatHtmlLabelText
|
||||
import KondoKit.Helpers.formatNumber
|
||||
import KondoKit.Helpers.getProgressBarColor
|
||||
import KondoKit.Helpers.getSpriteId
|
||||
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
|
||||
import KondoKit.XPTable
|
||||
import KondoKit.components.PopupMenuComponent
|
||||
import KondoKit.components.ProgressBar
|
||||
import KondoKit.components.WidgetPanel
|
||||
import KondoKit.plugin.Companion.IMAGE_SIZE
|
||||
import KondoKit.plugin.Companion.LVL_ICON
|
||||
import KondoKit.plugin.Companion.TOTAL_XP_WIDGET_SIZE
|
||||
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||
import KondoKit.plugin.Companion.WIDGET_SIZE
|
||||
import KondoKit.plugin
|
||||
import KondoKit.plugin.Companion.POPUP_BACKGROUND
|
||||
import KondoKit.plugin.Companion.POPUP_FOREGROUND
|
||||
import KondoKit.plugin.Companion.playerXPMultiplier
|
||||
import KondoKit.plugin.Companion.primaryColor
|
||||
import KondoKit.plugin.Companion.secondaryColor
|
||||
import KondoKit.plugin.StateManager.focusedView
|
||||
import plugin.api.API
|
||||
import java.awt.*
|
||||
import java.awt.image.BufferedImage
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.*
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
object XPTrackerView : View, OnUpdateCallback, OnXPUpdateCallback {
|
||||
private val COMBAT_SKILLS = intArrayOf(0,1,2,3,4)
|
||||
val xpWidgets: MutableMap<Int, XPWidget> = HashMap()
|
||||
var totalXPWidget: XPWidget? = null
|
||||
val initialXP: MutableMap<Int, Int> = HashMap()
|
||||
var xpTrackerView: JPanel? = null
|
||||
const val VIEW_NAME = "XP_TRACKER_VIEW"
|
||||
override val name: String = VIEW_NAME
|
||||
override val iconSpriteId: Int = LVL_ICON
|
||||
private val skillIconCache: MutableMap<Int, BufferedImage> = HashMap()
|
||||
|
||||
|
||||
val npcHitpointsMap: Map<Int, Int> = try {
|
||||
val json = Helpers.readResourceText("res/npc_hitpoints_map.json") ?: "{}"
|
||||
val pairs = json.trim().removeSurrounding("{", "}").split(",")
|
||||
val map = mutableMapOf<Int, Int>()
|
||||
|
||||
for (pair in pairs) {
|
||||
val keyValue = pair.split(":")
|
||||
val id = keyValue[0].trim().trim('\"').toIntOrNull()
|
||||
val hitpoints = keyValue[1].trim()
|
||||
|
||||
if (id != null && hitpoints.isNotEmpty()) {
|
||||
map[id] = hitpoints.toIntOrNull() ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
map
|
||||
} catch (e: Exception) {
|
||||
println("XPTracker Error parsing NPC HP: ${e.message}")
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
private val widgetFont = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||
|
||||
private fun createPopupListener(popupMenu: JPopupMenu) = object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (e.isPopupTrigger) popupMenu.show(e.component, e.x, e.y)
|
||||
}
|
||||
|
||||
override fun mouseReleased(e: MouseEvent) {
|
||||
if (e.isPopupTrigger) popupMenu.show(e.component, e.x, e.y)
|
||||
}
|
||||
}
|
||||
|
||||
private fun attachPopup(component: Container, popupMenu: JPopupMenu) {
|
||||
val listener = createPopupListener(popupMenu)
|
||||
addMouseListenerToAll(component, listener)
|
||||
component.addMouseListener(listener)
|
||||
}
|
||||
|
||||
private fun BufferedImage.ensureOpaque(): BufferedImage {
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
val color = getRGB(x, y)
|
||||
if (color != 0) {
|
||||
setRGB(x, y, color or (0xFF shl 24))
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
private fun createIconContainer(image: BufferedImage): JPanel {
|
||||
val processed = image.ensureOpaque()
|
||||
val icon = ImageIcon(processed)
|
||||
val label = JLabel(icon).apply {
|
||||
horizontalAlignment = SwingConstants.CENTER
|
||||
verticalAlignment = SwingConstants.CENTER
|
||||
isOpaque = false
|
||||
}
|
||||
|
||||
return JPanel(BorderLayout()).apply {
|
||||
background = WIDGET_COLOR
|
||||
val wrapperSize = Dimension(IMAGE_SIZE)
|
||||
preferredSize = wrapperSize
|
||||
minimumSize = wrapperSize
|
||||
maximumSize = wrapperSize
|
||||
add(label, BorderLayout.CENTER)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMetricLabel(title: String, initialValue: String = "0", topPadding: Int = 0): JLabel {
|
||||
return JLabel(formatHtmlLabelText("$title ", primaryColor, initialValue, secondaryColor)).apply {
|
||||
horizontalAlignment = JLabel.LEFT
|
||||
font = widgetFont
|
||||
if (topPadding > 0) {
|
||||
border = BorderFactory.createEmptyBorder(topPadding, 0, 0, 0)
|
||||
} else {
|
||||
border = BorderFactory.createEmptyBorder(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMenuItem(text: String): JMenuItem = JMenuItem(text).apply {
|
||||
font = widgetFont
|
||||
background = POPUP_BACKGROUND
|
||||
foreground = POPUP_FOREGROUND
|
||||
}
|
||||
|
||||
private fun getSkillIcon(skillId: Int): BufferedImage {
|
||||
return skillIconCache[skillId] ?: getBufferedImageFromSprite(API.GetSprite(getSpriteId(skillId))).also {
|
||||
skillIconCache[skillId] = it
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTotalWidgetContainer(popupMenu: JPopupMenu): Container {
|
||||
totalXPWidget = createTotalXPWidget()
|
||||
return wrappedWidget(totalXPWidget!!.container).also { attachPopup(it, popupMenu) }
|
||||
}
|
||||
|
||||
override val panel: JPanel
|
||||
get() = xpTrackerView ?: JPanel()
|
||||
|
||||
override fun createView() {
|
||||
createXPTrackerView()
|
||||
}
|
||||
|
||||
override fun registerFunctions() {
|
||||
// Register callbacks with the plugin
|
||||
plugin.registerUpdateCallback(this)
|
||||
plugin.registerXPUpdateCallback(this)
|
||||
}
|
||||
|
||||
override fun onUpdate() {
|
||||
val widgets = xpWidgets.values
|
||||
val totalXP = totalXPWidget
|
||||
|
||||
widgets.forEach { xpWidget ->
|
||||
val elapsedTime = (System.currentTimeMillis() - xpWidget.startTime) / 1000.0 / 60.0 / 60.0
|
||||
val xpPerHour = if (elapsedTime > 0) (xpWidget.totalXpGained / elapsedTime).toInt() else 0
|
||||
val formattedXpPerHour = formatNumber(xpPerHour)
|
||||
xpWidget.xpPerHourLabel.text =
|
||||
formatHtmlLabelText("XP /hr: ", primaryColor, formattedXpPerHour, secondaryColor)
|
||||
xpWidget.container.repaint()
|
||||
}
|
||||
|
||||
totalXP?.let { widget ->
|
||||
val elapsedTime = (System.currentTimeMillis() - widget.startTime) / 1000.0 / 60.0 / 60.0
|
||||
val totalXPPerHour = if (elapsedTime > 0) (widget.totalXpGained / elapsedTime).toInt() else 0
|
||||
val formattedTotalXpPerHour = formatNumber(totalXPPerHour)
|
||||
widget.xpPerHourLabel.text =
|
||||
formatHtmlLabelText("XP /hr: ", primaryColor, formattedTotalXpPerHour, secondaryColor)
|
||||
widget.container.repaint()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onXPUpdate(skillId: Int, xp: Int) {
|
||||
if (!initialXP.containsKey(skillId)) {
|
||||
initialXP[skillId] = xp
|
||||
return
|
||||
}
|
||||
val previousXpSnapshot = initialXP[skillId] ?: xp
|
||||
if (xp == initialXP[skillId]) return
|
||||
|
||||
val ensureOnEdt = Runnable {
|
||||
var xpWidget = xpWidgets[skillId]
|
||||
if (xpWidget != null) {
|
||||
updateWidget(xpWidget, xp)
|
||||
} else {
|
||||
xpWidget = createXPWidget(skillId, previousXpSnapshot)
|
||||
xpWidgets[skillId] = xpWidget
|
||||
|
||||
val wrapped = wrappedWidget(xpWidget.container)
|
||||
val popupMenu = removeXPWidgetMenu(wrapped, skillId)
|
||||
attachPopup(wrapped, popupMenu)
|
||||
|
||||
xpTrackerView?.add(wrapped)
|
||||
xpTrackerView?.add(Box.createVerticalStrut(5))
|
||||
|
||||
if(focusedView == VIEW_NAME) {
|
||||
xpTrackerView?.revalidate()
|
||||
xpTrackerView?.repaint()
|
||||
}
|
||||
|
||||
updateWidget(xpWidget, xp)
|
||||
}
|
||||
}
|
||||
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
ensureOnEdt.run()
|
||||
} else {
|
||||
SwingUtilities.invokeLater(ensureOnEdt)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateWidget(xpWidget: XPWidget, xp: Int) {
|
||||
val (currentLevel, xpGainedSinceLastLevel) = XPTable.getLevelForXp(xp)
|
||||
|
||||
var xpGainedSinceLastUpdate = xp - xpWidget.previousXp
|
||||
xpWidget.totalXpGained += xpGainedSinceLastUpdate
|
||||
updateTotalXPWidget(xpGainedSinceLastUpdate)
|
||||
|
||||
val progress: Double
|
||||
if (currentLevel >= 99) {
|
||||
progress = 100.0 // Set progress to 100% if the level is 99 or above
|
||||
xpWidget.xpLeftLabel.text = "" // Hide XP Left when level is 99
|
||||
xpWidget.actionsRemainingLabel.text = ""
|
||||
} else {
|
||||
val nextLevelXp = XPTable.getXpRequiredForLevel(currentLevel + 1)
|
||||
val xpLeft = nextLevelXp - xp
|
||||
progress = xpGainedSinceLastLevel.toDouble() / (nextLevelXp - XPTable.getXpRequiredForLevel(currentLevel)) * 100
|
||||
val xpLeftstr = formatNumber(xpLeft)
|
||||
xpWidget.xpLeftLabel.text = formatHtmlLabelText("XP Left: ", primaryColor, xpLeftstr, secondaryColor)
|
||||
if(COMBAT_SKILLS.contains(xpWidget.skillId)) {
|
||||
if(LootTrackerView.lastConfirmedKillNpcId != -1 && npcHitpointsMap.isNotEmpty()) {
|
||||
val npcHP = npcHitpointsMap[LootTrackerView.lastConfirmedKillNpcId]
|
||||
val xpPerKill = when (xpWidget.skillId) {
|
||||
3 -> playerXPMultiplier * (npcHP ?: 1) // Hitpoints
|
||||
else -> playerXPMultiplier * (npcHP ?: 1) * 4 // Combat XP for other skills
|
||||
}
|
||||
val remainingKills = xpLeft / xpPerKill
|
||||
xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Kills: ", primaryColor, remainingKills.toString(), secondaryColor)
|
||||
}
|
||||
} else {
|
||||
if(xpGainedSinceLastUpdate == 0)
|
||||
xpGainedSinceLastUpdate = 1 // Avoid possible divide by 0
|
||||
val remainingActions = (xpLeft / xpGainedSinceLastUpdate).coerceAtLeast(1)
|
||||
xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Actions: ", primaryColor, remainingActions.toString(), secondaryColor)
|
||||
}
|
||||
}
|
||||
|
||||
val formattedXp = formatNumber(xpWidget.totalXpGained)
|
||||
xpWidget.xpGainedLabel.text = formatHtmlLabelText("XP Gained: ", primaryColor, formattedXp, secondaryColor)
|
||||
|
||||
// Update the progress bar with current level, progress, and next level
|
||||
xpWidget.progressBar.updateProgress(progress, currentLevel, if (currentLevel < 99) currentLevel + 1 else 99, focusedView == VIEW_NAME)
|
||||
|
||||
xpWidget.previousXp = xp
|
||||
if (focusedView == VIEW_NAME)
|
||||
xpWidget.container.repaint()
|
||||
}
|
||||
|
||||
|
||||
private fun updateTotalXPWidget(xpGainedSinceLastUpdate: Int) {
|
||||
val totalXPWidget = totalXPWidget ?: return
|
||||
totalXPWidget.totalXpGained += xpGainedSinceLastUpdate
|
||||
val formattedXp = formatNumber(totalXPWidget.totalXpGained)
|
||||
totalXPWidget.xpGainedLabel.text = formatHtmlLabelText("Gained: ", primaryColor, formattedXp, secondaryColor)
|
||||
if (focusedView == VIEW_NAME)
|
||||
totalXPWidget.container.repaint()
|
||||
}
|
||||
|
||||
|
||||
fun resetXPTracker(xpTrackerView: JPanel) {
|
||||
xpTrackerView.removeAll()
|
||||
val popupMenu = createResetMenu()
|
||||
|
||||
xpTrackerView.add(Box.createVerticalStrut(5))
|
||||
xpTrackerView.add(createTotalWidgetContainer(popupMenu))
|
||||
xpTrackerView.add(Box.createVerticalStrut(5))
|
||||
|
||||
initialXP.clear()
|
||||
xpWidgets.clear()
|
||||
|
||||
xpTrackerView.revalidate()
|
||||
if (focusedView == VIEW_NAME) {
|
||||
xpTrackerView.repaint()
|
||||
}
|
||||
}
|
||||
|
||||
fun createTotalXPWidget(): XPWidget {
|
||||
val widgetPanel = WidgetPanel(TOTAL_XP_WIDGET_SIZE.width, TOTAL_XP_WIDGET_SIZE.height, addDefaultPadding = false)
|
||||
|
||||
val iconContainer = createIconContainer(getBufferedImageFromSprite(API.GetSprite(LVL_ICON)))
|
||||
|
||||
val textPanel = JPanel(GridLayout(2, 1, 5, 0)).apply {
|
||||
background = WIDGET_COLOR
|
||||
}
|
||||
|
||||
val xpGainedLabel = createMetricLabel("Gained:")
|
||||
val xpPerHourLabel = createMetricLabel("XP /hr:")
|
||||
|
||||
textPanel.add(xpGainedLabel)
|
||||
textPanel.add(xpPerHourLabel)
|
||||
|
||||
widgetPanel.add(iconContainer, BorderLayout.WEST)
|
||||
widgetPanel.add(textPanel, BorderLayout.CENTER)
|
||||
|
||||
return XPWidget(
|
||||
skillId = -1,
|
||||
container = widgetPanel,
|
||||
xpGainedLabel = xpGainedLabel,
|
||||
xpLeftLabel = createMetricLabel("XP Left:"),
|
||||
xpPerHourLabel = xpPerHourLabel,
|
||||
progressBar = ProgressBar(0.0, Color.BLACK), // Unused
|
||||
totalXpGained = 0,
|
||||
startTime = System.currentTimeMillis(),
|
||||
previousXp = 0,
|
||||
actionsRemainingLabel = JLabel().apply { font = widgetFont },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun createXPTrackerView() {
|
||||
val widgetViewPanel = BaseView(VIEW_NAME, addDefaultSpacing = false).apply {
|
||||
add(Box.createVerticalStrut(5))
|
||||
}
|
||||
|
||||
val popupMenu = createResetMenu()
|
||||
widgetViewPanel.add(createTotalWidgetContainer(popupMenu))
|
||||
widgetViewPanel.add(Box.createVerticalStrut(5))
|
||||
|
||||
xpTrackerView = widgetViewPanel
|
||||
|
||||
// Preload skill icons to avoid first-drop lag
|
||||
try {
|
||||
for (i in 0 until 24) {
|
||||
getSkillIcon(i)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Ignore preload errors; fallback at use time
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun createResetMenu(): JPopupMenu {
|
||||
val popupMenu = PopupMenuComponent()
|
||||
|
||||
val resetItem = createMenuItem("Reset Tracker")
|
||||
popupMenu.add(resetItem)
|
||||
|
||||
resetItem.addActionListener { plugin.registerDrawAction { resetXPTracker(xpTrackerView!!) } }
|
||||
return popupMenu
|
||||
}
|
||||
|
||||
fun removeXPWidgetMenu(toRemove: Container, skillId: Int): JPopupMenu {
|
||||
val popupMenu = PopupMenuComponent()
|
||||
|
||||
val resetItem = createMenuItem("Reset")
|
||||
popupMenu.add(resetItem)
|
||||
|
||||
val removeItem = createMenuItem("Remove")
|
||||
popupMenu.add(removeItem)
|
||||
|
||||
resetItem.addActionListener {
|
||||
xpWidgets[skillId]?.let { widget ->
|
||||
// Baseline at current XP and clear per-widget counters
|
||||
initialXP[skillId] = widget.previousXp
|
||||
widget.totalXpGained = 0
|
||||
widget.startTime = System.currentTimeMillis()
|
||||
|
||||
// Recompute labels/progress for current XP without adding totals
|
||||
updateWidget(widget, widget.previousXp)
|
||||
}
|
||||
}
|
||||
|
||||
removeItem.addActionListener {
|
||||
// Reset the per-skill baseline to the current XP so next widget starts fresh
|
||||
xpWidgets[skillId]?.let { widget ->
|
||||
initialXP[skillId] = widget.previousXp
|
||||
}
|
||||
// Remove widget container and following spacer if present
|
||||
xpTrackerView?.let { parent ->
|
||||
val components = parent.components
|
||||
val toRemoveIndex = components.indexOf(toRemove)
|
||||
if (toRemoveIndex >= 0 && toRemoveIndex < components.size - 1) {
|
||||
val nextComponent = components[toRemoveIndex + 1]
|
||||
if (nextComponent is Box.Filler) {
|
||||
parent.remove(nextComponent)
|
||||
}
|
||||
}
|
||||
parent.remove(toRemove)
|
||||
xpWidgets.remove(skillId)
|
||||
parent.revalidate()
|
||||
if (focusedView == VIEW_NAME) parent.repaint()
|
||||
}
|
||||
}
|
||||
return popupMenu
|
||||
}
|
||||
|
||||
|
||||
fun createXPWidget(skillId: Int, previousXp: Int): XPWidget {
|
||||
val widgetPanel = WidgetPanel(WIDGET_SIZE.width, WIDGET_SIZE.height, addDefaultPadding = false)
|
||||
|
||||
val iconContainer = createIconContainer(getSkillIcon(skillId))
|
||||
|
||||
val textPanel = JPanel(GridLayout(2, 2, 5, 0)).apply {
|
||||
background = WIDGET_COLOR
|
||||
}
|
||||
|
||||
val xpGainedLabel = createMetricLabel("XP Gained:")
|
||||
val xpLeftLabel = createMetricLabel("XP Left:", "0K")
|
||||
val xpPerHourLabel = createMetricLabel("XP /hr:")
|
||||
val actionsTitle = if (COMBAT_SKILLS.contains(skillId)) "Kills:" else "Actions:"
|
||||
val actionsLabel = createMetricLabel(actionsTitle)
|
||||
|
||||
val progressBar = ProgressBar(0.0, getProgressBarColor(skillId)).apply {
|
||||
preferredSize = Dimension(160, 22)
|
||||
minimumSize = preferredSize
|
||||
maximumSize = preferredSize
|
||||
}
|
||||
|
||||
val progressPanel = JPanel(BorderLayout()).apply {
|
||||
background = WIDGET_COLOR
|
||||
add(progressBar, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
textPanel.add(xpGainedLabel)
|
||||
textPanel.add(xpLeftLabel)
|
||||
textPanel.add(xpPerHourLabel)
|
||||
textPanel.add(actionsLabel)
|
||||
|
||||
widgetPanel.add(iconContainer, BorderLayout.WEST)
|
||||
widgetPanel.add(textPanel, BorderLayout.CENTER)
|
||||
widgetPanel.add(progressPanel, BorderLayout.SOUTH)
|
||||
|
||||
widgetPanel.revalidate()
|
||||
if(focusedView == VIEW_NAME)
|
||||
widgetPanel.repaint()
|
||||
|
||||
return XPWidget(
|
||||
skillId = skillId,
|
||||
container = widgetPanel,
|
||||
xpGainedLabel = xpGainedLabel,
|
||||
xpLeftLabel = xpLeftLabel,
|
||||
xpPerHourLabel = xpPerHourLabel,
|
||||
progressBar = progressBar,
|
||||
totalXpGained = 0,
|
||||
actionsRemainingLabel = actionsLabel,
|
||||
startTime = System.currentTimeMillis(),
|
||||
previousXp = previousXp
|
||||
)
|
||||
}
|
||||
|
||||
fun wrappedWidget(component: Component, padding: Int = 7): Container {
|
||||
val outerPanelSize = Dimension(
|
||||
component.preferredSize.width + 2 * padding,
|
||||
component.preferredSize.height + 2 * padding
|
||||
)
|
||||
val outerPanel = JPanel(GridBagLayout()).apply {
|
||||
background = WIDGET_COLOR
|
||||
preferredSize = outerPanelSize
|
||||
maximumSize = outerPanelSize
|
||||
minimumSize = outerPanelSize
|
||||
}
|
||||
val innerPanel = JPanel(BorderLayout()).apply {
|
||||
background = WIDGET_COLOR
|
||||
preferredSize = component.preferredSize
|
||||
maximumSize = component.preferredSize
|
||||
minimumSize = component.preferredSize
|
||||
add(component, BorderLayout.CENTER)
|
||||
}
|
||||
val gbc = GridBagConstraints().apply {
|
||||
anchor = GridBagConstraints.CENTER
|
||||
}
|
||||
outerPanel.add(innerPanel, gbc)
|
||||
return outerPanel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class XPWidget(
|
||||
val container: Container,
|
||||
val skillId: Int,
|
||||
val xpGainedLabel: JLabel,
|
||||
val xpLeftLabel: JLabel,
|
||||
val xpPerHourLabel: JLabel,
|
||||
val actionsRemainingLabel: JLabel,
|
||||
val progressBar: ProgressBar,
|
||||
var totalXpGained: Int = 0,
|
||||
var startTime: Long = System.currentTimeMillis(),
|
||||
var previousXp: Int = 0
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue