First commit of tasks-tracker-plugin-master

This commit is contained in:
2025-10-27 08:31:47 +08:00
parent b80f09a4ea
commit a5aab68ea4
121 changed files with 5353 additions and 3 deletions

15
.gitignore vendored
View File

@@ -21,6 +21,15 @@ group-ironmen-tracker-master/bin/
group-ironmen-tracker-master/spotless/ group-ironmen-tracker-master/spotless/
tasks-tracker-plugin-master/* tasks-tracker-plugin-master/.gradle
tasks-tracker-plugin-master/build
tasks-tracker-plugin-master/.idea/
tasks-tracker-plugin-master/.project
tasks-tracker-plugin-master/.settings/
tasks-tracker-plugin-master/.classpath
tasks-tracker-plugin-master/nbactions.xml
tasks-tracker-plugin-master/nb-configuration.xml
tasks-tracker-plugin-master/nbproject/
tasks-tracker-plugin-master/.run
tasks-tracker-plugin-master/bin
tasks-tracker-plugin-master/**/FileDataStoreReader.java

12
tasks-tracker-plugin-master/.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
.gradle
build
.idea/
.project
.settings/
.classpath
nbactions.xml
nb-configuration.xml
nbproject/
.run
bin
**/FileDataStoreReader.java

View File

