diff --git a/.gitignore b/.gitignore
index 4c7b9a18..aecbd17d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,15 @@ group-ironmen-tracker-master/bin/
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
diff --git a/tasks-tracker-plugin-master/.gitignore b/tasks-tracker-plugin-master/.gitignore
new file mode 100644
index 00000000..fa54513f
--- /dev/null
+++ b/tasks-tracker-plugin-master/.gitignore
@@ -0,0 +1,12 @@
+.gradle
+build
+.idea/
+.project
+.settings/
+.classpath
+nbactions.xml
+nb-configuration.xml
+nbproject/
+.run
+bin
+**/FileDataStoreReader.java
\ No newline at end of file
diff --git a/tasks-tracker-plugin-master/LICENSE b/tasks-tracker-plugin-master/LICENSE
new file mode 100644
index 00000000..b4d4a96c
--- /dev/null
+++ b/tasks-tracker-plugin-master/LICENSE
@@ -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.
diff --git a/tasks-tracker-plugin-master/README.md b/tasks-tracker-plugin-master/README.md
new file mode 100644
index 00000000..9e3f1570
--- /dev/null
+++ b/tasks-tracker-plugin-master/README.md
@@ -0,0 +1,18 @@
+# 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
+
+
+
diff --git a/tasks-tracker-plugin-master/build.gradle b/tasks-tracker-plugin-master/build.gradle
new file mode 100644
index 00000000..b6a006d1
--- /dev/null
+++ b/tasks-tracker-plugin-master/build.gradle
@@ -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)
+}
diff --git a/tasks-tracker-plugin-master/gradle/wrapper/gradle-wrapper.jar b/tasks-tracker-plugin-master/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..e708b1c0
Binary files /dev/null and b/tasks-tracker-plugin-master/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/tasks-tracker-plugin-master/gradle/wrapper/gradle-wrapper.properties b/tasks-tracker-plugin-master/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..12d38de6
--- /dev/null
+++ b/tasks-tracker-plugin-master/gradle/wrapper/gradle-wrapper.properties
@@ -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
diff --git a/tasks-tracker-plugin-master/gradlew b/tasks-tracker-plugin-master/gradlew
new file mode 100644
index 00000000..f5feea6d
--- /dev/null
+++ b/tasks-tracker-plugin-master/gradlew
@@ -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" "$@"
diff --git a/tasks-tracker-plugin-master/gradlew.bat b/tasks-tracker-plugin-master/gradlew.bat
new file mode 100644
index 00000000..9d21a218
--- /dev/null
+++ b/tasks-tracker-plugin-master/gradlew.bat
@@ -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
diff --git a/tasks-tracker-plugin-master/icon.png b/tasks-tracker-plugin-master/icon.png
new file mode 100644
index 00000000..7dc707fe
Binary files /dev/null and b/tasks-tracker-plugin-master/icon.png differ
diff --git a/tasks-tracker-plugin-master/runelite-plugin.properties b/tasks-tracker-plugin-master/runelite-plugin.properties
new file mode 100644
index 00000000..532e58a2
--- /dev/null
+++ b/tasks-tracker-plugin-master/runelite-plugin.properties
@@ -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
\ No newline at end of file
diff --git a/tasks-tracker-plugin-master/settings.gradle b/tasks-tracker-plugin-master/settings.gradle
new file mode 100644
index 00000000..fd8cdcfa
--- /dev/null
+++ b/tasks-tracker-plugin-master/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'tasks-tracker'
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/HtmlUtil.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/HtmlUtil.java
new file mode 100644
index 00000000..75efb406
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/HtmlUtil.java
@@ -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 = "
";
+
+ public static String wrapWithHtml(String text)
+ {
+ return "" + text + "";
+ }
+
+ public static String wrapWithWrappingParagraph(String text, int width)
+ {
+ return "
" + text + "
";
+ }
+
+ public static String wrapWithBold(String text)
+ {
+ return "" + text + "";
+ }
+
+ public static String imageTag(URL url)
+ {
+ return "
";
+ }
+
+ public static String colorTag(String color, String text)
+ {
+ return "" + text + "";
+ }
+
+ 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);
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/TasksTrackerConfig.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/TasksTrackerConfig.java
new file mode 100644
index 00000000..fbaa4a2c
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/TasksTrackerConfig.java
@@ -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;
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/TasksTrackerPlugin.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/TasksTrackerPlugin.java
new file mode 100644
index 00000000..94ca9f31
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/TasksTrackerPlugin.java
@@ -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 varpIdsToUpdate = new HashSet<>();
+ private long lastVarpUpdate = 0;
+ private NavigationButton navButton;
+ private RuneScapeProfileType currentProfileType;
+ private final Map 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 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 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 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 processTaskStatus(TaskFromStruct task)
+ {
+ CompletableFuture 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 processVarpAndUpdateTasks(@Nullable Integer varpId)
+ {
+ log.info("processVarpAndUpdateTasks: " + (varpId != null ? varpId : "all"));
+
+ List tasks = varpId != null ?
+ taskService.getTasksFromVarpId(varpId) :
+ taskService.getTasks();
+
+ List> taskFutures = new ArrayList<>();
+ for (TaskFromStruct task : tasks)
+ {
+ CompletableFuture taskFuture = processTaskStatus(task);
+ taskFutures.add(taskFuture);
+ }
+
+ CompletableFuture 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");
+ }
+ });
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/config/ConfigValues.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/config/ConfigValues.java
new file mode 100644
index 00000000..21c7ac2f
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/config/ConfigValues.java
@@ -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;
+ }
+
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/Export.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/Export.java
new file mode 100644
index 00000000..1dcc0c5a
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/Export.java
@@ -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 varbits;
+ @Expose private final HashMap varps;
+ @Expose private final HashMap tasks;
+
+ public Export(TaskType taskType, List 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 getVarbits(TaskType taskType)
+ {
+ assert client.isClientThread();
+
+ HashMap varbitValueMap = new HashMap<>();
+ for (int varbitId : taskType.getVarbits())
+ {
+ varbitValueMap.put(varbitId, client.getVarbitValue(varbitId));
+ }
+
+ return varbitValueMap;
+ }
+
+ public HashMap getVarps(TaskType taskType)
+ {
+ assert client.isClientThread();
+
+ HashMap 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 getTaskSavesById(List tasks)
+ {
+ HashMap 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;
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/LongSerializer.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/LongSerializer.java
new file mode 100644
index 00000000..9f7b27ca
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/LongSerializer.java
@@ -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
+{
+ @Override
+ public JsonElement serialize(Long value, Type type, JsonSerializationContext jsonSerializationContext)
+ {
+ return new JsonPrimitive(new DecimalFormat("#").format(value));
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/TasksSummary.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/TasksSummary.java
new file mode 100644
index 00000000..2c201ac7
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/TasksSummary.java
@@ -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 tasks)
+ {
+ tasks.forEach(task -> {
+ if (task.isTracked()) {
+ trackedTasksCount++;
+ int points = task.getPoints();
+ log.debug("TasksSummary {} {}", task.getName(), points);
+ trackedTasksPoints += points;
+ }
+ });
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/TrackerConfigStore.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/TrackerConfigStore.java
new file mode 100644
index 00000000..9a267e81
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/TrackerConfigStore.java
@@ -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 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 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();
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/FilterDataClient.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/FilterDataClient.java
new file mode 100644
index 00000000..633818af
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/FilterDataClient.java
@@ -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 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 filterConfigs = this.gson.fromJson(responseReader, listType);
+ HashMap filterConfigsByConfigKey = new HashMap<>();
+ for (FilterConfig filterConfig : filterConfigs)
+ {
+ filterConfigsByConfigKey.put(filterConfig.getConfigKey(), filterConfig);
+ }
+ return filterConfigsByConfigKey;
+ }
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/JsonDataStore.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/JsonDataStore.java
new file mode 100644
index 00000000..3c6ec83f
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/JsonDataStore.java
@@ -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";
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/ManifestClient.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/ManifestClient.java
new file mode 100644
index 00000000..3b03a11d
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/ManifestClient.java
@@ -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;
+ }
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/TaskDataClient.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/TaskDataClient.java
new file mode 100644
index 00000000..32bff48d
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/TaskDataClient.java
@@ -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 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 taskTypeDefinitions = this.gson.fromJson(responseReader, listType);
+
+ HashMap taskTypes = new HashMap<>();
+ for (TaskTypeDefinition taskTypeDefinition : taskTypeDefinitions)
+ {
+ taskTypes.put(taskTypeDefinition.getTaskJsonName(), new TaskType(client, clientThread, spriteManager, taskTypeDefinition));
+ }
+ return taskTypes;
+ }
+ }
+
+ public List 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);
+ }
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/reader/DataStoreReader.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/reader/DataStoreReader.java
new file mode 100644
index 00000000..74dd601e
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/reader/DataStoreReader.java
@@ -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;
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/reader/HttpDataStoreReader.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/reader/HttpDataStoreReader.java
new file mode 100644
index 00000000..34f43245
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/reader/HttpDataStoreReader.java
@@ -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();
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/FilterConfig.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/FilterConfig.java
new file mode 100644
index 00000000..2be17d79
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/FilterConfig.java
@@ -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 customItems;
+
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/FilterCustomItem.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/FilterCustomItem.java
new file mode 100644
index 00000000..63d766a9
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/FilterCustomItem.java
@@ -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;
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/FilterType.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/FilterType.java
new file mode 100644
index 00000000..7f7d1f00
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/FilterType.java
@@ -0,0 +1,7 @@
+package net.reldo.taskstracker.data.jsondatastore.types;
+
+public enum FilterType
+{
+ BUTTON_FILTER,
+ DROPDOWN_FILTER
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/FilterValueType.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/FilterValueType.java
new file mode 100644
index 00000000..9b8998a7
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/FilterValueType.java
@@ -0,0 +1,10 @@
+package net.reldo.taskstracker.data.jsondatastore.types;
+
+public enum FilterValueType
+{
+ PARAM_INTEGER,
+ PARAM_STRING,
+ SKILL,
+ METADATA,
+ GLOBAL
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/Manifest.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/Manifest.java
new file mode 100644
index 00000000..c5ae8c91
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/Manifest.java
@@ -0,0 +1,10 @@
+package net.reldo.taskstracker.data.jsondatastore.types;
+
+import lombok.Data;
+
+@Data
+public class Manifest
+{
+ public String taskTypeMetadata;
+ public String filterMetadata;
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/TaskDefinition.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/TaskDefinition.java
new file mode 100644
index 00000000..bb90ce89
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/TaskDefinition.java
@@ -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 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 metadata;
+
+ /**
+ * Notes from the OSRS wiki
+ */
+ private String wikiNotes;
+
+ /**
+ * Completion percent from the OSRS wiki
+ */
+ private Float completionPercent;
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/TaskDefinitionSkill.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/TaskDefinitionSkill.java
new file mode 100644
index 00000000..a96739bf
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/TaskDefinitionSkill.java
@@ -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;
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/TaskTypeDefinition.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/TaskTypeDefinition.java
new file mode 100644
index 00000000..d8c2172a
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/jsondatastore/types/TaskTypeDefinition.java
@@ -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 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 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 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 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 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 tierSpriteIdMap = new HashMap<>();
+
+ /**
+ * Varps used to store task progress
+ * Used for exports from the plugin
+ */
+ private ArrayList 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;
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/reldo/ReldoImport.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/reldo/ReldoImport.java
new file mode 100644
index 00000000..a2e2baac
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/reldo/ReldoImport.java
@@ -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 tasks;
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/reldo/ReldoTaskSave.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/reldo/ReldoTaskSave.java
new file mode 100644
index 00000000..7254f819
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/reldo/ReldoTaskSave.java
@@ -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;
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/ConfigTaskSave.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/ConfigTaskSave.java
new file mode 100644
index 00000000..ae4fcdf9
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/ConfigTaskSave.java
@@ -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();
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/TaskFromStruct.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/TaskFromStruct.java
new file mode 100644
index 00000000..06963d31
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/TaskFromStruct.java
@@ -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 _stringParams = new HashMap<>();
+ private final Map _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();
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/TaskService.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/TaskService.java
new file mode 100644
index 00000000..d86a8f02
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/TaskService.java
@@ -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 tasks = new ArrayList<>();
+ @Getter
+ private final HashMap sortedIndexes = new HashMap<>();
+ private HashMap _taskTypes = new HashMap<>();
+ private HashSet currentTaskTypeVarps = new HashSet<>();
+ private final ExecutorService futureExecutor = Executors.newSingleThreadExecutor();
+
+ public CompletableFuture 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 loadAllTasksStructData(Collection tasks) {
+ Collection> taskFutures = new ArrayList<>();
+ for (TaskFromStruct task : tasks) {
+ CompletableFuture 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 future : taskFutures) {
+ if (!future.join()) {
+ return false;
+ }
+ }
+ return true;
+ });
+ }
+
+ public CompletableFuture 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 newTasks = new ArrayList<>();
+ return newTaskType.loadTaskTypeDataAsync().thenCompose((isTaskTypeLoaded) -> {
+ if (!isTaskTypeLoaded) {
+ log.error("Error loading task type during setTaskType");
+ return CompletableFuture.completedFuture(false);
+ }
+
+ CompletableFuture future = new CompletableFuture<>();
+ futureExecutor.submit(() -> {
+ try {
+ Collection 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 comparator)
+ {
+ List 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> getTaskTypesByJsonName()
+ {
+ if (_taskTypes.size() > 0)
+ {
+ return CompletableFuture.completedFuture(_taskTypes);
+ }
+
+ try
+ {
+ CompletableFuture> 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> getStringEnumValuesAsync(String enumName)
+ {
+ Integer enumId = currentTaskType.getStringEnumMap().get(enumName);
+ if (enumId == null)
+ {
+ return CompletableFuture.completedFuture(new HashMap<>());
+ }
+
+ CompletableFuture> future = new CompletableFuture<>();
+ clientThread.invoke(() -> {
+ try
+ {
+ EnumComposition enumComposition = client.getEnum(enumId);
+ int[] keys = enumComposition.getKeys();
+ HashMap 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 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 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());
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/TaskType.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/TaskType.java
new file mode 100644
index 00000000..684a276a
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/TaskType.java
@@ -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 spritesById = new HashMap<>();
+ @Getter
+ private final HashMap tierSprites = new HashMap<>();
+ @Getter
+ private final HashMap 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 loadTaskTypeDataAsync()
+ {
+ CompletableFuture 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 getButtonFiltersSpriteIds()
+ {
+ HashSet 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 getTaskVarps()
+ {
+ return _taskTypeDefinition.getTaskVarps();
+ }
+
+ public String getTaskJsonName()
+ {
+ return _taskTypeDefinition.getTaskJsonName();
+ }
+
+ public HashMap getIntParamMap()
+ {
+ return _taskTypeDefinition.getIntParamMap();
+ }
+
+ public HashMap getStringParamMap()
+ {
+ return _taskTypeDefinition.getStringParamMap();
+ }
+
+ public HashMap getStringEnumMap()
+ {
+ return _taskTypeDefinition.getStringEnumMap();
+ }
+
+ public String getName()
+ {
+ return _taskTypeDefinition.getName();
+ }
+
+ public ArrayList getFilters()
+ {
+ return _taskTypeDefinition.getFilters();
+ }
+
+ public int[] getOtherVarps()
+ {
+ return _taskTypeDefinition.getOtherVarps();
+ }
+
+ public int[] getVarbits()
+ {
+ return _taskTypeDefinition.getVarbits();
+ }
+
+ public int getTaskCompletedScriptId()
+ {
+ return _taskTypeDefinition.getTaskCompletedScriptId();
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/filters/Filter.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/filters/Filter.java
new file mode 100644
index 00000000..db4fdbeb
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/filters/Filter.java
@@ -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);
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/filters/FilterService.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/filters/FilterService.java
new file mode 100644
index 00000000..eb6190a1
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/filters/FilterService.java
@@ -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 _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();
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/filters/ParamButtonFilter.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/filters/ParamButtonFilter.java
new file mode 100644
index 00000000..d91dd017
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/filters/ParamButtonFilter.java
@@ -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");
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/filters/ParamDropdownFilter.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/filters/ParamDropdownFilter.java
new file mode 100644
index 00000000..77256ab4
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/data/task/filters/ParamDropdownFilter.java
@@ -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;
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/Colors.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/Colors.java
new file mode 100644
index 00000000..8b51eb34
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/Colors.java
@@ -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);
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/Icons.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/Icons.java
new file mode 100644
index 00000000..c8e90a05
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/Icons.java
@@ -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"));
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/LoggedInPanel.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/LoggedInPanel.java
new file mode 100644
index 00000000..1d6abd20
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/LoggedInPanel.java
@@ -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> 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 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 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> filterStore = new HashMap<>();
+
+ private void saveFilters()
+ {
+ ConfigValues.TaskListTabs tab = config.taskListTab();
+
+ Map 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 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 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> taskTypeItems = new ArrayList<>();
+ taskTypes.forEach((taskTypeJsonName, taskType) -> {
+ ComboItem item = new ComboItem<>(taskType, taskType.getName());
+ taskTypeItems.add(item);
+ taskTypeDropdown.addItem(item);
+ });
+
+ ComboItem 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();
+ });
+ }
+ });
+ });
+ });
+ }
+}
\ No newline at end of file
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/LoggedOutPanel.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/LoggedOutPanel.java
new file mode 100644
index 00000000..16331bc4
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/LoggedOutPanel.java
@@ -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."));
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/SortPanel.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/SortPanel.java
new file mode 100644
index 00000000..dec703eb
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/SortPanel.java
@@ -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 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 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);
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/SubFilterPanel.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/SubFilterPanel.java
new file mode 100644
index 00000000..16b69261
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/SubFilterPanel.java
@@ -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 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 getFilterPanels(ArrayList filterConfigs)
+ {
+ List 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 enumEntries = taskService.getStringEnumValuesAsync(enumName).get(); // TODO: blocking call
+ ArrayList> options = new ArrayList<>();
+ options.add(new ComboItem<>(-1, ""));
+ for (Map.Entry 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];
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/TaskListPanel.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/TaskListPanel.java
new file mode 100644
index 00000000..0fffbd9d
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/TaskListPanel.java
@@ -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 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 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 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("" + getEmptyTaskListMessage() + "");
+ 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 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.");
+ }
+ }
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/TaskPanel.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/TaskPanel.java
new file mode 100644
index 00000000..b17fda6c
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/TaskPanel.java
@@ -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 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 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";
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/TaskPanelFactory.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/TaskPanelFactory.java
new file mode 100644
index 00000000..df1b6fda
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/TaskPanelFactory.java
@@ -0,0 +1,8 @@
+package net.reldo.taskstracker.panel;
+
+import net.reldo.taskstracker.data.task.TaskFromStruct;
+
+public interface TaskPanelFactory
+{
+ TaskPanel create(TaskFromStruct task);
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/TasksTrackerPluginPanel.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/TasksTrackerPluginPanel.java
new file mode 100644
index 00000000..9fd6a485
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/TasksTrackerPluginPanel.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/components/CheckBox.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/components/CheckBox.java
new file mode 100644
index 00000000..2905ce84
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/components/CheckBox.java
@@ -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);
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/components/FixedWidthPanel.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/components/FixedWidthPanel.java
new file mode 100644
index 00000000..d2803d8b
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/components/FixedWidthPanel.java
@@ -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);
+ }
+
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/components/MultiToggleButton.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/components/MultiToggleButton.java
new file mode 100644
index 00000000..6c4fc47f
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/components/MultiToggleButton.java
@@ -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);
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/components/SearchBox.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/components/SearchBox.java
new file mode 100644
index 00000000..68529959
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/components/SearchBox.java
@@ -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();
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/components/TriToggleButton.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/components/TriToggleButton.java
new file mode 100644
index 00000000..603d57e2
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/components/TriToggleButton.java
@@ -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;
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/filters/ComboItem.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/filters/ComboItem.java
new file mode 100644
index 00000000..6cdac2a1
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/filters/ComboItem.java
@@ -0,0 +1,29 @@
+package net.reldo.taskstracker.panel.filters;
+
+public class ComboItem
+{
+ 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;
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/filters/DynamicButtonFilterPanel.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/filters/DynamicButtonFilterPanel.java
new file mode 100644
index 00000000..1e1881bc
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/filters/DynamicButtonFilterPanel.java
@@ -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 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 buttonImages = getIconImages();
+ LinkedHashMap 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 getIconImages()
+ {
+ LinkedHashMap 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 getTooltips()
+ {
+ LinkedHashMap tooltips = new LinkedHashMap<>();
+ for (FilterCustomItem customItem : filterConfig.getCustomItems())
+ {
+ String key = customItem.getValue().toString();
+ tooltips.put(key, customItem.getTooltip());
+ }
+ return tooltips;
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/filters/DynamicDropdownFilterPanel.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/filters/DynamicDropdownFilterPanel.java
new file mode 100644
index 00000000..6277b505
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/filters/DynamicDropdownFilterPanel.java
@@ -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 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 makeDropdownPanel()
+ {
+ JComboBox 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.");
+ }
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/filters/FilterButtonPanel.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/filters/FilterButtonPanel.java
new file mode 100644
index 00000000..8395bf99
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/filters/FilterButtonPanel.java
@@ -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 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 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.");
+ }
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/filters/FilterPanel.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/filters/FilterPanel.java
new file mode 100644
index 00000000..ce76e853
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/panel/filters/FilterPanel.java
@@ -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();
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/quests/DiaryData.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/quests/DiaryData.java
new file mode 100644
index 00000000..e867414b
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/quests/DiaryData.java
@@ -0,0 +1,15 @@
+package net.reldo.taskstracker.quests;
+
+import java.util.HashMap;
+import net.runelite.api.Client;
+
+public class DiaryData extends HashMap
+{
+ public DiaryData(Client client)
+ {
+ for (DiaryVarbits diary : DiaryVarbits.values())
+ {
+ this.put(diary.id, diary.getProgress(client));
+ }
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/quests/DiaryVarbits.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/quests/DiaryVarbits.java
new file mode 100644
index 00000000..732f9c96
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/quests/DiaryVarbits.java
@@ -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);
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/quests/QuestData.java b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/quests/QuestData.java
new file mode 100644
index 00000000..10f2aa19
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/main/java/net/reldo/taskstracker/quests/QuestData.java
@@ -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
+{
+ public QuestData(Client client)
+ {
+ for (Quest quest : Quest.values())
+ {
+ this.put(quest.getId(), quest.getState(client));
+ }
+ }
+}
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/eye-cross-grey.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/eye-cross-grey.png
new file mode 100644
index 00000000..7e42040b
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/eye-cross-grey.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/eye-red.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/eye-red.png
new file mode 100644
index 00000000..b4cea10e
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/eye-red.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/eye.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/eye.png
new file mode 100644
index 00000000..55c00226
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/eye.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/minus.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/minus.png
new file mode 100644
index 00000000..b44010a2
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/minus.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/collapsed.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/collapsed.png
new file mode 100644
index 00000000..a637936b
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/collapsed.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/complete_button/complete_and_incomplete_icon.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/complete_button/complete_and_incomplete_icon.png
new file mode 100644
index 00000000..60c67c8c
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/complete_button/complete_and_incomplete_icon.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/complete_button/complete_only_icon.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/complete_button/complete_only_icon.png
new file mode 100644
index 00000000..ae0d44b0
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/complete_button/complete_only_icon.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/complete_button/incomplete_only_icon.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/complete_button/incomplete_only_icon.png
new file mode 100644
index 00000000..9a0ba091
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/complete_button/incomplete_only_icon.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/expanded.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/expanded.png
new file mode 100644
index 00000000..35e4f4ed
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/expanded.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/filter_buttons_collapsed.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/filter_buttons_collapsed.png
new file mode 100644
index 00000000..d23684c4
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/filter_buttons_collapsed.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/filter_buttons_expanded.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/filter_buttons_expanded.png
new file mode 100644
index 00000000..2ac69454
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/filter_buttons_expanded.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/filter_menu_collapsed.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/filter_menu_collapsed.png
new file mode 100644
index 00000000..8440d0aa
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/filter_menu_collapsed.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/filter_menu_expanded.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/filter_menu_expanded.png
new file mode 100644
index 00000000..0981525c
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/filter_menu_expanded.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/ignored_button/invisible_icon.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/ignored_button/invisible_icon.png
new file mode 100644
index 00000000..c43883bf
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/ignored_button/invisible_icon.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/ignored_button/semivisible_icon.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/ignored_button/semivisible_icon.png
new file mode 100644
index 00000000..a52cbfe0
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/ignored_button/semivisible_icon.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/ignored_button/visible_icon.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/ignored_button/visible_icon.png
new file mode 100644
index 00000000..5c4232c8
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/ignored_button/visible_icon.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/no_skill.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/no_skill.png
new file mode 100644
index 00000000..12b88401
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/no_skill.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/search.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/search.png
new file mode 100644
index 00000000..3e723753
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/search.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/sort_button/ascending_icon.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/sort_button/ascending_icon.png
new file mode 100644
index 00000000..e097ecbe
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/sort_button/ascending_icon.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/sort_button/descending_icon.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/sort_button/descending_icon.png
new file mode 100644
index 00000000..f78e4893
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/sort_button/descending_icon.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/asgarnia.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/asgarnia.png
new file mode 100644
index 00000000..28ccfc09
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/asgarnia.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/desert.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/desert.png
new file mode 100644
index 00000000..6d061e70
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/desert.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/fremennik.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/fremennik.png
new file mode 100644
index 00000000..38992722
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/fremennik.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/global.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/global.png
new file mode 100644
index 00000000..12130775
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/global.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/kandarin.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/kandarin.png
new file mode 100644
index 00000000..8a151405
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/kandarin.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/karamja.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/karamja.png
new file mode 100644
index 00000000..e4093667
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/karamja.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/kourend.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/kourend.png
new file mode 100644
index 00000000..f526569c
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/kourend.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/misthalin.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/misthalin.png
new file mode 100644
index 00000000..2a3ae7b8
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/misthalin.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/morytania.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/morytania.png
new file mode 100644
index 00000000..85dc110d
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/morytania.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/tirannwn.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/tirannwn.png
new file mode 100644
index 00000000..4b7e9919
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/tirannwn.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/wilderness.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/wilderness.png
new file mode 100644
index 00000000..aee8fec3
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_areas/league4/wilderness.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/achievement.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/achievement.png
new file mode 100644
index 00000000..9c3f0171
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/achievement.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/combat.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/combat.png
new file mode 100644
index 00000000..a4dcf5c2
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/combat.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/minigame.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/minigame.png
new file mode 100644
index 00000000..86b2b94a
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/minigame.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/other.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/other.png
new file mode 100644
index 00000000..76f8c331
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/other.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/quest.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/quest.png
new file mode 100644
index 00000000..f8904e06
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/quest.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/skill.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/skill.png
new file mode 100644
index 00000000..5e30ce50
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_categories/league4/skill.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/easy.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/easy.png
new file mode 100644
index 00000000..58073170
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/easy.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/elite.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/elite.png
new file mode 100644
index 00000000..7506bc9a
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/elite.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/grandmaster.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/grandmaster.png
new file mode 100644
index 00000000..97abbc30
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/grandmaster.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/hard.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/hard.png
new file mode 100644
index 00000000..9b2645a2
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/hard.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/master.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/master.png
new file mode 100644
index 00000000..c7f4eaed
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/master.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/medium.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/medium.png
new file mode 100644
index 00000000..b868bcc4
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/combat/medium.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/league3/easy.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/league3/easy.png
new file mode 100644
index 00000000..40ede02a
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/league3/easy.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/league3/elite.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/league3/elite.png
new file mode 100644
index 00000000..15021616
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/league3/elite.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/league3/hard.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/league3/hard.png
new file mode 100644
index 00000000..adf50ef8
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/league3/hard.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/league3/master.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/league3/master.png
new file mode 100644
index 00000000..2219b9fc
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/league3/master.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/league3/medium.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/league3/medium.png
new file mode 100644
index 00000000..b157c3c3
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/task_tiers/league3/medium.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/tracked_button/tracked_and_untracked_icon.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/tracked_button/tracked_and_untracked_icon.png
new file mode 100644
index 00000000..8dad9aad
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/tracked_button/tracked_and_untracked_icon.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/tracked_button/tracked_icon.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/tracked_button/tracked_icon.png
new file mode 100644
index 00000000..4915f067
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/tracked_button/tracked_icon.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/tracked_button/untracked_icon.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/tracked_button/untracked_icon.png
new file mode 100644
index 00000000..bc705cc0
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel/components/tracked_button/untracked_icon.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel_icon.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel_icon.png
new file mode 100644
index 00000000..7dc707fe
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/panel_icon.png differ
diff --git a/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/plus.png b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/plus.png
new file mode 100644
index 00000000..50c22bb0
Binary files /dev/null and b/tasks-tracker-plugin-master/src/main/resources/net/reldo/taskstracker/plus.png differ
diff --git a/tasks-tracker-plugin-master/src/test/java/net/reldo/taskstracker/TasksTrackerPluginTest.java b/tasks-tracker-plugin-master/src/test/java/net/reldo/taskstracker/TasksTrackerPluginTest.java
new file mode 100644
index 00000000..abca993c
--- /dev/null
+++ b/tasks-tracker-plugin-master/src/test/java/net/reldo/taskstracker/TasksTrackerPluginTest.java
@@ -0,0 +1,13 @@
+package net.reldo.taskstracker;
+
+import net.runelite.client.RuneLite;
+import net.runelite.client.externalplugins.ExternalPluginManager;
+
+public class TasksTrackerPluginTest
+{
+ public static void main(String[] args) throws Exception
+ {
+ ExternalPluginManager.loadBuiltin(TasksTrackerPlugin.class);
+ RuneLite.main(args);
+ }
+}
\ No newline at end of file