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 1acfa8b..029c7b3 100644 Binary files a/panel-preview.png and b/panel-preview.png differ diff --git a/src/main/java/com/afkstatstracker/AfkStatsTrackerPanel.java b/src/main/java/com/afkstatstracker/AfkStatsTrackerPanel.java index 21fe39d..60fbc85 100644 --- a/src/main/java/com/afkstatstracker/AfkStatsTrackerPanel.java +++ b/src/main/java/com/afkstatstracker/AfkStatsTrackerPanel.java @@ -2,6 +2,7 @@ import java.awt.BorderLayout; import java.awt.Color; +import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; @@ -17,17 +18,22 @@ import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.util.Collections; +import java.util.List; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JLabel; +import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; +import javax.swing.ImageIcon; import javax.swing.SwingConstants; import javax.swing.Timer; import net.runelite.client.ui.ColorScheme; +import net.runelite.client.util.ImageUtil; import net.runelite.client.ui.PluginPanel; public class AfkStatsTrackerPanel extends PluginPanel @@ -40,6 +46,8 @@ public class AfkStatsTrackerPanel extends PluginPanel private JButton stopButton; private JLabel consistencyValueLabel; private JLabel avgIntervalValueLabel; + private JLabel sessionElapsedLabel; + private JLabel sessionClicksLabel; private ConsistencyIndicator consistencyIndicator; private IntervalIndicator intervalIndicator; @@ -104,6 +112,7 @@ public AfkStatsTrackerPanel(AfkStatsTrackerPlugin plugin, SessionHistoryManager consistencyIndicator = new ConsistencyIndicator(); intervalIndicator = new IntervalIndicator(); + statsPanel.add(createSessionCard()); statsPanel.add(consistencyPanel); statsPanel.add(consistencyIndicator); statsPanel.add(avgIntervalPanel); @@ -142,6 +151,38 @@ private JPanel createStatCard(String title, String tooltip) return card; } + private JPanel createSessionCard() + { + JPanel card = new JPanel(new BorderLayout()); + card.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(1, 0, 0, 0, ColorScheme.DARK_GRAY_COLOR), + BorderFactory.createEmptyBorder(8, 5, 8, 5) + )); + card.setToolTipText("Elapsed time and click count for the current session"); + + JLabel titleLabel = new JLabel("Session"); + titleLabel.setForeground(Color.GRAY); + card.add(titleLabel, BorderLayout.NORTH); + + JPanel valueRow = new JPanel(); + valueRow.setLayout(new BoxLayout(valueRow, BoxLayout.X_AXIS)); + + sessionElapsedLabel = new JLabel("0s"); + sessionElapsedLabel.setFont(sessionElapsedLabel.getFont().deriveFont(Font.BOLD, 20f)); + sessionElapsedLabel.setAlignmentY(Component.BOTTOM_ALIGNMENT); + + sessionClicksLabel = new JLabel(" · 0 clicks"); + sessionClicksLabel.setForeground(Color.GRAY); + sessionClicksLabel.setAlignmentY(Component.BOTTOM_ALIGNMENT); + + valueRow.add(sessionElapsedLabel); + valueRow.add(sessionClicksLabel); + card.add(valueRow, BorderLayout.CENTER); + + card.setMaximumSize(new Dimension(Integer.MAX_VALUE, card.getPreferredSize().height)); + return card; + } + private JPanel createHistorySection() { JPanel section = new JPanel(new BorderLayout()); @@ -183,13 +224,16 @@ public void refreshHistoryPanel() { historyContainer.removeAll(); - for (Session session : sessionHistoryManager.getSessions()) + List 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 0000000..ddc01a6 Binary files /dev/null and b/src/main/resources/com/afkstatstracker/copy.png differ diff --git a/src/main/resources/com/afkstatstracker/delete.png b/src/main/resources/com/afkstatstracker/delete.png new file mode 100644 index 0000000..128b4ce Binary files /dev/null and b/src/main/resources/com/afkstatstracker/delete.png differ 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()); + } +}