@@ -0,0 +1,25 @@
BSD 2-Clause License
Copyright (c) 2021, Tyler Hardy
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,18 @@
# Tasks Tracker
![image](https://img.shields.io/endpoint?url=https://api.runelite.net/pluginhub/shields/installs/plugin/tasks-tracker)
![image](https://img.shields.io/endpoint?url=https://api.runelite.net/pluginhub/shields/rank/plugin/tasks-tracker)
Provides a RuneLite panel to track & filter tasks for Combat Achievements and Leagues.
## Support
Visit [our Discord](https://discord.gg/eCeKwhEzyK) for live support
## Features
* View tasks & progress in RuneLite panel without opening game UI
* Search tasks by text or filters
* Track tasks in a tracked list tab
* Export for external services
![image](https://user-images.githubusercontent.com/17709869/146846182-de573b11-ee53-482e-b3d2-a103f86d6089.png)
![image](https://user-images.githubusercontent.com/17709869/152713557-4f153acf-ff68-4299-852a-8898b5edb34c.gif)

View File

@@ -0,0 +1,33 @@
plugins {
id 'java'
}
repositories {
mavenLocal()
maven {
url = 'https://repo.runelite.net'
}
mavenCentral()
}
def runeLiteVersion = 'latest.release'
dependencies {
compileOnly group: 'net.runelite', name:'client', version: runeLiteVersion
compileOnly 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
testImplementation 'junit:junit:4.12'
testImplementation group: 'net.runelite', name:'client', version: runeLiteVersion
testImplementation group: 'net.runelite', name:'jshell', version: runeLiteVersion
testImplementation group: 'com.squareup.okhttp3', name: 'mockwebserver', version: '3.14.9'
}
group = 'net.reldo'
version = '1.0-SNAPSHOT'
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
options.release.set(11)
}

Binary file not shown.

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

252
tasks-tracker-plugin-master/gradlew vendored Normal file
View File

@@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
tasks-tracker-plugin-master/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,6 @@
displayName=Tasks Tracker
author=Reldo.net
support=https://github.com/osrs-reldo/tasks-tracker-plugin
description=Provides a panel to track league/combat tasks & export for external services
tags=combat,league,leagues,achievements,tasks,league3,leagues3,relic,fragment,unlock,unlocks
plugins=net.reldo.taskstracker.TasksTrackerPlugin

View File

@@ -0,0 +1 @@
rootProject.name = 'tasks-tracker'

View File

@@ -0,0 +1,41 @@
package net.reldo.taskstracker;
import java.awt.Color;
import java.net.URL;
public class HtmlUtil
{
public static String HTML_LINE_BREAK = "<br>";
public static String wrapWithHtml(String text)
{
return "<html>" + text + "</html>";
}
public static String wrapWithWrappingParagraph(String text, int width)
{
return "<p width=\"" + width + "\">" + text + "</p>";
}
public static String wrapWithBold(String text)
{
return "<b>" + text + "</b>";
}
public static String imageTag(URL url)
{
return "<img src=\"" + url + "\">";
}
public static String colorTag(String color, String text)
{
return "<span style=\"color: " + color + "\">" + text + "</span>";
}
public static String colorTag(Color color, String text)
{
String buf = Integer.toHexString(color.getRGB());
String hex = "#" + buf.substring(buf.length() - 6);
return colorTag(hex, text);
}
}

View File

@@ -0,0 +1,140 @@
package net.reldo.taskstracker;
import net.reldo.taskstracker.config.ConfigValues;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@ConfigGroup(TasksTrackerPlugin.CONFIG_GROUP_NAME)
public interface TasksTrackerConfig extends Config
{
@ConfigItem(
position = 10,
keyName = "untrackUponCompletion",
name = "Untrack Tasks Upon Completion",
description = "Configures whether completed tasks should also automatically untracked when the task is completed."
)
default boolean untrackUponCompletion()
{
return false;
}
@ConfigItem(
position = 11,
keyName = "filterPanelCollapsible",
name = "Filter Panels Collapsible",
description = "Shows button that allows filter panels to be hidden."
)
default boolean filterPanelCollapsible()
{
return true;
}
@ConfigItem(
position = 12,
keyName = "saveSubFilterState", //@todo generalise this to all sub-filters
name = "Save Filter State",
description = "Configures whether the state of area filters should be saved and recalled when switching task type or restarting the plugin.",
hidden = true
)
default boolean saveSubFilterState()
{
return true;
}
@ConfigItem(
position = 100,
keyName = "completedFilter",
name = "Completed Tasks Filter",
description = "Configures whether completed tasks should be displayed.",
hidden = true
)
default ConfigValues.CompletedFilterValues completedFilter()
{
return ConfigValues.CompletedFilterValues.COMPLETE_AND_INCOMPLETE;
}
@ConfigItem(
position = 101,
keyName = "trackedFilter",
name = "Tracked Tasks Filter",
description = "Configures whether tracked tasks should be displayed.",
hidden = true
)
default ConfigValues.TrackedFilterValues trackedFilter()
{
return ConfigValues.TrackedFilterValues.TRACKED_AND_UNTRACKED;
}
@ConfigItem(
position = 102,
keyName = "ignoredFilter",
name = "Ignored Tasks Filter",
description = "Configures whether ignored tasks should be displayed.",
hidden = true
)
default ConfigValues.IgnoredFilterValues ignoredFilter()
{
return ConfigValues.IgnoredFilterValues.NOT_IGNORED;
}
@ConfigItem(
position = 103,
keyName = "taskListTab",
name = "Selected Task List Tab",
description = "Configures the currently selected tab on the task list.",
hidden = true
)
default ConfigValues.TaskListTabs taskListTab()
{
return ConfigValues.TaskListTabs.ALL;
}
@ConfigItem(
position = 106,
keyName = "taskTypeJsonName",
name = "Task Type",
description = "Configures the task type which is displayed in the panel.",
hidden = true
)
default String taskTypeJsonName()
{
return "COMBAT";
}
@ConfigItem(
position = 109,
keyName = "dropdownFilter",
name = "Dropdown Filter",
description = "Configures the dropdown to filter tasks on.",
hidden = true
)
default String dropdownFilter()
{
return "";
}
@ConfigItem(
position = 110,
keyName = "sortCriteria",
name = "Sort Criteria",
description = "Configures the criteria to sort tasks on.",
hidden = true
)
default String sortCriteria()
{
return "Default";
}
@ConfigItem(
position = 111,
keyName = "sortDirection",
name = "Sort Direction",
description = "Configures the direction to sort tasks.",
hidden = true
)
default ConfigValues.SortDirections sortDirection()
{
return ConfigValues.SortDirections.ASCENDING;
}
}

View File

@@ -0,0 +1,584 @@
package net.reldo.taskstracker;
import com.google.gson.Gson;
import com.google.inject.Binder;
import com.google.inject.Provides;
import java.awt.Color;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.awt.image.BufferedImage;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.swing.JDialog;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.data.Export;
import net.reldo.taskstracker.data.LongSerializer;
import net.reldo.taskstracker.data.TasksSummary;
import net.reldo.taskstracker.data.TrackerConfigStore;
import net.reldo.taskstracker.data.jsondatastore.reader.DataStoreReader;
import net.reldo.taskstracker.data.jsondatastore.reader.HttpDataStoreReader;
import net.reldo.taskstracker.data.reldo.ReldoImport;
import net.reldo.taskstracker.data.task.TaskFromStruct;
import net.reldo.taskstracker.data.task.TaskService;
import net.reldo.taskstracker.data.task.TaskType;
import net.reldo.taskstracker.data.task.filters.FilterService;
import net.reldo.taskstracker.panel.TasksTrackerPluginPanel;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.Experience;
import net.runelite.api.GameState;
import net.runelite.api.Skill;
import net.runelite.api.events.CommandExecuted;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.StatChanged;
import net.runelite.api.events.VarbitChanged;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.chat.ChatMessageBuilder;
import net.runelite.client.chat.ChatMessageManager;
import net.runelite.client.chat.QueuedMessage;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.config.RuneScapeProfileType;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.events.ProfileChanged;
import net.runelite.client.game.SpriteManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.plugins.PluginManager;
import net.runelite.client.ui.ClientToolbar;
import net.runelite.client.ui.NavigationButton;
import net.runelite.client.util.ImageUtil;
import net.runelite.client.util.LinkBrowser;
@Slf4j
@PluginDescriptor(
name = "Tasks Tracker"
)
public class TasksTrackerPlugin extends Plugin
{
public static final String CONFIG_GROUP_NAME = "tasks-tracker";
public int[] playerSkills;
public String taskTextFilter;
public TasksTrackerPluginPanel pluginPanel;
private static final long VARP_UPDATE_THROTTLE_DELAY_MS = 7 * 1000;
private boolean forceUpdateVarpsFlag = false;
private Set<Integer> varpIdsToUpdate = new HashSet<>();
private long lastVarpUpdate = 0;
private NavigationButton navButton;
private RuneScapeProfileType currentProfileType;
private final Map<Skill, Integer> oldExperience = new EnumMap<>(Skill.class);
@Inject @Named("runelite.version") private String runeliteVersion;
@Inject private Gson gson;
@Inject private Client client;
@Inject private SpriteManager spriteManager;
@Inject private PluginManager pluginManager;
@Inject private ClientToolbar clientToolbar;
@Inject private ClientThread clientThread;
@Inject private ChatMessageManager chatMessageManager;
@Getter @Inject private ConfigManager configManager;
@Getter @Inject private TasksTrackerConfig config;
@Inject private TrackerConfigStore trackerConfigStore;
@Inject private TaskService taskService;
@Inject private FilterService filterService;
@Override
public void configure(Binder binder)
{
binder.bind(DataStoreReader.class).to(HttpDataStoreReader.class);
super.configure(binder);
}
@Provides
TasksTrackerConfig getConfig(ConfigManager configManager)
{
return configManager.getConfig(TasksTrackerConfig.class);
}
@Override
protected void startUp()
{
try
{
String taskTypeJsonName = config.taskTypeJsonName();
taskService.setTaskType(taskTypeJsonName);
}
catch (Exception ex)
{
log.error("error setting task type in startUp", ex);
}
forceUpdateVarpsFlag = false;
pluginPanel = new TasksTrackerPluginPanel(this, config, spriteManager, taskService);
boolean isLoggedIn = isLoggedInState(client.getGameState());
pluginPanel.setLoggedIn(isLoggedIn);
if (isLoggedIn)
{
forceUpdateVarpsFlag = true;
}
final BufferedImage icon = ImageUtil.loadImageResource(getClass(), "panel_icon.png");
navButton = NavigationButton.builder()
.tooltip("Task Tracker")
.icon(icon)
.priority(5)
.panel(pluginPanel)
.build();
clientToolbar.addNavigation(navButton);
log.info("Tasks Tracker started!");
}
@Override
protected void shutDown()
{
pluginPanel.hideLoggedInPanel();
pluginPanel = null;
taskService.clearTaskTypes();
clientToolbar.removeNavigation(navButton);
log.info("Tasks Tracker stopped!");
}
@Subscribe
public void onCommandExecuted(CommandExecuted commandExecuted)
{
if (!commandExecuted.getCommand().startsWith("tt")) return;
if (commandExecuted.getCommand().equalsIgnoreCase("tt-process-varp"))
{
String[] args = commandExecuted.getArguments();
if (args.length == 0) return;
try
{
int varpId = Integer.parseInt(args[0]);
log.debug("Processing varpId " + varpId);
processVarpAndUpdateTasks(varpId);
}
catch (NumberFormatException e)
{
log.debug("Invalid varpId, provide a valid integer");
}
}
}
@Subscribe
public void onVarbitChanged(VarbitChanged varbitChanged)
{
if (forceUpdateVarpsFlag || taskService.isTaskTypeChanged())
{
// Force update is coming on next game tick, so ignore varbit change events
return;
}
int varpId = varbitChanged.getVarpId();
if (!taskService.isVarpInCurrentTaskType(varpId))
{
return;
}
varpIdsToUpdate.add(varbitChanged.getVarpId());
}
@Subscribe
public void onConfigChanged(ConfigChanged configChanged)
{
if (!configChanged.getGroup().equals(CONFIG_GROUP_NAME))
{
return;
}
log.debug("onConfigChanged {} {}", configChanged.getKey(), configChanged.getNewValue());
if (configChanged.getKey().equals("untrackUponCompletion") && config.untrackUponCompletion())
{
forceVarpUpdate();
}
if (configChanged.getKey().equals("filterPanelCollapsible"))
{
SwingUtilities.invokeLater(pluginPanel::redraw);
}
}
@Subscribe
public void onGameStateChanged(GameStateChanged gameStateChanged)
{
log.debug("onGameStateChanged {}", gameStateChanged.getGameState().toString());
GameState newGameState = gameStateChanged.getGameState();
RuneScapeProfileType newProfileType = RuneScapeProfileType.getCurrent(client);
SwingUtilities.invokeLater(() -> pluginPanel.setLoggedIn(isLoggedInState(newGameState)));
// Logged in
if (newGameState == GameState.LOGGING_IN)
{
forceUpdateVarpsFlag = true;
}
// Changed game mode
if (isLoggedInState(newGameState) && currentProfileType != null && currentProfileType != newProfileType)
{
forceUpdateVarpsFlag = true;
}
currentProfileType = newProfileType;
}
private boolean isLoggedInState(GameState gameState)
{
return gameState == GameState.LOGGED_IN || gameState == GameState.HOPPING || gameState == GameState.LOADING;
}
@Subscribe
public void onGameTick(GameTick gameTick)
{
if (forceUpdateVarpsFlag || taskService.isTaskTypeChanged())
{
log.debug("forceUpdateVarpsFlag game tick {} {}", forceUpdateVarpsFlag, taskService.isTaskTypeChanged());
trackerConfigStore.loadCurrentTaskTypeFromConfig();
forceVarpUpdate();
SwingUtilities.invokeLater(() -> pluginPanel.redraw());
forceUpdateVarpsFlag = false;
taskService.setTaskTypeChanged(false);
}
// Flush throttled varp updates
long currentTimeEpoch = System.currentTimeMillis();
if (currentTimeEpoch - lastVarpUpdate > VARP_UPDATE_THROTTLE_DELAY_MS)
{
flushVarpUpdates(varpIdsToUpdate);
varpIdsToUpdate = new HashSet<>();
lastVarpUpdate = currentTimeEpoch;
}
}
@Subscribe
public void onStatChanged(StatChanged statChanged)
{
// @todo deprecate one of these, we don't need to track player skills twice.
// Cache current player skills
int[] newSkills = client.getRealSkillLevels();
boolean changed = !Arrays.equals(playerSkills, newSkills);
if (changed)
{
playerSkills = client.getRealSkillLevels();
}
final Skill skill = statChanged.getSkill();
// Modified from m0bilebtw's modification from Nightfirecat's virtual level ups plugin
final int xpAfter = client.getSkillExperience(skill);
final int levelAfter = Experience.getLevelForXp(xpAfter);
final int xpBefore = oldExperience.getOrDefault(skill, -1);
final int levelBefore = xpBefore == -1 ? -1 : Experience.getLevelForXp(xpBefore);
oldExperience.put(skill, xpAfter);
// Do not proceed if any of the following are true:
// * xpBefore == -1 (don't fire when first setting new known value)
// * xpAfter <= xpBefore (do not allow 200m -> 200m exp drops)
// * levelBefore >= levelAfter (stop if we're not actually reaching a new level)
// * levelAfter > MAX_REAL_LEVEL (stop if above 99)
if (xpBefore == -1 || xpAfter <= xpBefore || levelBefore >= levelAfter || levelAfter > Experience.MAX_REAL_LEVEL)
{
return;
}
// If we get here, 'skill' was leveled up!
SwingUtilities.invokeLater(() -> pluginPanel.taskListPanel.refreshTaskPanelsWithSkill(skill));
}
@Subscribe
public void onProfileChanged(ProfileChanged profileChanged)
{
final Optional<Plugin> taskTrackerPlugin = pluginManager.getPlugins().stream().filter(p -> p.getName().equals("Tasks Tracker")).findFirst();
if (taskTrackerPlugin.isPresent() && pluginManager.isPluginEnabled(taskTrackerPlugin.get()))
{
reloadTaskType();
}
}
public void refreshAllTasks()
{
SwingUtilities.invokeLater(() -> pluginPanel.refreshAllTasks());
}
public void reloadTaskType() {
taskService.clearTaskTypes();
filterService.clearFilterConfigs();
try {
String taskTypeJsonName = config.taskTypeJsonName();
taskService.setTaskType(taskTypeJsonName).thenAccept(isSet -> {
if (!isSet) {
return;
}
SwingUtilities.invokeLater(() ->
{
pluginPanel.redraw();
pluginPanel.refreshAllTasks();
});
});
} catch (Exception ex) {
log.error("error setting task type in reload", ex);
}
}
public void saveCurrentTaskTypeData()
{
log.debug("saveCurrentTaskTypeData");
trackerConfigStore.saveCurrentTaskTypeData();
}
public void openImportJsonDialog()
{
JOptionPane optionPane = new JOptionPane("Paste import data into the text field below to import task tracker data.", JOptionPane.INFORMATION_MESSAGE);
optionPane.setWantsInput(true);
JDialog inputDialog = optionPane.createDialog(this.pluginPanel, "Import Tasks Input");
inputDialog.setAlwaysOnTop(true);
inputDialog.setVisible(true);
if (optionPane.getInputValue().equals("") || optionPane.getInputValue().equals("uninitializedValue"))
{
this.showMessageBox("Import Tasks Error", "Input was empty so no data has been imported.", JOptionPane.ERROR_MESSAGE, false);
return;
}
String json = "";
ReldoImport reldoImport;
try
{
json = (String) optionPane.getInputValue();
reldoImport = this.gson.fromJson(json, ReldoImport.class);
}
catch (Exception ex)
{
this.showMessageBox("Import Tasks Error", "There was an issue importing task tracker data. " + ex.getMessage(), JOptionPane.ERROR_MESSAGE, false);
log.error("There was an issue importing task tracker data.", ex);
log.debug("reldoImport json: {}", json);
return;
}
if (!reldoImport.taskTypeName.equalsIgnoreCase(config.taskTypeJsonName()))
{
this.showMessageBox("Import Tasks Error", String.format("Wrong task type. Select the %s task type to import this data.", reldoImport.taskTypeName), JOptionPane.ERROR_MESSAGE, false);
return;
}
optionPane = new JOptionPane("Importing tasks will overwrite task tracker settings and cannot be undone. Are you sure you want to import tasks?", JOptionPane.WARNING_MESSAGE, JOptionPane.YES_NO_OPTION);
JDialog confirmDialog = optionPane.createDialog(this.pluginPanel, "Import Tasks Overwrite Confirmation");
confirmDialog.setAlwaysOnTop(true);
confirmDialog.setVisible(true);
Object selectedValue = optionPane.getValue();
if (selectedValue == null)
{
return;
}
if (selectedValue.equals(JOptionPane.YES_OPTION))
{
HashMap<Integer, TaskFromStruct> tasksById = new HashMap<>();
taskService.getTasks().forEach((task) -> tasksById.put(task.getIntParam("id"), task));
reldoImport.getTasks().forEach((id, reldoTaskSave) -> {
TaskFromStruct task = tasksById.get(id);
task.loadReldoSave(reldoTaskSave);
});
trackerConfigStore.saveCurrentTaskTypeData();
pluginPanel.redraw();
}
}
public void sendTotalsToChat()
{
TasksSummary summary = new TasksSummary(taskService.getTasks());
int trackedTasks = summary.trackedTasksCount;
int trackedPoints = summary.trackedTasksPoints;
final String message = new ChatMessageBuilder()
.append(Color.BLACK, String.format("Task Tracker - Tracked Tasks: %s | Tracked Points: %s", trackedTasks, trackedPoints))
.build();
chatMessageManager.queue(
QueuedMessage.builder()
.type(ChatMessageType.CONSOLE)
.runeLiteFormattedMessage(message)
.build());
}
public void copyJsonToClipboard()
{
clientThread.invokeLater(() -> {
// Not worried with this complexity on the client thread because it's from an infrequent button press
String json = getCurrentTaskTypeExportJson();
final StringSelection stringSelection = new StringSelection(json);
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(stringSelection, null);
String message = "Copied " + taskService.getCurrentTaskType().getName() + " data to clipboard!";
showMessageBox("Data Exported!", message, JOptionPane.INFORMATION_MESSAGE, true);
});
}
private void forceVarpUpdate()
{
log.debug("forceVarpUpdate");
processVarpAndUpdateTasks(null).thenAccept((processed) -> {
if (processed)
{
log.debug("forceVarpUpdate processed complete, saving");
saveCurrentTaskTypeData();
}
});
}
private void flushVarpUpdates(Set<Integer> varpIds)
{
log.debug("Flushing throttled varp updates {}", varpIds);
varpIds.forEach((id) -> processVarpAndUpdateTasks(id).thenAccept(processed -> {
if (processed)
{
log.debug("flushVarpUpdates processed complete, saving");
saveCurrentTaskTypeData();
}
}));
}
private CompletableFuture<Boolean> processTaskStatus(TaskFromStruct task)
{
CompletableFuture<Boolean> future = new CompletableFuture<>();
clientThread.invoke(() -> {
int taskId = task.getIntParam("id");
int varbitIndex = taskId / 32;
int bitIndex = taskId % 32;
try
{
int varpId = task.getTaskType().getTaskVarps().get(varbitIndex);
BigInteger varpValue = BigInteger.valueOf(client.getVarpValue(varpId));
boolean isTaskCompleted = varpValue.testBit(bitIndex);
task.setCompleted(isTaskCompleted);
if (isTaskCompleted && config.untrackUponCompletion())
{
task.setTracked(false);
}
log.debug("process taskFromStruct {} ({}) {}", task.getStringParam("name"), task.getIntParam("id"), isTaskCompleted);
future.complete(isTaskCompleted);
}
catch (Exception ex)
{
log.error("Error processing task status {}", taskId, ex);
future.completeExceptionally(ex);
}
});
return future;
}
/**
* Update task completion status. If no varpId is specified, it updates all tasks in the current task type
* @param varpId varp id to update (optional)
* @return An observable that emits true if all tasks were processed
*/
private CompletableFuture<Boolean> processVarpAndUpdateTasks(@Nullable Integer varpId)
{
log.info("processVarpAndUpdateTasks: " + (varpId != null ? varpId : "all"));
List<TaskFromStruct> tasks = varpId != null ?
taskService.getTasksFromVarpId(varpId) :
taskService.getTasks();
List<CompletableFuture<Boolean>> taskFutures = new ArrayList<>();
for (TaskFromStruct task : tasks)
{
CompletableFuture<Boolean> taskFuture = processTaskStatus(task);
taskFutures.add(taskFuture);
}
CompletableFuture<Void> allTasksFuture = CompletableFuture.allOf(taskFutures.toArray(new CompletableFuture[0]));
return allTasksFuture
.thenRun(() -> {
if (varpId != null)
{
SwingUtilities.invokeLater(() -> pluginPanel.taskListPanel.refreshMultipleTasks(tasks));
} else {
SwingUtilities.invokeLater(() -> pluginPanel.refreshAllTasks());
}
})
.thenApply(v -> true);
}
private String getCurrentTaskTypeExportJson()
{
TaskType taskType = taskService.getCurrentTaskType();
Gson gson = this.gson.newBuilder()
.excludeFieldsWithoutExposeAnnotation()
.registerTypeAdapter(float.class, new LongSerializer())
.create();
if (taskType == null)
{
String error = "Cannot export to JSON; no task type selected.";
log.error(error);
return error;
}
else
{
Export export = new Export(taskType, taskService.getTasks(), runeliteVersion, client);
return gson.toJson(export);
}
}
private void showMessageBox(final String title, final String message, int messageType, boolean showOpenLeagueTools)
{
SwingUtilities.invokeLater(() -> {
JOptionPane optionPane;
JDialog dialog;
if (showOpenLeagueTools)
{
String[] options = {"Open OS League Tools", "Ok"};
optionPane = new JOptionPane(message, messageType, JOptionPane.YES_NO_OPTION, null, options, options[1]);
}
else
{
optionPane = new JOptionPane(message, messageType);
}
dialog = optionPane.createDialog(pluginPanel, title);
dialog.setAlwaysOnTop(true);
dialog.setVisible(true);
Object selectedValue = optionPane.getValue();
if (selectedValue == null)
{
return;
}
if (selectedValue.equals("Open OS League Tools"))
{
LinkBrowser.browse("https://www.osleague.tools/tracker?open=import&tab=tasks");
}
});
}
}

View File

@@ -0,0 +1,40 @@
package net.reldo.taskstracker.config;
public class ConfigValues {
public enum CompletedFilterValues
{
COMPLETE_AND_INCOMPLETE,
COMPLETE,
INCOMPLETE;
}
public enum TrackedFilterValues
{
TRACKED_AND_UNTRACKED,
TRACKED,
UNTRACKED;
}
public enum IgnoredFilterValues
{
NOT_IGNORED,
IGNORED_AND_NOT_IGNORED,
IGNORED;
}
public enum TaskListTabs
{
TRACKED,
ALL,
CUSTOM;
}
public enum SortDirections
{
ASCENDING,
DESCENDING;
}
}

View File

@@ -0,0 +1,94 @@
package net.reldo.taskstracker.data;
import com.google.gson.annotations.Expose;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import lombok.Getter;
import net.reldo.taskstracker.data.task.ConfigTaskSave;
import net.reldo.taskstracker.data.task.TaskFromStruct;
import net.reldo.taskstracker.data.task.TaskType;
import net.reldo.taskstracker.quests.DiaryData;
import net.reldo.taskstracker.quests.QuestData;
import net.runelite.api.Actor;
import net.runelite.api.Client;
@Getter
public class Export
{
private final Client client;
@Expose private final QuestData quests;
@Expose private final DiaryData diaries;
@Expose private String displayName;
@Expose private final int runescapeVersion;
@Expose private final String runeliteVersion;
@Expose private final long timestamp;
@Expose private final String taskType;
@Expose private final HashMap<Integer, Integer> varbits;
@Expose private final HashMap<Integer, Integer> varps;
@Expose private final HashMap<String, ConfigTaskSave> tasks;
public Export(TaskType taskType, List<TaskFromStruct> tasks, String runeliteVersion, Client client)
{
this.client = client;
Actor localPlayer = client.getLocalPlayer();
if (localPlayer != null)
{
this.displayName = localPlayer.getName();
}
quests = new QuestData(client);
diaries = new DiaryData(client);
runescapeVersion = client.getRevision();
this.runeliteVersion = runeliteVersion;
timestamp = Instant.now().toEpochMilli();
this.taskType = taskType.getTaskJsonName();
varbits = getVarbits(taskType);
varps = getVarps(taskType);
this.tasks = getTaskSavesById(tasks);
}
private HashMap<Integer, Integer> getVarbits(TaskType taskType)
{
assert client.isClientThread();
HashMap<Integer, Integer> varbitValueMap = new HashMap<>();
for (int varbitId : taskType.getVarbits())
{
varbitValueMap.put(varbitId, client.getVarbitValue(varbitId));
}
return varbitValueMap;
}
public HashMap<Integer, Integer> getVarps(TaskType taskType)
{
assert client.isClientThread();
HashMap<Integer, Integer> varpValueMap = new HashMap<>();
for (int varpId : taskType.getTaskVarps())
{
varpValueMap.put(varpId, client.getVarpValue(varpId));
}
for (int varpId : taskType.getOtherVarps())
{
varpValueMap.put(varpId, client.getVarpValue(varpId));
}
return varpValueMap;
}
public HashMap<String, ConfigTaskSave> getTaskSavesById(List<TaskFromStruct> tasks)
{
HashMap<String, ConfigTaskSave> taskSavesById = new HashMap<>();
for (TaskFromStruct task : tasks)
{
if (task.getCompletedOn() == 0 && task.getIgnoredOn() == 0 && task.getTrackedOn() == 0)
{
continue;
}
taskSavesById.put(String.valueOf(task.getIntParam("id")), task.getSaveData());
}
return taskSavesById;
}
}

View File

@@ -0,0 +1,17 @@
package net.reldo.taskstracker.data;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
import java.text.DecimalFormat;
public class LongSerializer implements JsonSerializer<Long>
{
@Override
public JsonElement serialize(Long value, Type type, JsonSerializationContext jsonSerializationContext)
{
return new JsonPrimitive(new DecimalFormat("#").format(value));
}
}

View File

@@ -0,0 +1,24 @@
package net.reldo.taskstracker.data;
import java.util.Collection;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.data.task.TaskFromStruct;
@Slf4j
public class TasksSummary
{
public int trackedTasksCount = 0;
public int trackedTasksPoints = 0;
public TasksSummary(Collection<TaskFromStruct> tasks)
{
tasks.forEach(task -> {
if (task.isTracked()) {
trackedTasksCount++;
int points = task.getPoints();
log.debug("TasksSummary {} {}", task.getName(), points);
trackedTasksPoints += points;
}
});
}
}

View File

@@ -0,0 +1,94 @@
package net.reldo.taskstracker.data;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.TasksTrackerPlugin;
import net.reldo.taskstracker.data.task.TaskFromStruct;
import net.reldo.taskstracker.data.task.ConfigTaskSave;
import net.reldo.taskstracker.data.task.TaskService;
import net.reldo.taskstracker.data.task.TaskType;
import net.runelite.client.config.ConfigManager;
@Singleton
@Slf4j
public class TrackerConfigStore
{
public static final String CONFIG_TASKS_PREFIX = "tasks";
public static final String CONFIG_GROUP_PREFIX_SEPARATOR = "-";
public static final String CONFIG_GROUP_NAME = TasksTrackerPlugin.CONFIG_GROUP_NAME;
private final Gson customGson;
@Inject
private TaskService taskService;
@Inject
private ConfigManager configManager;
@Inject
public TrackerConfigStore(Gson gson)
{
this.customGson = gson.newBuilder()
.excludeFieldsWithoutExposeAnnotation()
.registerTypeAdapter(float.class, new LongSerializer())
.create();
}
public void loadCurrentTaskTypeFromConfig()
{
TaskType currentTaskType = taskService.getCurrentTaskType();
if (currentTaskType == null)
{
log.debug("loadTaskTypeFromConfig type is null, skipping");
return;
}
log.debug("loadTaskTypeFromConfig {}", currentTaskType.getName());
String configKey = getCurrentTaskTypeConfigKey();
String configJson = configManager.getRSProfileConfiguration(CONFIG_GROUP_NAME, configKey);
if (configJson == null)
{
log.debug("No save information for task type {}, not applying save", currentTaskType.getName());
return;
}
Type deserializeType = TypeToken.getParameterized(HashMap.class, Integer.class, ConfigTaskSave.class).getType();
try
{
HashMap<Integer, ConfigTaskSave> saveData = customGson.fromJson(configJson, deserializeType);
taskService.applySave(currentTaskType, saveData);
}
catch (JsonParseException ex)
{
log.error("{} {} json invalid. wiping saved data", CONFIG_GROUP_NAME, configKey, ex);
configManager.unsetRSProfileConfiguration(CONFIG_GROUP_NAME, configKey);
}
}
public void saveCurrentTaskTypeData()
{
log.debug("saveTaskTypeToConfig");
Map<Integer, ConfigTaskSave> saveDataByStructId = taskService.getTasks().stream()
.filter(task -> task.getCompletedOn() != 0 || task.getIgnoredOn() != 0 || task.getTrackedOn() != 0)
.collect(Collectors.toMap(
TaskFromStruct::getStructId,
TaskFromStruct::getSaveData,
(existing, replacement) -> existing,
HashMap::new
));
String configValue = this.customGson.toJson(saveDataByStructId);
String configKey = CONFIG_TASKS_PREFIX + CONFIG_GROUP_PREFIX_SEPARATOR + taskService.getCurrentTaskType().getTaskJsonName();
configManager.setRSProfileConfiguration(CONFIG_GROUP_NAME, configKey, configValue);
}
private String getCurrentTaskTypeConfigKey()
{
return CONFIG_TASKS_PREFIX + CONFIG_GROUP_PREFIX_SEPARATOR + taskService.getCurrentTaskType().getTaskJsonName();
}
}

View File

@@ -0,0 +1,50 @@
package net.reldo.taskstracker.data.jsondatastore;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.data.jsondatastore.reader.DataStoreReader;
import net.reldo.taskstracker.data.jsondatastore.types.FilterConfig;
import okhttp3.OkHttpClient;
@Singleton
@Slf4j
public class FilterDataClient
{
@Inject private ManifestClient manifestClient;
@Inject private OkHttpClient okHttpClient;
@Inject private Gson gson;
@Inject private DataStoreReader dataStoreReader;
public FilterDataClient()
{
log.debug("init filter data client");
}
public HashMap<String, FilterConfig> getFilterConfigs() throws Exception
{
log.debug("get filter configs");
try(InputStream stream = this.dataStoreReader.readFilterConfigs(this.manifestClient.getManifest().filterMetadata);
InputStreamReader responseReader = new InputStreamReader(stream, StandardCharsets.UTF_8))
{
Type listType = TypeToken.getParameterized(ArrayList.class, FilterConfig.class).getType();
List<FilterConfig> filterConfigs = this.gson.fromJson(responseReader, listType);
HashMap<String, FilterConfig> filterConfigsByConfigKey = new HashMap<>();
for (FilterConfig filterConfig : filterConfigs)
{
filterConfigsByConfigKey.put(filterConfig.getConfigKey(), filterConfig);
}
return filterConfigsByConfigKey;
}
}
}

View File

@@ -0,0 +1,6 @@
package net.reldo.taskstracker.data.jsondatastore;
public class JsonDataStore
{
public static String baseUrl = "https://raw.githubusercontent.com/osrs-reldo/task-json-store/refs/heads/main";
}

View File

@@ -0,0 +1,44 @@
package net.reldo.taskstracker.data.jsondatastore;
import com.google.common.io.CharStreams;
import com.google.gson.Gson;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.data.jsondatastore.reader.DataStoreReader;
import net.reldo.taskstracker.data.jsondatastore.types.Manifest;
import okhttp3.OkHttpClient;
@Singleton
@Slf4j
public class ManifestClient
{
@Inject private OkHttpClient okHttpClient;
@Inject private Gson gson;
@Inject private DataStoreReader dataStoreReader;
private static Manifest _manifest = null;
public ManifestClient()
{
log.debug("init manifestclient");
}
public Manifest getManifest() throws Exception
{
if (_manifest != null) {
return _manifest;
}
try(InputStream stream = this.dataStoreReader.readManifestData();
InputStreamReader responseReader = new InputStreamReader(stream, StandardCharsets.UTF_8))
{
String manifestJson = CharStreams.toString(responseReader); // ew, why not a stream? not working...
_manifest = this.gson.fromJson(manifestJson, Manifest.class);
log.debug("_manifest = " + _manifest);
return _manifest;
}
}
}

View File

@@ -0,0 +1,67 @@
package net.reldo.taskstracker.data.jsondatastore;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.data.jsondatastore.reader.DataStoreReader;
import net.reldo.taskstracker.data.jsondatastore.types.TaskDefinition;
import net.reldo.taskstracker.data.jsondatastore.types.TaskTypeDefinition;
import net.reldo.taskstracker.data.task.TaskType;
import net.runelite.api.Client;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.game.SpriteManager;
import okhttp3.OkHttpClient;
@Singleton
@Slf4j
public class TaskDataClient
{
@Inject private ManifestClient manifestClient;
@Inject private OkHttpClient okHttpClient;
@Inject private Gson gson;
@Inject private DataStoreReader dataStoreReader;
@Inject private Client client;
@Inject private ClientThread clientThread;
@Inject private SpriteManager spriteManager;
public TaskDataClient()
{
log.debug("init task data client");
}
public HashMap<String, TaskType> getTaskTypes() throws Exception {
try (InputStream stream = this.dataStoreReader.readTaskTypes(this.manifestClient.getManifest().taskTypeMetadata);
InputStreamReader responseReader = new InputStreamReader(stream, StandardCharsets.UTF_8))
{
Type listType = TypeToken.getParameterized(ArrayList.class, TaskTypeDefinition.class).getType();
List<TaskTypeDefinition> taskTypeDefinitions = this.gson.fromJson(responseReader, listType);
HashMap<String, TaskType> taskTypes = new HashMap<>();
for (TaskTypeDefinition taskTypeDefinition : taskTypeDefinitions)
{
taskTypes.put(taskTypeDefinition.getTaskJsonName(), new TaskType(client, clientThread, spriteManager, taskTypeDefinition));
}
return taskTypes;
}
}
public List<TaskDefinition> getTaskDefinitions(String jsonFilename) throws Exception
{
try(InputStream stream = this.dataStoreReader.readTasks(jsonFilename);
InputStreamReader responseReader = new InputStreamReader(stream, StandardCharsets.UTF_8))
{
Type listType = TypeToken.getParameterized(ArrayList.class, TaskDefinition.class).getType();
return this.gson.fromJson(responseReader, listType);
}
}
}

View File

@@ -0,0 +1,11 @@
package net.reldo.taskstracker.data.jsondatastore.reader;
import java.io.InputStream;
public interface DataStoreReader
{
InputStream readManifestData() throws Exception;
InputStream readTaskTypes(String taskTypeFilename) throws Exception;
InputStream readTasks(String jsonFilename) throws Exception;
InputStream readFilterConfigs(String filterFilename) throws Exception;
}

View File

@@ -0,0 +1,130 @@
package net.reldo.taskstracker.data.jsondatastore.reader;
import java.io.InputStream;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.data.jsondatastore.JsonDataStore;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
@Singleton
@Slf4j
public class HttpDataStoreReader implements DataStoreReader
{
@Inject
private OkHttpClient okHttpClient;
@Override
public InputStream readManifestData() throws Exception
{
String manifestUrl = JsonDataStore.baseUrl + "/manifest.json";
log.debug("getManifest json from {} ...", manifestUrl);
Request request = new Request.Builder()
.url(manifestUrl)
.build();
Response response = this.okHttpClient.newCall(request).execute();
if (!response.isSuccessful())
{
String unsuccessful = "getManifest json request unsuccessful with status " + response.code();
if (response.body() != null)
{
unsuccessful += " and body \n" + response.body();
}
log.error(unsuccessful);
throw new Exception(unsuccessful);
}
if (response.body() == null)
{
log.error("getManifest returned without body");
throw new Exception("getManifest returned without body");
}
log.debug("getManifest json fetched successfully, deserializing result");
return response.body().byteStream();
}
@Override
public InputStream readTaskTypes(String taskTypeFilename) throws Exception
{
String taskJsonUrl = JsonDataStore.baseUrl + "/" + taskTypeFilename;
log.debug("getTaskTypes json from {} ...", taskJsonUrl);
Request request = new Request.Builder()
.url(taskJsonUrl)
.build();
Response response = this.okHttpClient.newCall(request).execute();
if (!response.isSuccessful())
{
String unsuccessful = "getTaskTypes json request unsuccessful with status " + response.code();
if (response.body() != null)
{
unsuccessful += " and body \n" + response.body();
}
log.error(unsuccessful);
throw new Exception(unsuccessful);
}
if (response.body() == null)
{
log.error("getTaskTypes returned without body");
throw new Exception("getTaskTypes returned without body");
}
log.debug("getTaskTypes json fetched successfully, deserializing result");
return response.body().byteStream();
}
@Override
public InputStream readTasks(String jsonFilename) throws Exception
{
String taskJsonUrl = String.format("%s/tasks/%s.min.json", JsonDataStore.baseUrl, jsonFilename);
log.debug("getTasks json from {} ...", taskJsonUrl);
Request request = new Request.Builder()
.url(taskJsonUrl)
.build();
Response response = this.okHttpClient.newCall(request).execute();
if (!response.isSuccessful())
{
String unsuccessful = "getTasks json request unsuccessful with status " + response.code();
if (response.body() != null)
{
unsuccessful += " and body \n" + response.body();
}
log.error(unsuccessful);
throw new Exception(unsuccessful);
}
if (response.body() == null)
{
log.error("getTasks returned without body");
throw new Exception("getTasks returned without body");
}
log.debug("getTasks json fetched successfully, deserializing result");
return response.body().byteStream();
}
@Override
public InputStream readFilterConfigs(String filterFilename) throws Exception
{
String filterJsonUrl = JsonDataStore.baseUrl + "/" + filterFilename;
log.debug("getTaskTypes json from {} ...", filterJsonUrl);
Request request = new Request.Builder()
.url(filterJsonUrl)
.build();
Response response = this.okHttpClient.newCall(request).execute();
if (!response.isSuccessful())
{
String unsuccessful = "getFilters json request unsuccessful with status " + response.code();
if (response.body() != null)
{
unsuccessful += " and body \n" + response.body();
}
log.error(unsuccessful);
throw new Exception(unsuccessful);
}
if (response.body() == null)
{
log.error("getFilters returned without body");
throw new Exception("getFilters returned without body");
}
log.debug("getFilters json fetched successfully, deserializing result");
return response.body().byteStream();
}
}

View File

@@ -0,0 +1,51 @@
package net.reldo.taskstracker.data.jsondatastore.types;
import java.util.ArrayList;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* Represents the configuration for a filter
*/
@Data
@AllArgsConstructor
public class FilterConfig {
/**
* Key under which to store the filter's selected values, generally prefixed by task type
*/
private String configKey;
/**
* The label displayed in the UI with the filter.
*/
private String label;
/**
* The filter type, see enum for types of filters supported.
*/
private FilterType filterType;
/**
* The source of the value(s) to use for the filter, see enum for types of values supported.
* If global is specified then configKey must match a filter config defined in filters.json
*/
private FilterValueType valueType;
/**
* The name of the param or metadata property to use for the filter.
* Can be left null for SKILL value type
*/
private String valueName;
/**
* Name of an enum specified in `TaskTypeDefinition.stringEnumMap` to provide labels for the filter
* Specifying this property will override the displayed integer value of `valueName`
*/
private String optionLabelEnum;
/**
* Item values in a button filter (dropdown not yet supported)
*/
private ArrayList<FilterCustomItem> customItems;
}

View File

@@ -0,0 +1,11 @@
package net.reldo.taskstracker.data.jsondatastore.types;
import lombok.Data;
@Data
public class FilterCustomItem
{
private Integer value;
private String tooltip;
private Integer spriteId;
}

View File

@@ -0,0 +1,7 @@
package net.reldo.taskstracker.data.jsondatastore.types;
public enum FilterType
{
BUTTON_FILTER,
DROPDOWN_FILTER
}

View File

@@ -0,0 +1,10 @@
package net.reldo.taskstracker.data.jsondatastore.types;
public enum FilterValueType
{
PARAM_INTEGER,
PARAM_STRING,
SKILL,
METADATA,
GLOBAL
}

View File

@@ -0,0 +1,10 @@
package net.reldo.taskstracker.data.jsondatastore.types;
import lombok.Data;
@Data
public class Manifest
{
public String taskTypeMetadata;
public String filterMetadata;
}

View File

@@ -0,0 +1,46 @@
package net.reldo.taskstracker.data.jsondatastore.types;
import java.util.List;
import java.util.Map;
import lombok.Data;
/**
* Represents a task with various attributes
*/
@Data
public class TaskDefinition
{
/**
* Struct id for task data
*/
private Integer structId;
/**
* Sort id based on the sort order in the game's UI
*/
private Integer sortId;
/**
* Skills required for the task.
*/
private List<TaskDefinitionSkill> skills;
/**
* Metadata related to the task that isn't represented in the Struct/params
* May or may not be used for task filters
* Examples:
* - notes = extra description like "a magic cabbage is a cabbage picked at Draynor Manor"
* - category = an extra category type that isn't a param
*/
private Map<String, Object> metadata;
/**
* Notes from the OSRS wiki
*/
private String wikiNotes;
/**
* Completion percent from the OSRS wiki
*/
private Float completionPercent;
}

View File

@@ -0,0 +1,20 @@
package net.reldo.taskstracker.data.jsondatastore.types;
import lombok.Data;
/**
* Represents a skill required for a task.
*/
@Data
public class TaskDefinitionSkill
{
/**
* The skill
*/
private String skill;
/**
* The level required
*/
private Integer level;
}

View File

@@ -0,0 +1,105 @@
package net.reldo.taskstracker.data.jsondatastore.types;
import java.util.ArrayList;
import java.util.HashMap;
import lombok.Data;
/**
* Represents a task type with relevant configuration for UI display and task management
*/
@Data
public class TaskTypeDefinition
{
/**
* Name of the task type for UI display
*/
private String name;
/**
* Description of the task type
*/
private String description;
/**
* Is the task type enabled?
*/
private boolean isEnabled;
/**
* Filename for the task JSON found in the tasks directory of task-json-store
* Extension not included.
*/
private String taskJsonName;
/**
* Filters for the task type
*/
private ArrayList<FilterConfig> filters;
/**
* A dictionary of parameters relevant to the task, with required id, name, description, tier
* The key is the plain English name for the parameter
* The value is an array of OSRS cache Struct ParamIDs that match with the plain English parameter
* Generally, there is only 1 value in the array, but multiple are available for fallback
*/
private HashMap<String, Integer> intParamMap;
/**
* A dictionary of parameters relevant to the task, with required id, name, description, tier
* The key is the plain English name for the parameter
* The value is an array of OSRS cache Struct ParamIDs that match with the plain English parameter
* Generally, there is only 1 value in the array, but multiple are available for fallback
*/
private HashMap<String, Integer> stringParamMap;
/**
* A dictionary of integer enums relevant to the task type
* The key is the plain English name describing the enum
* The value is an integer representing the enum id
* e.g. "tierSprites": 3213 (tier id maps to a sprite id)
*/
private HashMap<String, Integer> intEnumMap;
/**
* A dictionary of string enums relevant to the task type
* The key is the plain English name describing the enum
* The value is an integer representing the enum id
* e.g. "tierNames": 4757 (tier id maps to a sprite id)
*/
private HashMap<String, Integer> stringEnumMap;
/**
* A dictionary of tier sprite ids
* The key is a string representation of the tier id integer
* The value is an integer representing the sprite id
*/
private HashMap<String, Integer> tierSpriteIdMap = new HashMap<>();
/**
* Varps used to store task progress
* Used for exports from the plugin
*/
private ArrayList<Integer> taskVarps;
/**
* Other varps used for the task type
* Used for exports from the plugin
* Examples in the past: League Points, Sage Renown
*/
private int[] otherVarps;
/**
* Varbits used for the task type
* Used for exports from the plugin
* Examples in the past: Relics chosen, Tasks completed, unlocks, Fragment xp
*/
private int[] varbits;
/**
* The script id used to parse the completion of a task
* This is a rs2asm script
* Example: Combat achievements = script 4834
*/
private int taskCompletedScriptId;
}

View File

@@ -0,0 +1,13 @@
package net.reldo.taskstracker.data.reldo;
import java.util.HashMap;
import lombok.Data;
@Data
public class ReldoImport
{
public String taskTypeName;
public int version;
public String rsn;
private HashMap<Integer, ReldoTaskSave> tasks;
}

View File

@@ -0,0 +1,14 @@
package net.reldo.taskstracker.data.reldo;
import lombok.Data;
@Data
public class ReldoTaskSave
{
long completed;
long todo;
long ignored;
int order;
String notes;
long lastUpdated;
}

View File

@@ -0,0 +1,19 @@
package net.reldo.taskstracker.data.task;
import com.google.gson.annotations.Expose;
public class ConfigTaskSave
{
@Expose public final long completed;
@Expose public final long tracked;
@Expose public final Integer structId;
@Expose public final long ignored;
public ConfigTaskSave(TaskFromStruct task)
{
completed = task.getCompletedOn();
tracked = task.getTrackedOn();
ignored = task.getIgnoredOn();
structId = task.getStructId();
}
}

View File

@@ -0,0 +1,208 @@
package net.reldo.taskstracker.data.task;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.data.jsondatastore.types.TaskDefinition;
import net.reldo.taskstracker.data.reldo.ReldoTaskSave;
import net.runelite.api.Client;
import net.runelite.api.StructComposition;
@Slf4j
public class TaskFromStruct
{
@Getter
private final Integer structId;
@Getter
private final Integer sortId;
@Getter
private TaskType taskType;
@Getter
private final TaskDefinition taskDefinition;
@Getter
private boolean structLoaded;
@Getter @Setter
private long completedOn;
@Getter @Setter
private long trackedOn;
@Getter @Setter
private long ignoredOn;
private StructComposition _struct;
private final Map<String, String> _stringParams = new HashMap<>();
private final Map<String, Integer> _intParams = new HashMap<>();
public TaskFromStruct(TaskType taskType, TaskDefinition taskDefinition)
{
this.taskType = taskType;
this.taskDefinition = taskDefinition;
this.structId = taskDefinition.getStructId();
this.sortId = taskDefinition.getSortId();
}
public String getStringParam(String paramName)
{
return this._stringParams.get(paramName);
}
public Integer getIntParam(String paramName)
{
return this._intParams.get(paramName);
}
// TODO: Remove client from params
public boolean loadStructData(Client client)
{
assert client.isClientThread();
if (structLoaded) {
return true;
}
try
{
// log.debug("LOADING STRUCT DATA " + structId);
_struct = client.getStructComposition(structId);
taskType.getIntParamMap().forEach((paramName, paramId) -> {
int value = _struct.getIntValue(paramId);
// log.debug("{} {}", paramName, value);
_intParams.put(paramName, value);
});
taskType.getStringParamMap().forEach((paramName, paramId) -> {
String value = _struct.getStringValue(paramId);
// log.debug("{} {}", paramName, value);
_stringParams.put(paramName, value);
});
}
catch (Exception ex)
{
log.error("error loading struct data {}", ex, ex);
return false;
}
structLoaded = true;
return true;
}
public boolean isCompleted()
{
return completedOn > 0;
}
public int getPoints()
{
if (taskType.getTierPoints().size() == 0)
{
return 0;
}
Integer points = taskType.getTierPoints().get(getTier());
if (points == null)
{
return 0;
}
return points;
}
public void setCompleted(boolean completed)
{
long now = Instant.now().toEpochMilli();
if (completed && completedOn > 0 && completedOn <= now)
{
return;
}
completedOn = completed ? now : 0;
}
public boolean isTracked()
{
return trackedOn > 0;
}
public void setTracked(boolean state)
{
long now = Instant.now().toEpochMilli();
if (state && trackedOn > 0 && trackedOn <= now)
{
return;
}
trackedOn = state ? now : 0;
}
public boolean isIgnored()
{
return ignoredOn > 0;
}
public void setIgnored(boolean state)
{
long now = Instant.now().toEpochMilli();
if (state && ignoredOn > 0 && ignoredOn <= now)
{
return;
}
ignoredOn = state ? now : 0;
}
public void loadConfigSave(ConfigTaskSave loadedData)
{
setDates(loadedData.completed, loadedData.ignored, loadedData.tracked);
}
public void loadReldoSave(ReldoTaskSave loadedData)
{
setMostRecentDates(loadedData.getCompleted(), loadedData.getIgnored(), loadedData.getTodo());
}
private void setDates(long completedOn, long ignoredOn, long trackedOn)
{
// Set all dates regardless of how they compare
this.setCompletedOn(completedOn);
this.setIgnoredOn(ignoredOn);
this.setTrackedOn(trackedOn);
}
private void setMostRecentDates(long completedOn, long ignoredOn, long trackedOn)
{
// Older completions take priority; incomplete (0) also takes priority
if (completedOn < this.getCompletedOn())
{
this.setCompletedOn(completedOn);
}
// Newer ignores take priority
if (ignoredOn > this.getIgnoredOn())
{
this.setIgnoredOn(ignoredOn);
}
// Newer tracks take priority
if (trackedOn > this.getTrackedOn())
{
this.setTrackedOn(trackedOn);
}
}
public String getName()
{
return getStringParam("name");
}
public int getTier()
{
return getIntParam("tier");
}
public String getDescription()
{
return getStringParam("description");
}
public ConfigTaskSave getSaveData()
{
return new ConfigTaskSave(this);
}
public Float getCompletionPercent() {
return getTaskDefinition().getCompletionPercent();
}
}

View File

@@ -0,0 +1,306 @@
package net.reldo.taskstracker.data.task;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.TasksTrackerPlugin;
import net.reldo.taskstracker.data.jsondatastore.ManifestClient;
import net.reldo.taskstracker.data.jsondatastore.TaskDataClient;
import net.reldo.taskstracker.data.jsondatastore.types.FilterConfig;
import net.reldo.taskstracker.data.jsondatastore.types.FilterValueType;
import net.reldo.taskstracker.data.jsondatastore.types.TaskDefinition;
import net.reldo.taskstracker.data.task.filters.FilterService;
import net.runelite.api.Client;
import net.runelite.api.EnumComposition;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager;
@Singleton
@Slf4j
public class TaskService
{
@Inject private ManifestClient manifestClient;
@Inject private TaskDataClient taskDataClient;
@Inject private ClientThread clientThread;
@Inject private Client client;
@Inject private FilterService filterService;
@Inject private ConfigManager configManager;
@Getter
@Setter
private boolean taskTypeChanged = false;
@Getter
private TaskType currentTaskType;
@Getter
private final List<TaskFromStruct> tasks = new ArrayList<>();
@Getter
private final HashMap<String, int[]> sortedIndexes = new HashMap<>();
private HashMap<String, TaskType> _taskTypes = new HashMap<>();
private HashSet<Integer> currentTaskTypeVarps = new HashSet<>();
private final ExecutorService futureExecutor = Executors.newSingleThreadExecutor();
public CompletableFuture<Boolean> setTaskType(String taskTypeJsonName) {
return getTaskTypesByJsonName().thenCompose(taskTypes ->
{
TaskType newTaskType = taskTypes.get(taskTypeJsonName);
if (newTaskType == null)
{
log.error("unsupported task type {}, falling back to COMBAT", taskTypeJsonName);
newTaskType = taskTypes.get("COMBAT");
}
return this.setTaskType(newTaskType);
});
}
private CompletableFuture<Boolean> loadAllTasksStructData(Collection<TaskFromStruct> tasks) {
Collection<CompletableFuture<Boolean>> taskFutures = new ArrayList<>();
for (TaskFromStruct task : tasks) {
CompletableFuture<Boolean> taskFuture = new CompletableFuture<>();
clientThread.invoke(() -> {
boolean isTaskLoaded = task.loadStructData(client);
taskFuture.complete(isTaskLoaded);
});
taskFutures.add(taskFuture);
}
return CompletableFuture.allOf(taskFutures.toArray(new CompletableFuture[0])).thenApply(v -> {
for (CompletableFuture<Boolean> future : taskFutures) {
if (!future.join()) {
return false;
}
}
return true;
});
}
public CompletableFuture<Boolean> setTaskType(TaskType newTaskType) {
log.debug("setTaskType {}", newTaskType.getTaskJsonName());
if (newTaskType.equals(currentTaskType)) {
log.debug("Skipping setTaskType, same task type selected");
return CompletableFuture.completedFuture(false);
}
currentTaskType = newTaskType;
configManager.setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "taskTypeJsonName", newTaskType.getTaskJsonName());
// Complete creation of any GLOBAL value type filterConfigs
for (FilterConfig filterConfig : currentTaskType.getFilters()) {
if (filterConfig.getValueType().equals(FilterValueType.GLOBAL)) {
// Set valueType to the one required by the global filter
FilterConfig globalFilterConfig = filterService.getGlobalFilterByKey(filterConfig.getConfigKey());
filterConfig.setValueType(globalFilterConfig.getValueType());
// Set any filterConfig fields not already specified
Optional.ofNullable(filterConfig.getLabel()).ifPresentOrElse(val -> {}, () -> filterConfig.setLabel(globalFilterConfig.getLabel()));
Optional.ofNullable(filterConfig.getFilterType()).ifPresentOrElse(val -> {}, () -> filterConfig.setFilterType(globalFilterConfig.getFilterType()));
Optional.ofNullable(filterConfig.getValueName()).ifPresentOrElse(val -> {}, () -> filterConfig.setValueName(globalFilterConfig.getValueName()));
Optional.ofNullable(filterConfig.getOptionLabelEnum()).ifPresentOrElse(val -> {}, () -> filterConfig.setOptionLabelEnum(globalFilterConfig.getOptionLabelEnum()));
Optional.ofNullable(filterConfig.getCustomItems()).ifPresentOrElse(val -> {}, () -> filterConfig.setCustomItems(globalFilterConfig.getCustomItems()));
}
}
List<TaskFromStruct> newTasks = new ArrayList<>();
return newTaskType.loadTaskTypeDataAsync().thenCompose((isTaskTypeLoaded) -> {
if (!isTaskTypeLoaded) {
log.error("Error loading task type during setTaskType");
return CompletableFuture.completedFuture(false);
}
CompletableFuture<Boolean> future = new CompletableFuture<>();
futureExecutor.submit(() -> {
try {
Collection<TaskDefinition> taskDefinitions = taskDataClient.getTaskDefinitions(currentTaskType.getTaskJsonName());
for (TaskDefinition definition : taskDefinitions) {
TaskFromStruct task = new TaskFromStruct(currentTaskType, definition);
newTasks.add(task);
}
loadAllTasksStructData(newTasks).thenApply(future::complete);
} catch (Exception e3) {
future.completeExceptionally(e3);
}
});
return future;
}).thenCompose(areTasksLoaded -> {
if (!areTasksLoaded) {
return CompletableFuture.completedFuture(false);
}
tasks.clear();
tasks.addAll(newTasks);
// Index task list for each property
sortedIndexes.clear();
currentTaskType.getIntParamMap().keySet().forEach(paramName -> {
sortedIndexes.put(paramName, null);
addSortedIndex(paramName, Comparator.comparingInt((TaskFromStruct task) -> task.getIntParam(paramName)));
});
currentTaskType.getStringParamMap().keySet().forEach(paramName -> {
sortedIndexes.put(paramName, null);
addSortedIndex(paramName, Comparator.comparing((TaskFromStruct task) -> task.getStringParam(paramName)));
});
// todo: make this less of a special case.
if (tasks.stream().anyMatch(task -> task.getCompletionPercent() != null)) {
sortedIndexes.put("completion %", null);
addSortedIndex("completion %",
(TaskFromStruct task1, TaskFromStruct task2) ->
{
Float comp1 = task1.getTaskDefinition().getCompletionPercent() != null ? task1.getTaskDefinition().getCompletionPercent() : 0;
Float comp2 = task2.getTaskDefinition().getCompletionPercent() != null ? task2.getTaskDefinition().getCompletionPercent() : 0;
return comp1.compareTo(comp2);
});
}
currentTaskTypeVarps.clear();
currentTaskTypeVarps = new HashSet<>(currentTaskType.getTaskVarps());
taskTypeChanged = true;
return CompletableFuture.completedFuture(true);
});
}
private void addSortedIndex(String paramName, Comparator<TaskFromStruct> comparator)
{
List<TaskFromStruct> sortedTasks = tasks.stream()
.sorted(comparator)
.collect(Collectors.toCollection(ArrayList::new));
int[] sortedIndex = new int[tasks.size()];
for(int i = 0; i < sortedTasks.size(); i++)
{
sortedIndex[i] = tasks.indexOf(sortedTasks.get(i));
}
sortedIndexes.put(paramName, sortedIndex);
}
public int getSortedTaskIndex(String sortCriteria, int position)
{
if(sortedIndexes.containsKey(sortCriteria))
{
return sortedIndexes.get(sortCriteria)[position];
}
else
{
return position;
}
}
public boolean isVarpInCurrentTaskType(int varpId)
{
return currentTaskTypeVarps.contains(varpId);
}
public void clearTaskTypes()
{
this._taskTypes.clear();
}
/**
* Get a map of task type json names to task type
*
* @return Hashmap of TaskType indexed by task type json name
*/
public CompletableFuture<HashMap<String, TaskType>> getTaskTypesByJsonName()
{
if (_taskTypes.size() > 0)
{
return CompletableFuture.completedFuture(_taskTypes);
}
try
{
CompletableFuture<HashMap<String, TaskType>> future = new CompletableFuture<>();
futureExecutor.submit(() ->
{
try
{
_taskTypes = taskDataClient.getTaskTypes();
future.complete(_taskTypes);
}
catch (Exception e) {
future.completeExceptionally(e);
}
});
return future;
}
catch (Exception ex)
{
log.error("Unable to populate task types from data client", ex);
return CompletableFuture.completedFuture(new HashMap<>());
}
}
public CompletableFuture<HashMap<Integer, String>> getStringEnumValuesAsync(String enumName)
{
Integer enumId = currentTaskType.getStringEnumMap().get(enumName);
if (enumId == null)
{
return CompletableFuture.completedFuture(new HashMap<>());
}
CompletableFuture<HashMap<Integer, String>> future = new CompletableFuture<>();
clientThread.invoke(() -> {
try
{
EnumComposition enumComposition = client.getEnum(enumId);
int[] keys = enumComposition.getKeys();
HashMap<Integer, String> map = new HashMap<>();
for (int key : keys)
{
map.put(key, enumComposition.getStringValue(key));
}
future.complete(map);
}
catch (Exception ex)
{
log.error("Error getting string enum values", ex);
future.completeExceptionally(ex);
}
});
return future;
}
public void applySave(TaskType saveTaskType, HashMap<Integer, ConfigTaskSave> saveData)
{
String currentTaskTypeName = currentTaskType.getTaskJsonName();
String saveTaskTypeName = saveTaskType.getTaskJsonName();
if (!currentTaskTypeName.equals(saveTaskTypeName))
{
log.warn("Cannot apply save, task types do not match current={} save={}", currentTaskTypeName, saveTaskTypeName);
return;
}
for (TaskFromStruct task : getTasks())
{
ConfigTaskSave configTaskSave = saveData.get(task.getStructId());
if (configTaskSave == null)
{
continue;
}
task.loadConfigSave(configTaskSave);
}
}
public List<TaskFromStruct> getTasksFromVarpId(Integer varpId)
{
int varpIndex = getCurrentTaskType().getTaskVarps().indexOf(varpId);
int minTaskId = varpIndex * 32;
int maxTaskId = minTaskId + 32;
return getTasks().stream().filter(t -> {
int taskId = t.getIntParam("id");
return taskId >= minTaskId && taskId <= maxTaskId;
}).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,147 @@
package net.reldo.taskstracker.data.task;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.CompletableFuture;
import lombok.Getter;
import net.reldo.taskstracker.data.jsondatastore.types.FilterConfig;
import net.reldo.taskstracker.data.jsondatastore.types.FilterType;
import net.reldo.taskstracker.data.jsondatastore.types.TaskTypeDefinition;
import net.runelite.api.Client;
import net.runelite.api.EnumComposition;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.game.SpriteManager;
public class TaskType
{
@Getter
private final HashMap<Integer, BufferedImage> spritesById = new HashMap<>();
@Getter
private final HashMap<Integer, BufferedImage> tierSprites = new HashMap<>();
@Getter
private final HashMap<Integer, Integer> tierPoints = new HashMap<>();
private final Client client;
private final ClientThread clientThread;
private final SpriteManager spriteManager;
private final TaskTypeDefinition _taskTypeDefinition;
public TaskType(Client client, ClientThread clientThread, SpriteManager spriteManager, TaskTypeDefinition taskTypeDefinition)
{
this.client = client;
this.clientThread = clientThread;
this.spriteManager = spriteManager;
this._taskTypeDefinition = taskTypeDefinition;
}
public CompletableFuture<Boolean> loadTaskTypeDataAsync()
{
CompletableFuture<Boolean> future = new CompletableFuture<>();
clientThread.invoke(() -> {
try {
getButtonFiltersSpriteIds().forEach((spriteId) -> {
BufferedImage spriteImage = spriteManager.getSprite(spriteId, 0);
spritesById.put(spriteId, spriteImage);
});
_taskTypeDefinition.getTierSpriteIdMap().forEach((idKey, spriteId) -> {
Integer tierId = Integer.parseInt(idKey);
BufferedImage spriteImage = spriteManager.getSprite(spriteId, 0);
tierSprites.put(tierId, spriteImage);
});
if (_taskTypeDefinition.getIntEnumMap().containsKey("tierPoints"))
{
int enumId = _taskTypeDefinition.getIntEnumMap().get("tierPoints");
EnumComposition enumComposition = client.getEnum(enumId);
int[] keys = enumComposition.getKeys();
for (int key : keys)
{
tierPoints.put(key, enumComposition.getIntValue(key));
}
}
future.complete(true);
} catch (Exception e) {
future.completeExceptionally(e);
}
});
return future;
}
public String getFilterConfigPrefix()
{
return _taskTypeDefinition.getTaskJsonName() + ".";
}
private HashSet<Integer> getButtonFiltersSpriteIds()
{
HashSet<Integer> sprites = new HashSet<>();
_taskTypeDefinition.getFilters().stream().filter(
(filterConfig) -> filterConfig.getFilterType().equals(FilterType.BUTTON_FILTER)
).forEach((filterConfig) -> {
if (filterConfig.getCustomItems() != null)
{
filterConfig.getCustomItems().forEach((customSprite) -> {
Integer spriteId = customSprite.getSpriteId();
if (spriteId == null)
{
return;
}
sprites.add(spriteId);
});
}
});
return sprites;
}
public ArrayList<Integer> getTaskVarps()
{
return _taskTypeDefinition.getTaskVarps();
}
public String getTaskJsonName()
{
return _taskTypeDefinition.getTaskJsonName();
}
public HashMap<String, Integer> getIntParamMap()
{
return _taskTypeDefinition.getIntParamMap();
}
public HashMap<String, Integer> getStringParamMap()
{
return _taskTypeDefinition.getStringParamMap();
}
public HashMap<String, Integer> getStringEnumMap()
{
return _taskTypeDefinition.getStringEnumMap();
}
public String getName()
{
return _taskTypeDefinition.getName();
}
public ArrayList<FilterConfig> getFilters()
{
return _taskTypeDefinition.getFilters();
}
public int[] getOtherVarps()
{
return _taskTypeDefinition.getOtherVarps();
}
public int[] getVarbits()
{
return _taskTypeDefinition.getVarbits();
}
public int getTaskCompletedScriptId()
{
return _taskTypeDefinition.getTaskCompletedScriptId();
}
}

View File

@@ -0,0 +1,8 @@
package net.reldo.taskstracker.data.task.filters;
import net.reldo.taskstracker.data.task.TaskFromStruct;
public abstract class Filter
{
public abstract boolean meetsCriteria(TaskFromStruct task);
}

View File

@@ -0,0 +1,47 @@
package net.reldo.taskstracker.data.task.filters;
import java.util.HashMap;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.data.jsondatastore.FilterDataClient;
import net.reldo.taskstracker.data.jsondatastore.types.FilterConfig;
@Singleton
@Slf4j
public class FilterService
{
@Inject
private FilterDataClient filterDataClient;
// Filter config cache
private HashMap<String, FilterConfig> _filterConfigs = new HashMap<>();
public FilterConfig getGlobalFilterByKey(String filterKey)
{
// Instantiate filterConfigs if not already
if (_filterConfigs == null || _filterConfigs.isEmpty())
{
try
{
_filterConfigs = filterDataClient.getFilterConfigs();
return _filterConfigs.get(filterKey);
}
catch (Exception ex)
{
log.error("Unable to get filter configs", ex);
}
}
else
{
return _filterConfigs.get(filterKey);
}
return null;
}
public void clearFilterConfigs()
{
this._filterConfigs.clear();
}
}

View File

@@ -0,0 +1,34 @@
package net.reldo.taskstracker.data.task.filters;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.TasksTrackerPlugin;
import net.reldo.taskstracker.data.task.TaskFromStruct;
import net.runelite.client.config.ConfigManager;
@Slf4j
public class ParamButtonFilter extends Filter
{
private final String paramName;
private final String filterConfigKey;
private final ConfigManager configManager;
public ParamButtonFilter(ConfigManager configManager, String paramName, String filterConfigKey)
{
this.configManager = configManager;
this.paramName = paramName;
this.filterConfigKey = filterConfigKey;
}
@Override
public boolean meetsCriteria(TaskFromStruct task)
{
String configValue = configManager.getConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, filterConfigKey);
boolean isEmptyFilterSelection = configValue == null || configValue.isEmpty() || configValue.equals("-1");
if (isEmptyFilterSelection)
{
return false;
}
return configValue.contains("f-" + task.getIntParam(paramName) + "-f");
}
}

View File

@@ -0,0 +1,46 @@
package net.reldo.taskstracker.data.task.filters;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.TasksTrackerPlugin;
import net.reldo.taskstracker.data.task.TaskFromStruct;
import net.runelite.client.config.ConfigManager;
@Slf4j
public class ParamDropdownFilter extends Filter
{
private final String paramName;
private final String filterConfigKey;
private final ConfigManager configManager;
public ParamDropdownFilter(ConfigManager configManager, String paramName, String filterConfigKey)
{
this.configManager = configManager;
this.paramName = paramName;
this.filterConfigKey = filterConfigKey;
}
@Override
public boolean meetsCriteria(TaskFromStruct task)
{
String configValue = configManager.getConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, filterConfigKey);
boolean isEmptyFilterSelection = configValue == null || configValue.isEmpty() || configValue.equals("-1");
if (isEmptyFilterSelection)
{
return true;
}
if (task.getIntParam(paramName) != null)
{
try
{
Integer parsedConfigValue = Integer.parseInt(configValue);
return parsedConfigValue.equals(task.getIntParam(paramName));
}
catch (Exception ex)
{
log.warn("meetsCriteria error parsing config value for {}", configValue);
return true;
}
}
return true;
}
}

View File

@@ -0,0 +1,10 @@
package net.reldo.taskstracker.panel;
import java.awt.Color;
public class Colors {
public static Color QUALIFIED_TEXT_COLOR = new Color(34, 177, 77);
public static Color UNQUALIFIED_BACKGROUND_COLOR = new Color(70, 30, 0);
public static Color UNQUALIFIED_TEXT_COLOR = new Color(251, 93, 93);
public static Color COMPLETED_BACKGROUND_COLOR = new Color(0, 50, 0);
}

View File

@@ -0,0 +1,41 @@
package net.reldo.taskstracker.panel;
import java.awt.image.BufferedImage;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import net.reldo.taskstracker.TasksTrackerPlugin;
import net.runelite.client.util.ImageUtil;
public class Icons
{
private static final String completeBtnPath = "panel/components/complete_button/";
public static final Icon INCOMPLETE_ONLY_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, completeBtnPath + "incomplete_only_icon.png"));
public static final Icon COMPLETE_ONLY_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, completeBtnPath + "complete_only_icon.png"));
public static final Icon COMPLETE_INCOMPLETE_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, completeBtnPath + "complete_and_incomplete_icon.png"));
private static final String ignoredBtnPath = "panel/components/ignored_button/";
public static final BufferedImage semivisibleimg = ImageUtil.loadImageResource(TasksTrackerPlugin.class, ignoredBtnPath + "semivisible_icon.png");
public static final Icon SEMIVISIBLE_ICON = new ImageIcon(ImageUtil.alphaOffset(semivisibleimg, -180));
public static final Icon INVISIBLE_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, ignoredBtnPath + "invisible_icon.png"));
public static final Icon VISIBLE_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, ignoredBtnPath + "visible_icon.png"));
private static final String trackedBtnPath = "panel/components/tracked_button/";
public static final Icon UNTRACKED_ONLY_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, trackedBtnPath + "untracked_icon.png"));
public static final Icon TRACKED_ONLY_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, trackedBtnPath + "tracked_icon.png"));
public static final Icon TRACKED_UNTRACKED_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, trackedBtnPath + "tracked_and_untracked_icon.png"));
private static final String sortBtnPath = "panel/components/sort_button/";
public static final Icon ASCENDING_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, sortBtnPath + "ascending_icon.png"));
public static final Icon DESCENDING_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, sortBtnPath + "descending_icon.png"));
private static final String expandBtnPath = "panel/components/";
public static final Icon MENU_EXPANDED_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, expandBtnPath + "filter_menu_expanded.png"));
public static final BufferedImage collapseImg = ImageUtil.loadImageResource(TasksTrackerPlugin.class, expandBtnPath + "filter_menu_collapsed.png");
public static final Icon MENU_ICON_HOVER = new ImageIcon(collapseImg);
public static final Icon MENU_COLLAPSED_ICON = new ImageIcon(ImageUtil.alphaOffset(collapseImg, -180));
public static final ImageIcon PLUS_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, "plus.png"));
public static final ImageIcon MINUS_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, "minus.png"));
public static final ImageIcon EYE_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, "eye.png"));
public static final ImageIcon EYE_CROSS_GREY = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, "eye-cross-grey.png"));
}

