From 8c872fbacc5d041983694c1dfc9499445e0fa879 Mon Sep 17 00:00:00 2001 From: Reasel Date: Fri, 12 Jun 2026 18:51:55 -0700 Subject: [PATCH] feat: click gating and panel polish --- README.md | 3 + panel-preview.png | Bin 5705 -> 6655 bytes .../afkstatstracker/AfkStatsTrackerPanel.java | 94 +++++++++++++----- .../AfkStatsTrackerPlugin.java | 31 +++++- .../afkstatstracker/DurationFormatter.java | 26 +++++ .../MouseClickCounterListener.java | 19 ++-- .../resources/com/afkstatstracker/copy.png | Bin 0 -> 218 bytes .../resources/com/afkstatstracker/delete.png | Bin 0 -> 215 bytes .../DurationFormatterTest.java | 43 ++++++++ .../MouseClickCounterListenerTest.java | 37 +++++++ 10 files changed, 213 insertions(+), 40 deletions(-) create mode 100644 src/main/java/com/afkstatstracker/DurationFormatter.java create mode 100644 src/main/resources/com/afkstatstracker/copy.png create mode 100644 src/main/resources/com/afkstatstracker/delete.png create mode 100644 src/test/java/com/afkstatstracker/DurationFormatterTest.java create mode 100644 src/test/java/com/afkstatstracker/MouseClickCounterListenerTest.java diff --git a/README.md b/README.md index d10878c..43a400d 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,12 @@ This RuneLite plugin tracks mouse clicks during AFK sessions in Old School RuneS Tracked Stats: - Consistency: A score from 0 to 100 showing how regular your click timing is. Higher scores mean more consistent intervals. - Average Click Interval: The average time in milliseconds between clicks. +- Session: Live elapsed time and click count while a session is running. Purpose: To track afk metrics to compare between activies and methods. Similar to tracking DPS and Kills/hr. +Session history shows your most recent sessions first. Click a session name to rename it. Sessions can be copied to clipboard or deleted (with confirmation). + ## Wiki Guide See the [AFK Activity Tracker Guide](https://oldschool.runescape.wiki/w/Guide:AFK_Activity_Tracker) on the Old School RuneScape Wiki for a detailed breakdown of the tracked metrics, interactive scatter plots comparing activities, and aggregated averages across activity groups like Bankstanding, Fishing, and Salvaging. diff --git a/panel-preview.png b/panel-preview.png index 1acfa8b100ce4749f8c6fb791a2ad261874fd029..029c7b3fed785aa6de6a9b3065bad15f36046c66 100644 GIT binary patch literal 6655 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 literal 5705 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 P3(~%6058+H|M sessions = sessionHistoryManager.getSessions(); + Collections.reverse(sessions); + + for (Session session : sessions) { historyContainer.add(createSessionRow(session)); historyContainer.add(Box.createRigidArea(new Dimension(0, 3))); } - if (sessionHistoryManager.getSessions().isEmpty()) + if (sessions.isEmpty()) { JLabel emptyLabel = new JLabel("No sessions recorded"); emptyLabel.setForeground(Color.GRAY); @@ -235,31 +279,15 @@ public void mouseClicked(MouseEvent e) statsLabel.setForeground(Color.GRAY); // Copy icon - JLabel copyIcon = createHoverIcon("\uD83D\uDCCB", "Copy session stats"); + JLabel copyIcon = createHoverIcon( + new ImageIcon(ImageUtil.loadImageResource(AfkStatsTrackerPanel.class, "copy.png")), + "Copy session stats"); copyIcon.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { - long durationMs = session.getEndTime() - session.getStartTime(); - long totalSeconds = durationMs / 1000; - long hours = totalSeconds / 3600; - long minutes = (totalSeconds % 3600) / 60; - long seconds = totalSeconds % 60; - - String lengthStr; - if (hours > 0) - { - lengthStr = String.format("%dh %dm %ds", hours, minutes, seconds); - } - else if (minutes > 0) - { - lengthStr = String.format("%dm %ds", minutes, seconds); - } - else - { - lengthStr = String.format("%ds", seconds); - } + String lengthStr = DurationFormatter.format(session.getEndTime() - session.getStartTime()); String text = String.format( "Session: %s\nLength: %s\nConsistency: %d\nAvg Interval: %.0fms\nClicks: %d", @@ -274,14 +302,24 @@ else if (minutes > 0) }); // Delete icon - JLabel deleteIcon = createHoverIcon("\uD83D\uDDD1", "Delete session"); + JLabel deleteIcon = createHoverIcon( + new ImageIcon(ImageUtil.loadImageResource(AfkStatsTrackerPanel.class, "delete.png")), + "Delete session"); deleteIcon.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { - sessionHistoryManager.deleteSession(session.getId()); - refreshHistoryPanel(); + int choice = JOptionPane.showConfirmDialog( + AfkStatsTrackerPanel.this, + "Delete \"" + session.getName() + "\"?", + "Delete Session", + JOptionPane.YES_NO_OPTION); + if (choice == JOptionPane.YES_OPTION) + { + sessionHistoryManager.deleteSession(session.getId()); + refreshHistoryPanel(); + } } }); @@ -303,9 +341,9 @@ public void mouseClicked(MouseEvent e) return row; } - private JLabel createHoverIcon(String text, String tooltip) + private JLabel createHoverIcon(ImageIcon imageIcon, String tooltip) { - JLabel icon = new JLabel(text) + JLabel icon = new JLabel(imageIcon) { private boolean hovered = false; @@ -394,6 +432,8 @@ public void keyPressed(KeyEvent e) public void updateStats() { + sessionElapsedLabel.setText(DurationFormatter.format(plugin.getSessionElapsedMs())); + sessionClicksLabel.setText(" · " + plugin.getClickCount() + " clicks"); int consistency = (int) plugin.getConsistency(); consistencyValueLabel.setText(String.valueOf(consistency)); consistencyIndicator.setValue(consistency); diff --git a/src/main/java/com/afkstatstracker/AfkStatsTrackerPlugin.java b/src/main/java/com/afkstatstracker/AfkStatsTrackerPlugin.java index 32fc116..a5c1031 100644 --- a/src/main/java/com/afkstatstracker/AfkStatsTrackerPlugin.java +++ b/src/main/java/com/afkstatstracker/AfkStatsTrackerPlugin.java @@ -41,6 +41,7 @@ public class AfkStatsTrackerPlugin extends Plugin private NavigationButton navButton; private long startTime; + private long endTime; private boolean isTracking = false; @Inject @@ -81,12 +82,13 @@ public void save(String json) navButton = NavigationButton.builder() .tooltip("AFK Stats Tracker") .icon(ImageUtil.loadImageResource(getClass(), "icon.png")) // Need to add icon + .priority(Integer.MAX_VALUE) .panel(panel) .build(); clientToolbar.addNavigation(navButton); - mouseListener = new MouseClickCounterListener(client); + mouseListener = new MouseClickCounterListener(); mouseManager.registerMouseListener(mouseListener); } @@ -106,9 +108,15 @@ protected void shutDown() throws Exception public void startSession() { + if (isTracking) + { + return; + } + mouseListener.resetMouseClickCounterListener(); startTime = System.currentTimeMillis(); isTracking = true; + mouseManager.registerMouseListener(mouseListener); } public void stopSession() @@ -118,7 +126,8 @@ public void stopSession() return; } - long endTime = System.currentTimeMillis(); + mouseManager.unregisterMouseListener(mouseListener); + endTime = System.currentTimeMillis(); String id = UUID.randomUUID().toString(); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); String name = "Session " + dateFormat.format(new Date(startTime)); @@ -137,6 +146,24 @@ public void stopSession() isTracking = false; } + public boolean isTracking() + { + return isTracking; + } + + public long getSessionElapsedMs() + { + if (startTime == 0) + { + return 0; + } + if (isTracking) + { + return System.currentTimeMillis() - startTime; + } + return endTime - startTime; + } + public long getConsistency() { return computeConsistency(mouseListener.getClickCounter()); diff --git a/src/main/java/com/afkstatstracker/DurationFormatter.java b/src/main/java/com/afkstatstracker/DurationFormatter.java new file mode 100644 index 0000000..4a1bb67 --- /dev/null +++ b/src/main/java/com/afkstatstracker/DurationFormatter.java @@ -0,0 +1,26 @@ +package com.afkstatstracker; + +public final class DurationFormatter +{ + private DurationFormatter() + { + } + + public static String format(long durationMs) + { + long totalSeconds = Math.max(0, durationMs) / 1000; + long hours = totalSeconds / 3600; + long minutes = (totalSeconds % 3600) / 60; + long seconds = totalSeconds % 60; + + if (hours > 0) + { + return String.format("%dh %dm %ds", hours, minutes, seconds); + } + if (minutes > 0) + { + return String.format("%dm %ds", minutes, seconds); + } + return String.format("%ds", seconds); + } +} diff --git a/src/main/java/com/afkstatstracker/MouseClickCounterListener.java b/src/main/java/com/afkstatstracker/MouseClickCounterListener.java index 17cb53e..207ecff 100644 --- a/src/main/java/com/afkstatstracker/MouseClickCounterListener.java +++ b/src/main/java/com/afkstatstracker/MouseClickCounterListener.java @@ -3,17 +3,11 @@ import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.List; -import net.runelite.api.Client; import net.runelite.client.input.MouseAdapter; public class MouseClickCounterListener extends MouseAdapter { private final List clickTimestamps = new ArrayList<>(); - private final Client client; - MouseClickCounterListener(Client client) - { - this.client = client; - } @Override public MouseEvent mousePressed(MouseEvent mouseEvent) @@ -22,15 +16,18 @@ public MouseEvent mousePressed(MouseEvent mouseEvent) return mouseEvent; } - public List getClickCounter() { return this.clickTimestamps; } + public synchronized List getClickCounter() + { + return new ArrayList<>(clickTimestamps); + } - public void addClick() + public synchronized void addClick() { - this.clickTimestamps.add(System.currentTimeMillis()); + clickTimestamps.add(System.currentTimeMillis()); } - public void resetMouseClickCounterListener() + public synchronized void resetMouseClickCounterListener() { this.clickTimestamps.clear(); } -} \ No newline at end of file +} diff --git a/src/main/resources/com/afkstatstracker/copy.png b/src/main/resources/com/afkstatstracker/copy.png new file mode 100644 index 0000000000000000000000000000000000000000..ddc01a62404d189baabbb35b8546acece95dce73 GIT binary patch literal 218 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`3q4&NLn`8y2A$+>FyMK7((8`| zXD8$D*B{dq4y9qbj%k@(C-w&I}~;|G;I!()4=AK0xl zV?w94zxyIS8Jl-|l_ndcrtu^`;hz41kD-4jFwOrnfoYa}5%ZP8bwJi1YyX1D z2@kKDo+@=cymy<{hhr9@3px^-wl(hAwa$UFJK^E~`Cp~&cYNooJ7x4!Q-1UDUv{TE S=T8SZmci52&t;ucLK6U5>RnR+ literal 0 HcmV?d00001 diff --git a/src/main/resources/com/afkstatstracker/delete.png b/src/main/resources/com/afkstatstracker/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..128b4cec08caca4cc30aef129c34d242386ba9f2 GIT binary patch literal 215 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`^E_P~Ln`9721RqV7zli3d)-^M z|Es~jZ!NWfFD7nLzP+$5!qq@PLvf~LniWsJ!gCv|=XJWlUh-e&Ix*e*y>;oThlY15 P(4`EXu6{1-oD!M<6~|eO literal 0 HcmV?d00001 diff --git a/src/test/java/com/afkstatstracker/DurationFormatterTest.java b/src/test/java/com/afkstatstracker/DurationFormatterTest.java new file mode 100644 index 0000000..4341624 --- /dev/null +++ b/src/test/java/com/afkstatstracker/DurationFormatterTest.java @@ -0,0 +1,43 @@ +package com.afkstatstracker; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class DurationFormatterTest +{ + @Test + public void testZero() + { + assertEquals("0s", DurationFormatter.format(0)); + } + + @Test + public void testSubSecondRoundsDown() + { + assertEquals("0s", DurationFormatter.format(999)); + } + + @Test + public void testSecondsOnly() + { + assertEquals("45s", DurationFormatter.format(45_000)); + } + + @Test + public void testMinutesAndSeconds() + { + assertEquals("12m 34s", DurationFormatter.format(754_000)); + } + + @Test + public void testHoursMinutesSeconds() + { + assertEquals("2h 5m 9s", DurationFormatter.format(7_509_000)); + } + + @Test + public void testNegativeClampsToZero() + { + assertEquals("0s", DurationFormatter.format(-5_000)); + } +} diff --git a/src/test/java/com/afkstatstracker/MouseClickCounterListenerTest.java b/src/test/java/com/afkstatstracker/MouseClickCounterListenerTest.java new file mode 100644 index 0000000..01d93d5 --- /dev/null +++ b/src/test/java/com/afkstatstracker/MouseClickCounterListenerTest.java @@ -0,0 +1,37 @@ +package com.afkstatstracker; + +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class MouseClickCounterListenerTest +{ + @Test + public void testAddClickRecordsTimestamp() + { + MouseClickCounterListener listener = new MouseClickCounterListener(); + listener.addClick(); + listener.addClick(); + assertEquals(2, listener.getClickCounter().size()); + } + + @Test + public void testResetClearsTimestamps() + { + MouseClickCounterListener listener = new MouseClickCounterListener(); + listener.addClick(); + listener.resetMouseClickCounterListener(); + assertTrue(listener.getClickCounter().isEmpty()); + } + + @Test + public void testGetClickCounterReturnsDefensiveCopy() + { + MouseClickCounterListener listener = new MouseClickCounterListener(); + listener.addClick(); + List copy = listener.getClickCounter(); + copy.clear(); + assertEquals(1, listener.getClickCounter().size()); + } +}