View File

@@ -0,0 +1,543 @@
package net.reldo.taskstracker.panel;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JToggleButton;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.border.MatteBorder;
import javax.swing.plaf.basic.BasicButtonUI;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.TasksTrackerConfig;
import net.reldo.taskstracker.TasksTrackerPlugin;
import net.reldo.taskstracker.config.ConfigValues;
import net.reldo.taskstracker.data.jsondatastore.types.FilterConfig;
import net.reldo.taskstracker.data.jsondatastore.types.FilterType;
import net.reldo.taskstracker.data.task.TaskService;
import net.reldo.taskstracker.data.task.TaskType;
import net.reldo.taskstracker.panel.components.SearchBox;
import net.reldo.taskstracker.panel.components.TriToggleButton;
import net.reldo.taskstracker.panel.filters.ComboItem;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.ui.FontManager;
import net.runelite.client.ui.PluginPanel;
import net.runelite.client.util.SwingUtil;
@Slf4j
public class LoggedInPanel extends JPanel
{
public TaskListPanel taskListPanel;
private JComboBox<ComboItem<TaskType>> taskTypeDropdown;
private final TaskService taskService;
private final TasksTrackerPlugin plugin;
private final TasksTrackerConfig config;
// Filter buttons
private final TriToggleButton completedFilterBtn = new TriToggleButton();
private final TriToggleButton trackedFilterBtn = new TriToggleButton();
private final TriToggleButton ignoredFilterBtn = new TriToggleButton();
private final JPanel titlePanel = new JPanel();
// Task list tabs
private final JPanel tabPane = new JPanel();
// sub-filter panel
private SubFilterPanel subFilterPanel;
private SortPanel sortPanel;
private final JToggleButton collapseBtn = new JToggleButton();
public LoggedInPanel(TasksTrackerPlugin plugin, TasksTrackerConfig config, TaskService taskService)
{
super(false);
this.plugin = plugin;
this.taskService = taskService;
this.config = config;
createPanel();
}
@Override
public Dimension getPreferredSize()
{
return getParent().getSize();
}
public void redraw()
{
// taskTypeDropdown may become de-synced after profile change
String selectedTaskTypeJsonName = taskTypeDropdown.getItemAt(taskTypeDropdown.getSelectedIndex()).getValue().getTaskJsonName();
if(!selectedTaskTypeJsonName.equals(config.taskTypeJsonName()))
{
log.debug("Task type dropdown de-synced, attempting to find current task type");
for(int i = 0; i < taskTypeDropdown.getItemCount(); i++)
{
ComboItem<TaskType> item = taskTypeDropdown.getItemAt(i);
if(item.getValue().getTaskJsonName().equals(config.taskTypeJsonName()))
{
log.debug("Current task type found, setting selected task type");
taskTypeDropdown.setSelectedIndex(i);
break;
}
}
}
subFilterPanel.redraw();
sortPanel.redraw();
updateCollapseButtonText();
taskListPanel.redraw();
}
public void refreshAllTasks()
{
updateCollapseButtonText();
taskListPanel.refreshAllTasks();
}
private void createPanel()
{
setLayout(new BorderLayout());
setBackground(ColorScheme.DARK_GRAY_COLOR);
taskListPanel = new TaskListPanel(plugin, taskService);
add(getNorthPanel(), BorderLayout.NORTH);
add(getCenterPanel(), BorderLayout.CENTER);
add(getSouthPanel(), BorderLayout.SOUTH);
loadAndApplyFilters(config.taskListTab());
if(config.taskListTab().equals(ConfigValues.TaskListTabs.TRACKED))
{
trackedFilterBtn.setState(1);
trackedFilterBtn.setEnabled(false);
plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "trackedFilter", ConfigValues.TrackedFilterValues.TRACKED);
}
}
private JPanel getCenterPanel() {
// wrapper for the task list and tab buttons
final JPanel taskListPanel = new JPanel(new BorderLayout());
taskListPanel.setBackground(ColorScheme.DARK_GRAY_COLOR);
taskListPanel.setBorder(new MatteBorder(0, 0, 1, 0, ColorScheme.MEDIUM_GRAY_COLOR));
taskListPanel.setAlignmentX(JPanel.CENTER_ALIGNMENT);
tabPane.setLayout(new BoxLayout(tabPane, BoxLayout.X_AXIS));
tabPane.setBorder(new EmptyBorder(0,0,0,0));
tabPane.setPreferredSize(new Dimension(PluginPanel.PANEL_WIDTH,24));
JToggleButton trackedTab = tabButton("Tracked Tasks", ConfigValues.TaskListTabs.TRACKED);
JToggleButton allTab = tabButton("All Tasks", ConfigValues.TaskListTabs.ALL);
JToggleButton customTab = tabButton("Custom", ConfigValues.TaskListTabs.CUSTOM);
ButtonGroup tabGroup = new ButtonGroup();
tabGroup.add(trackedTab);
tabGroup.add(allTab);
tabGroup.add(customTab);
tabPane.add(Box.createHorizontalGlue());
tabPane.add(trackedTab);
tabPane.add(Box.createHorizontalGlue());
tabPane.add(allTab);
tabPane.add(Box.createHorizontalGlue());
tabPane.add(customTab);
tabPane.add(Box.createHorizontalGlue());
taskListPanel.add(tabPane, BorderLayout.NORTH);
taskListPanel.add(this.taskListPanel, BorderLayout.CENTER);
// set initial filter states to "complete and incomplete", "tracked and untracked", "not ignored"
Map<String, Integer> filterStates = new HashMap<>();
filterStates.put("completed",0);
filterStates.put("tracked",0);
filterStates.put("ignored",0);
for(ConfigValues.TaskListTabs tab : ConfigValues.TaskListTabs.values())
{
filterStore.put(tab, filterStates);
}
switch (config.taskListTab())
{
case TRACKED:
trackedTab.setSelected(true);
break;
case ALL:
allTab.setSelected(true);
break;
case CUSTOM:
customTab.setSelected(true);
break;
}
tabChanged(config.taskListTab());
return taskListPanel;
}
public void tabChanged(ConfigValues.TaskListTabs newTab)
{
if(newTab != null) {
changeTab(newTab);
switch (newTab) {
case TRACKED:
trackedFilterBtn.setState(1);
trackedFilterBtn.setEnabled(false);
plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "taskListTab", newTab);
filterButtonAction("tracked");
break;
case ALL:
trackedFilterBtn.setState(0);
trackedFilterBtn.setEnabled(false);
completedFilterBtn.setState(0);
completedFilterBtn.setEnabled(false);
ignoredFilterBtn.setState(1);
ignoredFilterBtn.setEnabled(false);
plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "taskListTab", newTab);
actionAllFilterButtons();
break;
case CUSTOM:
plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "taskListTab", newTab);
plugin.refreshAllTasks();
break;
default:
plugin.refreshAllTasks();
break;
}
}
}
private JToggleButton tabButton(String label, ConfigValues.TaskListTabs tab)
{
JToggleButton button = new JToggleButton(label);
button.setBorder(new EmptyBorder(2,5,2,5));
button.setBackground(ColorScheme.DARK_GRAY_COLOR);
button.setForeground(ColorScheme.LIGHT_GRAY_COLOR);
button.addActionListener(e -> tabChanged(tab));
return button;
}
private void changeTab(ConfigValues.TaskListTabs newTab)
{
saveFilters();
resetFilters();
loadAndApplyFilters(newTab);
}
private final Map<ConfigValues.TaskListTabs, Map<String, Integer>> filterStore = new HashMap<>();
private void saveFilters()
{
ConfigValues.TaskListTabs tab = config.taskListTab();
Map<String, Integer> filterStates = new HashMap<>();
filterStates.put("completed", config.completedFilter().ordinal());
filterStates.put("tracked", config.trackedFilter().ordinal());
filterStates.put("ignored", config.ignoredFilter().ordinal());
filterStore.put(tab, filterStates);
}
private void resetFilters()
{
completedFilterBtn.setEnabled(true);
trackedFilterBtn.setEnabled(true);
ignoredFilterBtn.setEnabled(true);
}
private void loadAndApplyFilters(ConfigValues.TaskListTabs tab)
{
Map<String,Integer> filterStates = filterStore.get(tab);
if(filterStates == null) return;
Enum configValue;
completedFilterBtn.setState(filterStates.get("completed"));
trackedFilterBtn.setState(filterStates.get("tracked"));
ignoredFilterBtn.setState(filterStates.get("ignored"));
configValue = ConfigValues.CompletedFilterValues.values()[completedFilterBtn.getState()];
plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "completedFilter", configValue);
configValue = ConfigValues.TrackedFilterValues.values()[trackedFilterBtn.getState()];
plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "trackedFilter", configValue);
configValue = ConfigValues.IgnoredFilterValues.values()[ignoredFilterBtn.getState()];
plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "ignoredFilter", configValue);
}
private JPanel getSouthPanel()
{
JPanel southPanel = new JPanel(new BorderLayout());
southPanel.setBorder(new EmptyBorder(5,0,2,0));
JButton importButton = new JButton("Import");
importButton.setBorder(new EmptyBorder(5, 5, 5, 5));
importButton.setLayout(new BorderLayout(0, PluginPanel.BORDER_OFFSET));
importButton.addActionListener(e -> plugin.openImportJsonDialog());
southPanel.add(importButton, BorderLayout.WEST);
JButton totalsButton = new JButton("Totals");
totalsButton.setBorder(new EmptyBorder(5, 5, 5, 5));
totalsButton.setLayout(new BorderLayout(0, PluginPanel.BORDER_OFFSET));
totalsButton.addActionListener(e -> plugin.sendTotalsToChat());
southPanel.add(totalsButton, BorderLayout.CENTER);
JButton exportButton = new JButton("Export");
exportButton.setBorder(new EmptyBorder(5, 5, 5, 5));
exportButton.setLayout(new BorderLayout(0, PluginPanel.BORDER_OFFSET));
exportButton.addActionListener(e -> plugin.copyJsonToClipboard());
southPanel.add(exportButton, BorderLayout.EAST);
return southPanel;
}
private JPanel getNorthPanel()
{
JPanel northPanel = new JPanel();
BoxLayout layout = new BoxLayout(northPanel, BoxLayout.Y_AXIS);
northPanel.setLayout(layout);
northPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
taskTypeDropdown = new JComboBox<>();
taskTypeDropdown.setAlignmentX(LEFT_ALIGNMENT);
taskTypeDropdown.setFocusable(false);
initTaskTypeDropdownAsync();
// Wrapper for collapsible sub-filter menu
JPanel subFilterWrapper = new JPanel();
subFilterWrapper.setLayout(new BorderLayout());
subFilterWrapper.setBorder(new MatteBorder(1, 0, 1, 0, ColorScheme.MEDIUM_GRAY_COLOR));
subFilterWrapper.setAlignmentX(LEFT_ALIGNMENT);
subFilterWrapper.setBackground(ColorScheme.DARKER_GRAY_COLOR);
// collapse button
SwingUtil.removeButtonDecorations(collapseBtn);
collapseBtn.setIcon(Icons.MENU_COLLAPSED_ICON);
collapseBtn.setSelectedIcon(Icons.MENU_EXPANDED_ICON);
collapseBtn.setRolloverIcon(Icons.MENU_ICON_HOVER);
SwingUtil.addModalTooltip(collapseBtn, "Collapse filters", "Expand filters");
collapseBtn.setBackground(ColorScheme.DARKER_GRAY_COLOR);
collapseBtn.setAlignmentX(LEFT_ALIGNMENT);
collapseBtn.setUI(new BasicButtonUI()); // substance breaks the layout
collapseBtn.addActionListener(ev -> subFilterPanel.setVisible(!subFilterPanel.isVisible()));
collapseBtn.setHorizontalTextPosition(JButton.CENTER);
collapseBtn.setForeground(ColorScheme.LIGHT_GRAY_COLOR);
collapseBtn.setFont(FontManager.getRunescapeSmallFont());
collapseBtn.setBorder(new EmptyBorder(2, 0, 2, 0));
collapseBtn.setFocusable(false);
// panel to hold sub-filters
subFilterPanel = new SubFilterPanel(plugin, taskService);
subFilterWrapper.add(collapseBtn, BorderLayout.NORTH);
subFilterWrapper.add(subFilterPanel, BorderLayout.CENTER);
sortPanel = new SortPanel(plugin, taskService, taskListPanel);
northPanel.add(getTitleAndButtonPanel());
northPanel.add(Box.createVerticalStrut(10));
northPanel.add(taskTypeDropdown);
northPanel.add(Box.createVerticalStrut(2));
northPanel.add(getSearchPanel());
northPanel.add(Box.createVerticalStrut(2));
northPanel.add(sortPanel);
northPanel.add(Box.createVerticalStrut(2));
northPanel.add(subFilterWrapper);
return northPanel;
}
private JPanel getTitleAndButtonPanel()
{
titlePanel.setLayout(new BorderLayout());
titlePanel.setBackground(ColorScheme.DARK_GRAY_COLOR);
titlePanel.setPreferredSize(new Dimension(0, 30));
titlePanel.setBorder(new EmptyBorder(5, 5, 5, 10));
JLabel title = new JLabel("Tasks Tracker");
title.setHorizontalAlignment(SwingConstants.LEFT);
title.setForeground(Color.WHITE);
JPopupMenu reloadPluginPopup = new JPopupMenu();
reloadPluginPopup.setBorder(new EmptyBorder(5, 5, 5, 5));
JMenuItem reloadPluginMenuItem = new JMenuItem("Reload plugin");
reloadPluginMenuItem.addActionListener(e -> plugin.reloadTaskType());
reloadPluginPopup.add(reloadPluginMenuItem);
title.setComponentPopupMenu(reloadPluginPopup);
// Filter button bar
final JPanel viewControls = new JPanel();
viewControls.setLayout(new BoxLayout(viewControls, BoxLayout.X_AXIS));
viewControls.setBackground(ColorScheme.DARK_GRAY_COLOR);
// Completed tasks filter button
SwingUtil.removeButtonDecorations(completedFilterBtn);
completedFilterBtn.setIcons(Icons.COMPLETE_INCOMPLETE_ICON, Icons.COMPLETE_ONLY_ICON, Icons.INCOMPLETE_ONLY_ICON);
completedFilterBtn.setToolTips("All tasks", "Completed tasks only", "Incomplete tasks only");
completedFilterBtn.setBackground(ColorScheme.DARK_GRAY_COLOR);
completedFilterBtn.setStateChangedAction(e -> filterButtonAction("completed"));
completedFilterBtn.popupMenuEnabled(true);
completedFilterBtn.setState(config.completedFilter().ordinal());
viewControls.add(completedFilterBtn);
// Tracked tasks filter button
SwingUtil.removeButtonDecorations(trackedFilterBtn);
trackedFilterBtn.setIcons(Icons.TRACKED_UNTRACKED_ICON, Icons.TRACKED_ONLY_ICON, Icons.UNTRACKED_ONLY_ICON);
trackedFilterBtn.setToolTips("All tasks", "Tracked tasks only", "Untracked tasks only");
trackedFilterBtn.setBackground(ColorScheme.DARK_GRAY_COLOR);
trackedFilterBtn.setStateChangedAction(e -> filterButtonAction("tracked"));
trackedFilterBtn.popupMenuEnabled(true);
trackedFilterBtn.setState(config.trackedFilter().ordinal());
viewControls.add(trackedFilterBtn);
// Ignored tasks filter button
SwingUtil.removeButtonDecorations(ignoredFilterBtn);
ignoredFilterBtn.setIcons(Icons.SEMIVISIBLE_ICON, Icons.VISIBLE_ICON, Icons.INVISIBLE_ICON);
ignoredFilterBtn.setToolTips("Hide ignored tasks", "All tasks", "Ignored tasks only");
ignoredFilterBtn.setBackground(ColorScheme.DARK_GRAY_COLOR);
ignoredFilterBtn.setStateChangedAction(e -> filterButtonAction("ignored"));
ignoredFilterBtn.popupMenuEnabled(true);
ignoredFilterBtn.setState(config.ignoredFilter().ordinal());
viewControls.add(ignoredFilterBtn);
titlePanel.add(viewControls, BorderLayout.EAST);
titlePanel.add(title, BorderLayout.WEST);
titlePanel.setAlignmentX(LEFT_ALIGNMENT);
return titlePanel;
}
private void filterButtonActionNoRefresh(String filter)
{
int state;
Enum configValue;
switch (filter)
{
case "completed":
state = completedFilterBtn.getState();
configValue = ConfigValues.CompletedFilterValues.values()[state];
break;
case "tracked":
state = trackedFilterBtn.getState();
configValue = ConfigValues.TrackedFilterValues.values()[state];
break;
case "ignored":
state = ignoredFilterBtn.getState();
configValue = ConfigValues.IgnoredFilterValues.values()[state];
break;
default:
log.debug("Filter button action failed due to unrecognised filter.");
return;
}
plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, filter + "Filter", configValue);
}
private void filterButtonAction(String filter)
{
filterButtonActionNoRefresh(filter);
plugin.refreshAllTasks();
}
private void actionAllFilterButtons()
{
filterButtonActionNoRefresh("tracked");
filterButtonActionNoRefresh("ignored");
filterButtonActionNoRefresh("completed");
plugin.refreshAllTasks();
}
private JPanel getSearchPanel()
{
JPanel filtersPanel = new JPanel();
filtersPanel.setAlignmentX(LEFT_ALIGNMENT);
filtersPanel.setLayout(new BoxLayout(filtersPanel, BoxLayout.Y_AXIS));
SearchBox textSearch = new SearchBox();
textSearch.addTextChangedListener(() -> {
plugin.taskTextFilter = textSearch.getText().toLowerCase();
plugin.refreshAllTasks();
});
filtersPanel.add(textSearch);
return filtersPanel;
}
private void updateCollapseButtonText()
{
if (taskService.getCurrentTaskType() == null) return;
ArrayList<FilterConfig> filters = taskService.getCurrentTaskType().getFilters();
int countInclusive = 0;
int countExclusive = 0;
for (FilterConfig filterConfig : filters)
{
String filterText = Optional.ofNullable(plugin.getConfigManager()
.getConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME,
taskService.getCurrentTaskType().getFilterConfigPrefix() + filterConfig.getConfigKey()))
.orElse("");
int count = (filterText.isEmpty() || filterText.equals("-1")) ? 0 : filterText.split(",").length;
if (filterConfig.getFilterType().equals(FilterType.BUTTON_FILTER)) countInclusive += count;
if (filterConfig.getFilterType().equals(FilterType.DROPDOWN_FILTER)) countExclusive += count;
}
collapseBtn.setText(countInclusive + " inclusive, " + countExclusive + " exclusive filters");
}
private void initTaskTypeDropdownAsync() {
TaskType currentTaskType = taskService.getCurrentTaskType();
taskService.getTaskTypesByJsonName().thenAccept(taskTypes -> {
ArrayList<ComboItem<TaskType>> taskTypeItems = new ArrayList<>();
taskTypes.forEach((taskTypeJsonName, taskType) -> {
ComboItem<TaskType> item = new ComboItem<>(taskType, taskType.getName());
taskTypeItems.add(item);
taskTypeDropdown.addItem(item);
});
ComboItem<TaskType> currentTaskTypeComboItem = taskTypeItems.stream()
.filter(item -> item.getValue().equals(currentTaskType))
.findFirst().orElseGet(() -> taskTypeItems.get(0));
taskTypeDropdown.setSelectedItem(currentTaskTypeComboItem);
taskTypeDropdown.addActionListener(e -> {
TaskType taskType = taskTypeDropdown.getItemAt(taskTypeDropdown.getSelectedIndex()).getValue();
taskService.setTaskType(taskType).thenAccept(wasTaskTypeChanged ->{
if (wasTaskTypeChanged) {
SwingUtilities.invokeLater(() ->
{
redraw();
plugin.refreshAllTasks();
});
}
});
});
});
}
}

View File

@@ -0,0 +1,12 @@
package net.reldo.taskstracker.panel;
import javax.swing.JLabel;
import javax.swing.JPanel;
public class LoggedOutPanel extends JPanel
{
public LoggedOutPanel()
{
this.add(new JLabel("Log into an account to track tasks."));
}
}

View File

@@ -0,0 +1,85 @@
package net.reldo.taskstracker.panel;
import java.util.List;
import java.util.stream.Collectors;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.JComboBox;
import javax.swing.SwingUtilities;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.TasksTrackerPlugin;
import net.reldo.taskstracker.config.ConfigValues;
import net.reldo.taskstracker.data.task.TaskService;
import net.reldo.taskstracker.panel.components.FixedWidthPanel;
import net.reldo.taskstracker.panel.components.MultiToggleButton;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.util.SwingUtil;
@Slf4j
public class SortPanel extends FixedWidthPanel
{
private final TasksTrackerPlugin plugin;
private final TaskService taskService;
private final TaskListPanel taskListPanel;
private final ConfigManager configManager;
private JComboBox<String> sortDropdown;
private MultiToggleButton directionButton;
public SortPanel(TasksTrackerPlugin plugin, TaskService taskService, TaskListPanel taskListPanel)
{
this.plugin = plugin;
this.configManager = plugin.getConfigManager();
this.taskService = taskService;
this.taskListPanel = taskListPanel;
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setAlignmentX(LEFT_ALIGNMENT);
}
public void redraw()
{
removeAll();
List<String> criteriaList = taskService.getSortedIndexes().keySet().stream()
.sorted()
.map((str) -> str.substring(0, 1).toUpperCase() + str.substring(1))
.collect(Collectors.toList());
criteriaList.add(0,"Default");
String[] criteriaArray = criteriaList.toArray(new String[0]);
sortDropdown = new JComboBox<>(criteriaArray);
sortDropdown.setAlignmentX(LEFT_ALIGNMENT);
sortDropdown.setSelectedIndex(0);
sortDropdown.addActionListener(e -> {
updateConfig();
SwingUtilities.invokeLater(taskListPanel::redraw);
});
sortDropdown.setFocusable(false);
directionButton = new MultiToggleButton(2);
SwingUtil.removeButtonDecorations(directionButton);
directionButton.setIcons(new Icon[]{Icons.ASCENDING_ICON, Icons.DESCENDING_ICON});
directionButton.setToolTips(new String[]{"Ascending", "Descending"});
directionButton.setBackground(ColorScheme.DARK_GRAY_COLOR);
directionButton.setStateChangedAction(e -> {
updateConfig();
SwingUtilities.invokeLater(taskListPanel::redraw);
});
add(sortDropdown);
add(directionButton);
updateConfig();
}
protected void updateConfig()
{
log.debug("updateConfig {}, {}, {}", TasksTrackerPlugin.CONFIG_GROUP_NAME, "sortCriteria", sortDropdown.getItemAt(sortDropdown.getSelectedIndex()).toLowerCase());
configManager.setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "sortCriteria", sortDropdown.getItemAt(sortDropdown.getSelectedIndex()).toLowerCase());
ConfigValues.SortDirections configValue = ConfigValues.SortDirections.values()[directionButton.getState()];
log.debug("updateConfig {}, {}, {}", TasksTrackerPlugin.CONFIG_GROUP_NAME, "sortDirection", configValue);
configManager.setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "sortDirection", configValue);
}
}

View File

@@ -0,0 +1,122 @@
package net.reldo.taskstracker.panel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import javax.swing.BoxLayout;
import javax.swing.border.EmptyBorder;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.TasksTrackerPlugin;
import net.reldo.taskstracker.data.jsondatastore.types.FilterConfig;
import net.reldo.taskstracker.data.jsondatastore.types.FilterValueType;
import net.reldo.taskstracker.data.task.TaskService;
import net.reldo.taskstracker.panel.components.FixedWidthPanel;
import net.reldo.taskstracker.panel.filters.ComboItem;
import net.reldo.taskstracker.panel.filters.DynamicButtonFilterPanel;
import net.reldo.taskstracker.panel.filters.DynamicDropdownFilterPanel;
import net.reldo.taskstracker.panel.filters.FilterPanel;
import net.runelite.client.ui.ColorScheme;
@Slf4j
public class SubFilterPanel extends FixedWidthPanel
{
private final List<FilterPanel> filterPanels = new ArrayList<>();
private TasksTrackerPlugin plugin;
private TaskService taskService;
public SubFilterPanel(TasksTrackerPlugin plugin, TaskService taskService)
{
this.plugin = plugin;
this.taskService = taskService;
log.debug("SubFilterPanel.constructor");
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
setBorder(new EmptyBorder(0, 0, 0, 0));
setBackground(ColorScheme.DARKER_GRAY_COLOR);
setVisible(false);
}
public void redraw()
{
log.debug("SubFilterPanel.redraw"); // TODO: figure out why this calls multiple times upon switching task type
removeAll();
filterPanels.clear();
filterPanels.addAll(getFilterPanels(taskService.getCurrentTaskType().getFilters()));
filterPanels.forEach((filterPanel) -> {
add(filterPanel);
filterPanel.redraw();
});
}
private List<FilterPanel> getFilterPanels(ArrayList<FilterConfig> filterConfigs)
{
List<FilterPanel> filterPanels = new ArrayList<>();
for (FilterConfig filterConfig : filterConfigs)
{
try
{
FilterPanel filterPanel = createDynamicFilterPanel(filterConfig);
if (filterPanel == null)
{
continue;
}
filterPanels.add(filterPanel);
}
catch (Exception ex)
{
log.error("error creating filter panel {} {}", filterConfig.getConfigKey(), ex);
}
}
return filterPanels;
}
private FilterPanel createDynamicFilterPanel(FilterConfig filterConfig) throws Exception
{
switch (filterConfig.getFilterType())
{
case BUTTON_FILTER:
return new DynamicButtonFilterPanel(plugin, filterConfig, taskService.getCurrentTaskType());
case DROPDOWN_FILTER:
ComboItem[] dropdownItems = getDropdownItems(filterConfig);
return new DynamicDropdownFilterPanel(plugin, filterConfig, taskService.getCurrentTaskType(), dropdownItems);
default:
log.error("invalid filter type " + filterConfig.getFilterType());
return null;
}
}
private ComboItem[] getDropdownItems(FilterConfig filterConfig) throws ExecutionException, InterruptedException
{
if (filterConfig.getValueType() == null)
{
throw new Error("invalid filterConfig for dropdown items");
}
if (filterConfig.getValueType().equals(FilterValueType.PARAM_INTEGER))
{
String enumName = filterConfig.getOptionLabelEnum();
if (!enumName.isEmpty())
{
HashMap<Integer, String> enumEntries = taskService.getStringEnumValuesAsync(enumName).get(); // TODO: blocking call
ArrayList<ComboItem<Integer>> options = new ArrayList<>();
options.add(new ComboItem<>(-1, ""));
for (Map.Entry<Integer, String> entry : enumEntries.entrySet())
{
if (filterConfig.getValueName().equals("tier"))
{
if (entry.getValue().equals("All") || entry.getValue().equals("Tier"))
{
continue;
}
}
options.add(new ComboItem<>(entry.getKey(), entry.getValue()));
}
return options.toArray(new ComboItem[0]);
}
}
return new ComboItem[0];
}
}

View File

@@ -0,0 +1,202 @@
package net.reldo.taskstracker.panel;
import java.awt.Component;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import javax.swing.BoxLayout;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.TasksTrackerPlugin;
import net.reldo.taskstracker.config.ConfigValues;
import net.reldo.taskstracker.data.jsondatastore.types.TaskDefinitionSkill;
import net.reldo.taskstracker.data.task.TaskFromStruct;
import net.reldo.taskstracker.data.task.TaskService;
import net.reldo.taskstracker.panel.components.FixedWidthPanel;
import net.runelite.api.Skill;
import net.runelite.client.ui.FontManager;
@Slf4j
public class TaskListPanel extends JScrollPane
{
public TasksTrackerPlugin plugin;
private final HashMap<Integer, TaskPanel> taskPanelsByStructId = new HashMap<>();
private final TaskListListPanel taskList;
private final TaskService taskService;
private final JLabel emptyTasks = new JLabel();
public TaskListPanel(TasksTrackerPlugin plugin, TaskService taskService)
{
this.plugin = plugin;
taskList = new TaskListListPanel(plugin);
this.taskService = taskService;
setViewportView(taskList);
setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
}
public String getEmptyTaskListMessage()
{
return "No tasks match the current filters.";
}
public void redraw()
{
taskList.redraw();
}
public void refreshAllTasks()
{
log.debug("TaskListPanel.refreshAllTasks");
if (!SwingUtilities.isEventDispatchThread())
{
log.error("Task list panel refresh failed - not event dispatch thread.");
return;
}
for (TaskPanel taskPanel : taskPanelsByStructId.values())
{
taskPanel.refresh();
}
}
public void refreshMultipleTasks(Collection<TaskFromStruct> tasks)
{
log.debug("TaskListPanel.refreshMultipleTasks {}", tasks.size());
if (!SwingUtilities.isEventDispatchThread())
{
log.error("Task list panel refresh failed - not event dispatch thread.");
return;
}
for (TaskFromStruct task : tasks)
{
refresh(task);
}
}
public void refreshTask(TaskFromStruct task)
{
log.debug("TaskListPanel.refreshMultipleTasks {}", task.getName());
refresh(task);
}
private void refresh(TaskFromStruct task)
{
if (!SwingUtilities.isEventDispatchThread())
{
log.error("Task list panel refresh failed - not event dispatch thread.");
return;
}
if (task == null)
{
log.debug("Attempted to refresh null task");
return;
}
emptyTasks.setVisible(false);
TaskPanel panel = taskPanelsByStructId.get(task.getStructId());
if (panel != null)
{
panel.refresh();
}
boolean isAnyTaskPanelVisible = taskPanelsByStructId.values().stream()
.anyMatch(TaskPanel::isVisible);
if (!isAnyTaskPanelVisible)
{
emptyTasks.setVisible(true);
}
}
public void refreshTaskPanelsWithSkill(Skill skill)
{
// Refresh all task panels for tasks with 'skill' or
// 'SKILLS' (any skill) or 'TOTAL LEVEL' as a requirement.
taskPanelsByStructId.values().stream()
.filter(tp ->
{
List<TaskDefinitionSkill> skillsList = tp.task.getTaskDefinition().getSkills();
if (skillsList == null || skillsList.isEmpty())
{
return false;
}
return skillsList.stream()
.map(TaskDefinitionSkill::getSkill)
.anyMatch(s -> s.equalsIgnoreCase(skill.getName()) ||
s.equalsIgnoreCase("SKILLS") ||
s.equalsIgnoreCase("TOTAL LEVEL")
);
})
.forEach(TaskPanel::refresh);
}
private class TaskListListPanel extends FixedWidthPanel
{
private final TasksTrackerPlugin plugin;
public TaskListListPanel(TasksTrackerPlugin plugin)
{
this.plugin = plugin;
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
setBorder(new EmptyBorder(0, 10, 10, 10));
setAlignmentX(Component.LEFT_ALIGNMENT);
emptyTasks.setBorder(new EmptyBorder(10,0,10,0));
emptyTasks.setText("<html><center>" + getEmptyTaskListMessage() + "</center></html>");
emptyTasks.setFont(FontManager.getRunescapeSmallFont());
emptyTasks.setHorizontalAlignment(JLabel.CENTER);
emptyTasks.setVerticalAlignment(JLabel.CENTER);
add(emptyTasks);
emptyTasks.setVisible(false);
}
public void redraw()
{
log.debug("TaskListPanel.redraw");
if(SwingUtilities.isEventDispatchThread())
{
removeAll();
taskPanelsByStructId.clear();
add(emptyTasks);
emptyTasks.setVisible(false);
log.debug("TaskListPanel creating panels");
List<TaskFromStruct> tasks = taskService.getTasks();
if (tasks == null || tasks.isEmpty())
{
emptyTasks.setVisible(true);
return;
}
for (int indexPosition = 0; indexPosition < tasks.size(); indexPosition++)
{
int adjustedIndexPosition = indexPosition;
if (plugin.getConfig().sortDirection().equals(ConfigValues.SortDirections.DESCENDING))
{
adjustedIndexPosition = tasks.size() - (adjustedIndexPosition + 1);
}
TaskFromStruct task = tasks.get(taskService.getSortedTaskIndex(plugin.getConfig().sortCriteria(), adjustedIndexPosition));
TaskPanel taskPanel = new TaskPanel(plugin, task);
add(taskPanel);
taskPanelsByStructId.put(task.getStructId(), taskPanel);
}
log.debug("TaskListPanel validate and repaint");
validate();
repaint();
}
else
{
log.error("Task list panel redraw failed - not event dispatch thread.");
}
}
}
}

View File

@@ -0,0 +1,429 @@
package net.reldo.taskstracker.panel;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JToggleButton;
import javax.swing.JToolTip;
import javax.swing.ToolTipManager;
import javax.swing.border.EmptyBorder;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.HtmlUtil;
import net.reldo.taskstracker.TasksTrackerConfig;
import net.reldo.taskstracker.TasksTrackerPlugin;
import net.reldo.taskstracker.config.ConfigValues.CompletedFilterValues;
import net.reldo.taskstracker.config.ConfigValues.IgnoredFilterValues;
import net.reldo.taskstracker.config.ConfigValues.TrackedFilterValues;
import net.reldo.taskstracker.data.jsondatastore.types.FilterType;
import net.reldo.taskstracker.data.jsondatastore.types.TaskDefinitionSkill;
import net.reldo.taskstracker.data.task.TaskFromStruct;
import net.reldo.taskstracker.data.task.filters.Filter;
import net.reldo.taskstracker.data.task.filters.ParamButtonFilter;
import net.reldo.taskstracker.data.task.filters.ParamDropdownFilter;
import net.runelite.api.Constants;
import net.runelite.api.Skill;
import net.runelite.client.game.SkillIconManager;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.ui.FontManager;
import net.runelite.client.ui.PluginPanel;
import net.runelite.client.util.SwingUtil;
@Slf4j
public class TaskPanel extends JPanel
{
public final TaskFromStruct task;
private final JLabel tierIcon = new JLabel();
private final JPanel container = new JPanel(new BorderLayout());
private final JPanel body = new JPanel(new BorderLayout());
private final JLabel name = new JLabel("task");
private final JLabel description = new JLabel("description");
private final JPanel buttons = new JPanel();
private final JToggleButton toggleTrack = new JToggleButton();
private final JToggleButton toggleIgnore = new JToggleButton();
protected final ArrayList<Filter> filters = new ArrayList<>();
protected TasksTrackerPlugin plugin;
public TaskPanel(TasksTrackerPlugin plugin, TaskFromStruct task)
{
super(new BorderLayout());
this.plugin = plugin;
this.task = task;
createPanel();
setComponentPopupMenu(getPopupMenu());
ToolTipManager.sharedInstance().registerComponent(this);
task.getTaskType().getFilters().forEach((filterConfig) -> {
String paramName = filterConfig.getValueName();
if (filterConfig.getFilterType().equals(FilterType.BUTTON_FILTER))
{
Filter filter = new ParamButtonFilter(plugin.getConfigManager(), paramName, task.getTaskType().getTaskJsonName() + "." + filterConfig.getConfigKey());
filters.add(filter);
}
else if (filterConfig.getFilterType().equals(FilterType.DROPDOWN_FILTER))
{
Filter filter = new ParamDropdownFilter(plugin.getConfigManager(), paramName, task.getTaskType().getTaskJsonName() + "." + filterConfig.getConfigKey());
filters.add(filter);
}
});
refresh();
}
public JPopupMenu getPopupMenu()
{
return null;
}
public String getTaskTooltip()
{
StringBuilder tooltipText = new StringBuilder();
tooltipText.append(HtmlUtil.wrapWithBold(task.getName())).append(HtmlUtil.HTML_LINE_BREAK);
tooltipText.append(task.getDescription()).append(HtmlUtil.HTML_LINE_BREAK);
String skillSection = getSkillSectionHtml();
if (skillSection != null)
{
tooltipText.append(skillSection).append(HtmlUtil.HTML_LINE_BREAK);
}
String wikiNotes = task.getTaskDefinition().getWikiNotes();
if (wikiNotes != null)
{
tooltipText.append(HtmlUtil.HTML_LINE_BREAK).append(wikiNotes).append(HtmlUtil.HTML_LINE_BREAK);
}
if (task.isCompleted())
{
tooltipText.append(HtmlUtil.HTML_LINE_BREAK);
String datePattern = "MM-dd-yyyy hh:mma";
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
tooltipText.append("").append(simpleDateFormat.format(new Date(task.getCompletedOn())));
}
Float completionPercent = task.getTaskDefinition().getCompletionPercent();
if (completionPercent != null)
{
tooltipText.append(HtmlUtil.HTML_LINE_BREAK).append("Players Completed: ").append(completionPercent).append('%');
}
return HtmlUtil.wrapWithHtml(
HtmlUtil.wrapWithWrappingParagraph(tooltipText.toString(), 200)
);
}
public Color getTaskBackgroundColor()
{
if (plugin.playerSkills == null)
{
return ColorScheme.DARKER_GRAY_COLOR;
}
if (task.isCompleted())
{
return Colors.COMPLETED_BACKGROUND_COLOR;
}
if (task.getTaskDefinition().getSkills() == null || task.getTaskDefinition().getSkills().size() == 0)
{
return ColorScheme.DARKER_GRAY_COLOR;
}
for (TaskDefinitionSkill requiredSkill : task.getTaskDefinition().getSkills())
{
Skill skill;
String requiredSkillName = requiredSkill.getSkill().toUpperCase();
try
{
skill = Skill.valueOf(requiredSkillName);
}
catch (IllegalArgumentException ex)
{
log.error("invalid skill name " + requiredSkillName);
continue;
}
if (plugin.playerSkills[skill.ordinal()] < requiredSkill.getLevel())
{
return Colors.UNQUALIFIED_BACKGROUND_COLOR;
}
}
return ColorScheme.DARKER_GRAY_COLOR;
}
public void createPanel()
{
setLayout(new BorderLayout());
setBorder(new EmptyBorder(0, 0, 7, 0));
container.setBorder(new EmptyBorder(7, 7, 6, 0));
// Body
name.setFont(FontManager.getRunescapeSmallFont());
name.setForeground(Color.WHITE);
body.add(name, BorderLayout.NORTH);
description.setFont(FontManager.getRunescapeSmallFont());
description.setForeground(Color.GRAY);
body.add(description, BorderLayout.CENTER);
// Buttons
buttons.setLayout(new BoxLayout(buttons, BoxLayout.Y_AXIS));
buttons.setBorder(new EmptyBorder(0, 0, 0, 7));
toggleTrack.setPreferredSize(new Dimension(8, 8));
toggleTrack.setIcon(Icons.PLUS_ICON);
toggleTrack.setSelectedIcon(Icons.MINUS_ICON);
toggleTrack.setBorder(new EmptyBorder(5, 0, 5, 0));
toggleTrack.addActionListener(e -> {
task.setTracked(toggleTrack.isSelected());
plugin.pluginPanel.taskListPanel.refreshTask(task);
plugin.saveCurrentTaskTypeData();
});
SwingUtil.removeButtonDecorations(toggleTrack);
toggleIgnore.setPreferredSize(new Dimension(8, 8));
toggleIgnore.setIcon(Icons.EYE_CROSS_GREY);
toggleIgnore.setSelectedIcon(Icons.EYE_ICON);
SwingUtil.addModalTooltip(toggleIgnore, "Unignore", "Ignore");
toggleIgnore.setBorder(new EmptyBorder(5, 0, 5, 0));
toggleIgnore.addActionListener(e -> {
task.setIgnored(!task.isIgnored());
plugin.pluginPanel.taskListPanel.refreshTask(task);
plugin.saveCurrentTaskTypeData();
});
SwingUtil.removeButtonDecorations(toggleIgnore);
buttons.add(toggleTrack);
buttons.add(toggleIgnore);
// Full
container.add(tierIcon, BorderLayout.WEST);
container.add(body, BorderLayout.CENTER);
container.add(buttons, BorderLayout.EAST);
BufferedImage tierSprite = task.getTaskType().getTierSprites().get(task.getTier());
if (tierSprite != null)
{
tierIcon.setMinimumSize(new Dimension(Constants.ITEM_SPRITE_WIDTH, Constants.ITEM_SPRITE_HEIGHT));
tierIcon.setIcon(new ImageIcon(tierSprite));
tierIcon.setBorder(new EmptyBorder(0, 0, 0, 5));
}
else
{
tierIcon.setBorder(new EmptyBorder(0, 0, 0, 0));
}
add(container, BorderLayout.NORTH);
addMouseListener(new MouseAdapter()
{
@Override
public void mouseReleased(MouseEvent e)
{
if (e.isPopupTrigger())
{
JPopupMenu menu = createWikiPopupMenu();
menu.show(e.getComponent(), e.getX(), e.getY());
}
}
});
}
public JPopupMenu createWikiPopupMenu()
{
JPopupMenu popupMenu = new JPopupMenu();
JMenuItem openWikiItem = new JMenuItem("Wiki");
openWikiItem.addActionListener(e -> openRuneScapeWiki());
popupMenu.add(openWikiItem);
return popupMenu;
}
private void openRuneScapeWiki()
{
String wikiUrl = String.format("https://oldschool.runescape.wiki/%s", URLEncoder.encode(task.getName().replace(' ', '_'), StandardCharsets.UTF_8));
if (Desktop.isDesktopSupported())
{
try
{
Desktop.getDesktop().browse(new URI(wikiUrl));
}
catch (IOException | URISyntaxException ex)
{
ex.printStackTrace();
}
}
else
{
log.warn("Desktop browsing is not supported on this system.");
}
}
public void refresh()
{
setBackgroundColor(getTaskBackgroundColor());
name.setText(HtmlUtil.wrapWithHtml(task.getName()));
description.setText(HtmlUtil.wrapWithHtml(task.getDescription()));
toggleTrack.setSelected(task.isTracked());
toggleIgnore.setSelected(task.isIgnored());
setVisible(meetsFilterCriteria());
revalidate();
}
protected boolean meetsFilterCriteria()
{
String nameLowercase = task.getName().toLowerCase();
String descriptionLowercase = task.getDescription().toLowerCase();
if (plugin.taskTextFilter != null &&
!nameLowercase.contains(plugin.taskTextFilter) &&
!descriptionLowercase.contains(plugin.taskTextFilter))
{
return false;
}
TasksTrackerConfig config = plugin.getConfig();
for (Filter filter : filters)
{
if (!filter.meetsCriteria(task))
{
return false;
}
}
if (config.completedFilter().equals(CompletedFilterValues.INCOMPLETE) && task.isCompleted())
{
return false;
}
if (config.completedFilter().equals(CompletedFilterValues.COMPLETE) && !task.isCompleted())
{
return false;
}
if (config.ignoredFilter().equals(IgnoredFilterValues.NOT_IGNORED) && task.isIgnored())
{
return false;
}
if (config.ignoredFilter().equals(IgnoredFilterValues.IGNORED) && !task.isIgnored())
{
return false;
}
if (config.trackedFilter().equals(TrackedFilterValues.UNTRACKED) && task.isTracked())
{
return false;
}
return !config.trackedFilter().equals(TrackedFilterValues.TRACKED) || task.isTracked();
}
private void setBackgroundColor(Color color)
{
container.setBackground(color);
body.setBackground(color);
buttons.setBackground(color);
}
@Override
public Dimension getMaximumSize()
{
return new Dimension(PluginPanel.PANEL_WIDTH, getPreferredSize().height);
}
@Override
public JToolTip createToolTip()
{
JToolTip customTooltip = new JToolTip();
customTooltip.setFont(FontManager.getRunescapeSmallFont());
return customTooltip;
}
@Override
public String getToolTipText(MouseEvent mouseEvent)
{
return getTaskTooltip();
}
private String getSkillSectionHtml()
{
List<TaskDefinitionSkill> requiredSkills = task.getTaskDefinition().getSkills();
if (requiredSkills == null)
{
return null;
}
StringBuilder skillSection = new StringBuilder();
skillSection.append(HtmlUtil.HTML_LINE_BREAK);
for (TaskDefinitionSkill requiredSkill : requiredSkills)
{
Skill skill;
try
{
skill = Skill.valueOf(requiredSkill.getSkill().toUpperCase());
}
catch (IllegalArgumentException ex)
{
log.warn("unknown skill: {}", requiredSkill.getSkill().toUpperCase(), ex);
continue;
}
Integer requiredLevel = requiredSkill.getLevel();
int playerLevel = -1;
if (requiredLevel == null)
{
continue;
}
if (plugin.playerSkills != null)
{
playerLevel = plugin.playerSkills[skill.ordinal()];
}
String skillMessage = getSkillRequirementHtml(requiredSkill.getSkill().toLowerCase(), playerLevel, requiredLevel);
skillSection.append(skillMessage).append(" ");
}
return skillSection.toString();
}
private String getSkillRequirementHtml(String skillName, Integer playerLevel, int requiredLevel)
{
String skillIconPath = "/skill_icons_small/" + skillName + ".png";
URL url = SkillIconManager.class.getResource(skillIconPath);
Color color = playerLevel >= requiredLevel ? Colors.QUALIFIED_TEXT_COLOR : Colors.UNQUALIFIED_TEXT_COLOR;
return HtmlUtil.imageTag(url) + " " + HtmlUtil.colorTag(color, playerLevel + "/" + requiredLevel);
}
private String getPointsTooltipText()
{
int points = this.task.getPoints();
if (points == 0)
{
return "";
}
return " - " + points + " points";
}
}

View File

@@ -0,0 +1,8 @@
package net.reldo.taskstracker.panel;
import net.reldo.taskstracker.data.task.TaskFromStruct;
public interface TaskPanelFactory
{
TaskPanel create(TaskFromStruct task);
}

View File

@@ -0,0 +1,108 @@
package net.reldo.taskstracker.panel;
import java.awt.BorderLayout;
import java.awt.Dimension;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.TasksTrackerConfig;
import net.reldo.taskstracker.TasksTrackerPlugin;
import net.reldo.taskstracker.data.task.TaskService;
import net.runelite.client.game.SpriteManager;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.ui.PluginPanel;
@Slf4j
public class TasksTrackerPluginPanel extends PluginPanel
{
private final LoggedInPanel loggedInPanel;
private final LoggedOutPanel loggedOutPanel = new LoggedOutPanel();
public TaskListPanel taskListPanel;
private boolean loggedInPanelVisible = false;
public TasksTrackerPluginPanel(TasksTrackerPlugin plugin, TasksTrackerConfig config, SpriteManager spriteManager, TaskService taskService)
{
super(false);
setBorder(new EmptyBorder(6, 6, 6, 6));
setBackground(ColorScheme.DARK_GRAY_COLOR);
setLayout(new BorderLayout());
loggedInPanel = new LoggedInPanel(plugin, config, taskService);
taskListPanel = loggedInPanel.taskListPanel;
add(loggedInPanel, BorderLayout.NORTH);
loggedInPanel.setVisible(false);
// Add error pane
add(loggedOutPanel);
}
@Override
public Dimension getPreferredSize()
{
return new Dimension(PANEL_WIDTH + SCROLLBAR_WIDTH, super.getPreferredSize().height);
}
public void redraw()
{
if (loggedInPanelVisible)
{
loggedInPanel.redraw();
}
}
public void refreshAllTasks()
{
if (loggedInPanelVisible)
{
loggedInPanel.refreshAllTasks();
}
}
public void setLoggedIn(boolean loggedIn)
{
if(SwingUtilities.isEventDispatchThread())
{
updateVisiblePanel(loggedIn);
}
else
{
log.error("Failed to update loggedIn state - not event dispatch thread.");
}
}
public void hideLoggedInPanel()
{
if(SwingUtilities.isEventDispatchThread())
{
updateVisiblePanel(false);
}
else
{
log.error("Failed to update logged in panel visibility - not event dispatch thread.");
}
}
private void updateVisiblePanel(boolean loggedInPanelVisible)
{
if (loggedInPanelVisible != this.loggedInPanelVisible)
{
if (loggedInPanelVisible)
{
loggedOutPanel.setVisible(false);
loggedInPanel.setVisible(true);
}
else
{
loggedInPanel.setVisible(false);
loggedOutPanel.setVisible(true);
}
validate();
repaint();
}
this.loggedInPanelVisible = loggedInPanelVisible;
}
}

View File

@@ -0,0 +1,42 @@
package net.reldo.taskstracker.panel.components;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionListener;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import net.runelite.client.ui.ColorScheme;
import static net.runelite.client.ui.PluginPanel.PANEL_WIDTH;
public class CheckBox extends JPanel
{
private final JCheckBox jCheckBox = new JCheckBox();
public CheckBox(String name)
{
setLayout(new BorderLayout());
setMinimumSize(new Dimension(PANEL_WIDTH, 0));
JLabel label = new JLabel(name);
add(label, BorderLayout.CENTER);
jCheckBox.setBackground(ColorScheme.LIGHT_GRAY_COLOR);
add(jCheckBox, BorderLayout.EAST);
}
public boolean isSelected()
{
return jCheckBox.isSelected();
}
public void setSelected(boolean selected)
{
jCheckBox.setSelected(selected);
}
public void addActionListener(ActionListener l)
{
jCheckBox.addActionListener(l);
}
}

View File

@@ -0,0 +1,15 @@
package net.reldo.taskstracker.panel.components;
import java.awt.Dimension;
import javax.swing.JPanel;
import net.runelite.client.ui.PluginPanel;
public class FixedWidthPanel extends JPanel
{
@Override
public Dimension getPreferredSize()
{
return new Dimension(PluginPanel.PANEL_WIDTH, super.getPreferredSize().height);
}
}

View File

@@ -0,0 +1,148 @@
package net.reldo.taskstracker.panel.components;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.border.EmptyBorder;
import lombok.Getter;
import lombok.Setter;
public class MultiToggleButton extends JButton
{
// Icons
private final Icon[] icons;
// Tooltips
private final String[] tooltips;
private final int stateCount;
@Setter
private ActionListener stateChangedAction = null;
@Getter
private int state = 0;
final JPopupMenu popupMenu = new JPopupMenu();
private boolean popupMenuEnabled = false;
public MultiToggleButton(int stateCount)
{
super();
this.stateCount = stateCount;
icons = new Icon[stateCount];
tooltips = new String[stateCount];
popupMenu.setBorder(new EmptyBorder(5, 5, 5, 5));
addActionListener(e -> changeStateThenAction());
}
public void popupMenuEnabled(boolean enabled)
{
if(popupMenuEnabled != enabled)
{
popupMenuEnabled = enabled;
if(popupMenuEnabled)
{
this.setComponentPopupMenu(popupMenu);
}
else
{
this.remove(popupMenu);
}
}
}
public void setIcon(Icon icon, int state)
{
if (state < 0 || state > stateCount || icon == null)
{
return;
}
icons[state] = icon;
if (state == this.state) setIconState();
}
public boolean setIcons(Icon[] icons)
{
if (icons == null || icons.length == 0)
{
return false;
}
for (int i = 0; i < icons.length; i++)
{
setIcon(icons[i], i);
}
return true;
}
public void setToolTip(String tooltip, int state)
{
if (state < 0 || state > 3 || tooltip == null)
{
return;
}
tooltips[state] = tooltip;
addPopupMenuItem(tooltip, state);
if (state == this.state) setTooltipState();
}
public boolean setToolTips(String[] tooltips)
{
if (tooltips == null || tooltips.length == 0)
{
return false;
}
for (int i = 0; i < tooltips.length; i++)
{
setToolTip(tooltips[i], i);
}
return true;
}
public void changeState()
{
setState((++state) % stateCount);
}
public void changeStateThenAction()
{
setStateThenAction((++state) % stateCount);
}
private void setIconState()
{
super.setIcon(icons[state]);
}
private void setTooltipState()
{
super.setToolTipText(tooltips[state]);
}
public void setState(int state)
{
this.state = state;
setIconState();
setTooltipState();
}
public void setStateThenAction(int state)
{
setState(state);
if(stateChangedAction != null) this.stateChangedAction.actionPerformed(new ActionEvent(this, 0, ""));
}
private void addPopupMenuItem(String text, int state)
{
JMenuItem menuItem = new JMenuItem(text);
menuItem.addActionListener(e -> {if(isEnabled())setState(state);});
popupMenu.add(menuItem);
}
}

View File

@@ -0,0 +1,53 @@
package net.reldo.taskstracker.panel.components;
import java.awt.Dimension;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.ui.PluginPanel;
import net.runelite.client.ui.components.IconTextField;
public class SearchBox extends IconTextField
{
private SearchBoxCallback fn;
public SearchBox()
{
this.setIcon(IconTextField.Icon.SEARCH);
this.setPreferredSize(new Dimension(PluginPanel.PANEL_WIDTH - 20, 30));
this.setBackground(ColorScheme.DARKER_GRAY_COLOR);
this.setHoverBackgroundColor(ColorScheme.DARK_GRAY_HOVER_COLOR);
this.getDocument().addDocumentListener(new DocumentListener()
{
@Override
public void insertUpdate(DocumentEvent documentEvent)
{
fn.call();
}
@Override
public void removeUpdate(DocumentEvent documentEvent)
{
fn.call();
}
@Override
public void changedUpdate(DocumentEvent documentEvent)
{
}
});
this.addActionListener(e -> fn.call()
);
}
public void addTextChangedListener(SearchBoxCallback fn)
{
this.fn = fn;
}
public interface SearchBoxCallback
{
void call();
}
}

View File

@@ -0,0 +1,39 @@
package net.reldo.taskstracker.panel.components;
import javax.swing.Icon;
public class TriToggleButton extends MultiToggleButton
{
public TriToggleButton()
{
super(3);
}
public boolean setIcons(Icon icon0, Icon icon1, Icon icon2)
{
if (icon0 == null || icon1 == null || icon2 == null)
{
return false;
}
setIcon(icon0, 0);
setIcon(icon1, 1);
setIcon(icon2, 2);
return true;
}
public boolean setToolTips(String tooltip0, String tooltip1, String tooltip2)
{
if (tooltip0 == null || tooltip1 == null || tooltip2 == null)
{
return false;
}
setToolTip(tooltip0, 0);
setToolTip(tooltip1, 1);
setToolTip(tooltip2, 2);
return true;
}
}

View File

@@ -0,0 +1,29 @@
package net.reldo.taskstracker.panel.filters;
public class ComboItem<T>
{
private T value;
private String label;
public ComboItem(T value, String label)
{
this.value = value;
this.label = label;
}
public T getValue()
{
return this.value;
}
public String getLabel()
{
return this.label;
}
@Override
public String toString()
{
return label;
}
}

View File

@@ -0,0 +1,131 @@
package net.reldo.taskstracker.panel.filters;
import com.google.common.collect.ImmutableList;
import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.image.BufferedImage;
import java.util.LinkedHashMap;
import java.util.List;
import javax.swing.JPanel;
import javax.swing.JToggleButton;
import javax.swing.border.EmptyBorder;
import net.reldo.taskstracker.TasksTrackerPlugin;
import net.reldo.taskstracker.data.jsondatastore.types.FilterConfig;
import net.reldo.taskstracker.data.jsondatastore.types.FilterCustomItem;
import net.reldo.taskstracker.data.task.TaskType;
import net.runelite.client.hiscore.HiscoreSkill;
import static net.runelite.client.hiscore.HiscoreSkill.*;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.util.ImageUtil;
public class DynamicButtonFilterPanel extends FilterButtonPanel
{
private final FilterConfig filterConfig;
private final TaskType taskType;
/**
* Real skills, ordered in the way they should be displayed in the panel.
*/
private static final List<HiscoreSkill> SKILLS = ImmutableList.of(
ATTACK, HITPOINTS, MINING,
STRENGTH, AGILITY, SMITHING,
DEFENCE, HERBLORE, FISHING,
RANGED, THIEVING, COOKING,
PRAYER, CRAFTING, FIREMAKING,
MAGIC, FLETCHING, WOODCUTTING,
RUNECRAFT, SLAYER, FARMING,
CONSTRUCTION, HUNTER
);
public DynamicButtonFilterPanel(TasksTrackerPlugin plugin, FilterConfig filterConfig, TaskType taskType)
{
super(plugin, filterConfig.getLabel());
this.filterConfig = filterConfig;
this.taskType = taskType;
this.configKey = taskType.getFilterConfigPrefix() + filterConfig.getConfigKey();
setLayout(new BorderLayout());
setBackground(ColorScheme.DARKER_GRAY_COLOR);
setBorder(new EmptyBorder(10, 10, 10, 10));
redraw();
}
@Override
protected JPanel makeButtonPanel()
{
// Panel that holds tier icons
JPanel buttonPanel = new JPanel();
buttonPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR);
LinkedHashMap<String, BufferedImage> buttonImages = getIconImages();
LinkedHashMap<String, String> buttonTooltips = getTooltips();
buttonPanel.setLayout(new GridLayout(buttonImages.size() / 3, 3));
// For each filter value create a button and add it to the UI
buttonImages.forEach((key, image) -> {
String tooltip = buttonTooltips.get(key);
JToggleButton button = makeButton(tooltip, image);
button.setSelected(getConfigButtonState(key));
buttons.put(key, button);
buttonPanel.add(button);
});
return buttonPanel;
}
@Override
protected LinkedHashMap<String, BufferedImage> getIconImages()
{
LinkedHashMap<String, BufferedImage> images = new LinkedHashMap<>();
if (filterConfig.getConfigKey().equals("skill"))
{
String skillName;
BufferedImage skillImage;
int index = 0;
for (FilterCustomItem customItem : filterConfig.getCustomItems())
{
if (customItem.getValue() != 0)
{
skillName = SKILLS.get(index).name().toLowerCase();
String directory = "/skill_icons_small/";
String skillIcon = directory + skillName + ".png";
skillImage = ImageUtil.loadImageResource(getClass(), skillIcon);
}
else
{
skillImage = ImageUtil.loadImageResource(TasksTrackerPlugin.class, "panel/components/no_skill.png");
}
String key = customItem.getValue().toString();
images.put(key, skillImage);
index++;
}
}
else
{
for (FilterCustomItem customItem : filterConfig.getCustomItems())
{
String key = customItem.getValue().toString();
images.put(key, taskType.getSpritesById().get(customItem.getSpriteId()));
}
}
return images;
}
private LinkedHashMap<String, String> getTooltips()
{
LinkedHashMap<String, String> tooltips = new LinkedHashMap<>();
for (FilterCustomItem customItem : filterConfig.getCustomItems())
{
String key = customItem.getValue().toString();
tooltips.put(key, customItem.getTooltip());
}
return tooltips;
}
}

View File

@@ -0,0 +1,92 @@
package net.reldo.taskstracker.panel.filters;
import java.awt.GridLayout;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.TasksTrackerPlugin;
import net.reldo.taskstracker.data.jsondatastore.types.FilterConfig;
import net.reldo.taskstracker.data.task.TaskType;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.ui.FontManager;
@Slf4j
public class DynamicDropdownFilterPanel extends FilterPanel
{
private final String configKey;
private final FilterConfig filterConfig;
private final ConfigManager configManager;
private final TasksTrackerPlugin plugin;
private final ComboItem[] items;
private JComboBox<ComboItem> dropdown;
public DynamicDropdownFilterPanel(TasksTrackerPlugin plugin, FilterConfig filterConfig, TaskType taskType, ComboItem[] items)
{
this.configManager = plugin.getConfigManager();
this.plugin = plugin;
this.filterConfig = filterConfig;
this.items = items;
this.configKey = taskType.getFilterConfigPrefix() + filterConfig.getConfigKey();
setLayout(new GridLayout(1,2));
setBackground(ColorScheme.DARKER_GRAY_COLOR);
setBorder(new EmptyBorder(5, 10, 5, 10));
}
private JComboBox<ComboItem> makeDropdownPanel()
{
JComboBox<ComboItem> dropdown = new JComboBox<>(items);
dropdown.setFont(FontManager.getRunescapeSmallFont());
dropdown.setAlignmentX(LEFT_ALIGNMENT);
dropdown.setSelectedItem(items[0]);
dropdown.setFocusable(false);
dropdown.setBackground(ColorScheme.DARK_GRAY_COLOR.brighter());
dropdown.addActionListener(e -> {
ComboItem selection = dropdown.getItemAt(dropdown.getSelectedIndex());
updateFilterConfig();
plugin.refreshAllTasks();
log.debug("selected: {} {}", selection.getLabel(), selection.getValue());
});
return dropdown;
}
private JLabel makeLabel()
{
JLabel label = new JLabel(filterConfig.getLabel() + ":");
label.setFont(FontManager.getRunescapeSmallFont());
label.setForeground(ColorScheme.LIGHT_GRAY_COLOR);
return label;
}
protected void updateFilterConfig()
{
log.debug("updateFilterConfig {}, {}, {}", TasksTrackerPlugin.CONFIG_GROUP_NAME, configKey, dropdown.getItemAt(dropdown.getSelectedIndex()).getValue());
configManager.setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, configKey, dropdown.getItemAt(dropdown.getSelectedIndex()).getValue());
}
public void redraw()
{
if(SwingUtilities.isEventDispatchThread())
{
removeAll();
dropdown = makeDropdownPanel();
add(makeLabel());
add(dropdown);
updateFilterConfig();
validate();
repaint();
}
else
{
log.error("Dropdown filter panel redraw failed - not event dispatch thread.");
}
}
}

View File

@@ -0,0 +1,223 @@
package net.reldo.taskstracker.panel.filters;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JToggleButton;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.basic.BasicBorders;
import javax.swing.plaf.basic.BasicButtonUI;
import lombok.extern.slf4j.Slf4j;
import net.reldo.taskstracker.TasksTrackerPlugin;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.ui.FontManager;
import net.runelite.client.util.ImageUtil;
import net.runelite.client.util.SwingUtil;
@Slf4j
public abstract class FilterButtonPanel extends FilterPanel
{
protected final TasksTrackerPlugin plugin;
private final String label;
protected final Map<String, JToggleButton> buttons = new HashMap<>();
protected String configKey;
protected JPanel buttonPanel;
protected JToggleButton collapseBtn;
private final String expandBtnPath = "panel/components/";
private final BufferedImage collapseImg = ImageUtil.loadImageResource(TasksTrackerPlugin.class, expandBtnPath + "filter_buttons_collapsed.png");
private final Icon MENU_COLLAPSED_ICON = new ImageIcon(ImageUtil.alphaOffset(collapseImg, -180));
private final Icon MENU_ICON_HOVER = new ImageIcon(collapseImg);
private final BufferedImage expandedImg = ImageUtil.loadImageResource(TasksTrackerPlugin.class, expandBtnPath + "filter_buttons_expanded.png");
private final Icon MENU_EXPANDED_ICON = new ImageIcon(ImageUtil.alphaOffset(expandedImg, -180));
private final Icon MENU_ICON_HOVER_SELECTED = new ImageIcon(expandedImg);
public FilterButtonPanel(TasksTrackerPlugin plugin, String label)
{
this.plugin = plugin;
this.label = label;
}
protected abstract LinkedHashMap<String, BufferedImage> getIconImages();
protected abstract JPanel makeButtonPanel();
protected JToggleButton makeButton(String tooltip, BufferedImage image)
{
JToggleButton button = new JToggleButton();
button.setBackground(ColorScheme.DARKER_GRAY_COLOR);
button.setBorder(new BasicBorders.ToggleButtonBorder(ColorScheme.DARKER_GRAY_COLOR,
ColorScheme.DARKER_GRAY_COLOR.darker(),
ColorScheme.MEDIUM_GRAY_COLOR.darker(),
ColorScheme.MEDIUM_GRAY_COLOR));
button.setFocusable(false);
if (image != null) {
ImageIcon selectedIcon = new ImageIcon(image);
ImageIcon deselectedIcon = new ImageIcon(ImageUtil.alphaOffset(image, -180));
button.setIcon(deselectedIcon);
button.setSelectedIcon(selectedIcon);
button.setPreferredSize(new Dimension(image.getWidth(), image.getHeight() + 10));
} else {
button.setPreferredSize(new Dimension(button.getPreferredSize().width, 50));
}
button.setToolTipText(tooltip.substring(0,1).toUpperCase() + tooltip.substring(1).toLowerCase());
button.addActionListener(e -> {
updateFilterText();
updateCollapseButtonText();
plugin.refreshAllTasks();
});
button.setSelected(true);
return button;
}
protected JPanel allOrNoneButtons()
{
JPanel buttonWrapper = new JPanel();
buttonWrapper.setLayout(new BoxLayout(buttonWrapper, BoxLayout.X_AXIS));
buttonWrapper.setBackground(ColorScheme.DARKER_GRAY_COLOR);
buttonWrapper.setAlignmentX(JPanel.CENTER_ALIGNMENT);
JButton all = new JButton("all");
SwingUtil.removeButtonDecorations(all);
all.setFocusable(false);
all.setForeground(ColorScheme.MEDIUM_GRAY_COLOR);
all.setFont(FontManager.getRunescapeSmallFont());
all.setPreferredSize(new Dimension(50, 0));
all.addActionListener(e -> {
setAllSelected(true);
updateFilterText();
updateCollapseButtonText();
plugin.refreshAllTasks();
});
JButton none = new JButton("none");
SwingUtil.removeButtonDecorations(none);
none.setFocusable(false);
none.setForeground(ColorScheme.MEDIUM_GRAY_COLOR);
none.setFont(FontManager.getRunescapeSmallFont());
none.setPreferredSize(new Dimension(50, 0));
none.addActionListener(e -> {
setAllSelected(false);
updateFilterText();
updateCollapseButtonText();
plugin.refreshAllTasks();
});
JLabel separator = new JLabel("|");
separator.setForeground(ColorScheme.MEDIUM_GRAY_COLOR);
buttonWrapper.add(Box.createHorizontalGlue());
buttonWrapper.add(all);
buttonWrapper.add(Box.createHorizontalGlue());
buttonWrapper.add(separator);
buttonWrapper.add(Box.createHorizontalGlue());
buttonWrapper.add(none);
buttonWrapper.add(Box.createHorizontalGlue());
return buttonWrapper;
}
public JToggleButton makeCollapseButton()
{
JToggleButton collapseBtn = new JToggleButton();
// collapse button
SwingUtil.removeButtonDecorations(collapseBtn);
collapseBtn.setIcon(MENU_COLLAPSED_ICON);
collapseBtn.setSelectedIcon(MENU_EXPANDED_ICON);
collapseBtn.setRolloverIcon(MENU_ICON_HOVER);
collapseBtn.setRolloverSelectedIcon(MENU_ICON_HOVER_SELECTED);
SwingUtil.addModalTooltip(collapseBtn, "Collapse filters", "Expand filters");
collapseBtn.setBackground(ColorScheme.DARKER_GRAY_COLOR);
collapseBtn.setAlignmentX(LEFT_ALIGNMENT);
collapseBtn.setUI(new BasicButtonUI()); // substance breaks the layout
collapseBtn.addActionListener(ev -> buttonPanel.setVisible(!buttonPanel.isVisible()));
collapseBtn.setHorizontalTextPosition(JButton.CENTER);
collapseBtn.setForeground(ColorScheme.LIGHT_GRAY_COLOR);
collapseBtn.setFont(FontManager.getRunescapeSmallFont());
collapseBtn.setBorder(new EmptyBorder(2, 0, 2, 0));
collapseBtn.setFocusable(false);
collapseBtn.setSelected(true);
return collapseBtn;
}
protected void updateFilterText()
{
String filterText = buttons.entrySet().stream()
.filter(e -> e.getValue().isSelected())
.map(e -> "f-" + e.getKey() + "-f") // prefix included to cover cases where one key name is contained in another (e.g. "Master" -> "Grandmaster")
.collect(Collectors.joining(","));
plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, configKey, filterText);
}
protected boolean getConfigButtonState(String buttonKey)
{
String configValue = plugin.getConfigManager().getConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, configKey);
boolean isEmptyFilterSelection = configValue == null || configValue.isEmpty() || configValue.equals("-1");
if (isEmptyFilterSelection)
{
return true;
}
return configValue.contains("f-" + buttonKey + "-f");
}
protected void setAllSelected(boolean state)
{
buttons.values().forEach(button -> button.setSelected(state));
}
protected void updateCollapseButtonText()
{
collapseBtn.setText(label + " - " + buttons.values().stream().filter(JToggleButton::isSelected).count() + " / " + buttons.size());
}
public void redraw()
{
if(SwingUtilities.isEventDispatchThread())
{
buttons.clear();
removeAll();
collapseBtn = makeCollapseButton();
buttonPanel = makeButtonPanel();
add(collapseBtn, BorderLayout.NORTH);
add(buttonPanel, BorderLayout.CENTER);
add(allOrNoneButtons(), BorderLayout.SOUTH);
updateFilterText();
updateCollapseButtonText();
collapseBtn.setVisible(plugin.getConfig().filterPanelCollapsible());
validate();
repaint();
}
else
{
log.error("Filter button panel redraw failed - not event dispatch thread.");
}
}
}

View File

@@ -0,0 +1,8 @@
package net.reldo.taskstracker.panel.filters;
import net.reldo.taskstracker.panel.components.FixedWidthPanel;
public abstract class FilterPanel extends FixedWidthPanel
{
public abstract void redraw();
}

View File

@@ -0,0 +1,15 @@
package net.reldo.taskstracker.quests;
import java.util.HashMap;
import net.runelite.api.Client;
public class DiaryData extends HashMap<Integer, Integer>
{
public DiaryData(Client client)
{
for (DiaryVarbits diary : DiaryVarbits.values())
{
this.put(diary.id, diary.getProgress(client));
}
}
}

View File

@@ -0,0 +1,66 @@
package net.reldo.taskstracker.quests;
import net.runelite.api.Client;
public enum DiaryVarbits
{
DIARY_ARDOUGNE_EASY(4458),
DIARY_ARDOUGNE_MEDIUM(4459),
DIARY_ARDOUGNE_HARD(4460),
DIARY_ARDOUGNE_ELITE(4461),
DIARY_DESERT_EASY(4483),
DIARY_DESERT_MEDIUM(4484),
DIARY_DESERT_HARD(4485),
DIARY_DESERT_ELITE(4486),
DIARY_FALADOR_EASY(4462),
DIARY_FALADOR_MEDIUM(4463),
DIARY_FALADOR_HARD(4464),
DIARY_FALADOR_ELITE(4465),
DIARY_FREMENNIK_EASY(4491),
DIARY_FREMENNIK_MEDIUM(4492),
DIARY_FREMENNIK_HARD(4493),
DIARY_FREMENNIK_ELITE(4494),
DIARY_KANDARIN_EASY(4475),
DIARY_KANDARIN_MEDIUM(4476),
DIARY_KANDARIN_HARD(4477),
DIARY_KANDARIN_ELITE(4478),
DIARY_KARAMJA_EASY(3578),
DIARY_KARAMJA_MEDIUM(3599),
DIARY_KARAMJA_HARD(3611),
DIARY_KARAMJA_ELITE(4566),
DIARY_KOUREND_EASY(7925),
DIARY_KOUREND_MEDIUM(7926),
DIARY_KOUREND_HARD(7927),
DIARY_KOUREND_ELITE(7928),
DIARY_LUMBRIDGE_EASY(4495),
DIARY_LUMBRIDGE_MEDIUM(4496),
DIARY_LUMBRIDGE_HARD(4497),
DIARY_LUMBRIDGE_ELITE(4498),
DIARY_MORYTANIA_EASY(4487),
DIARY_MORYTANIA_MEDIUM(4488),
DIARY_MORYTANIA_HARD(4489),
DIARY_MORYTANIA_ELITE(4490),
DIARY_VARROCK_EASY(4479),
DIARY_VARROCK_MEDIUM(4480),
DIARY_VARROCK_HARD(4481),
DIARY_VARROCK_ELITE(4482),
DIARY_WESTERN_EASY(4471),
DIARY_WESTERN_MEDIUM(4472),
DIARY_WESTERN_HARD(4473),
DIARY_WESTERN_ELITE(4474),
DIARY_WILDERNESS_EASY(4466),
DIARY_WILDERNESS_MEDIUM(4467),
DIARY_WILDERNESS_HARD(4468),
DIARY_WILDERNESS_ELITE(4469);
public int id;
DiaryVarbits(int id) {
this.id = id;
}
public int getProgress(Client client)
{
return client.getVarbitValue(id);
}
}

View File

@@ -0,0 +1,17 @@
package net.reldo.taskstracker.quests;
import java.util.HashMap;
import net.runelite.api.Client;
import net.runelite.api.Quest;
import net.runelite.api.QuestState;
public class QuestData extends HashMap<Integer, QuestState>
{
public QuestData(Client client)
{
for (Quest quest : Quest.values())
{
this.put(quest.getId(), quest.getState(client));
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Some files were not shown because too many files have changed in this diff Show More