diff --git a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/exception/MethodNotAllowedException.java b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/exception/MethodNotAllowedException.java
new file mode 100644
index 0000000000..248d945e8d
--- /dev/null
+++ b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/exception/MethodNotAllowedException.java
@@ -0,0 +1,46 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.web.resolver.exception;
+
+import com.djrapitops.plan.delivery.web.resolver.request.Request;
+
+/**
+ * Throw this exception when a Resolver gets invalid {@link Request#getMethod()}.
+ *
+ * Plan will construct error json automatically.
+ * Note that you might need to handle the error page, which is json: {@code {"status": 405, "error": "message"}}
+ *
+ * @author AuroraLS3
+ */
+public class MethodNotAllowedException extends IllegalStateException {
+
+ private final String[] allowedMethods;
+
+ /**
+ * Default constructor.
+ *
+ * @param allowedMethods POST, GET, etc. - avoid including any input incoming in the request to prevent XSS.
+ */
+ public MethodNotAllowedException(String... allowedMethods) {
+ super("Method not allowed");
+ this.allowedMethods = allowedMethods;
+ }
+
+ public String[] getAllowedMethods() {
+ return allowedMethods;
+ }
+}
diff --git a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/request/Request.java b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/request/Request.java
index 86e62eaa54..c8e1c08d53 100644
--- a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/request/Request.java
+++ b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/request/Request.java
@@ -35,6 +35,7 @@ public final class Request {
private final WebUser user;
private final Map headers;
private final byte[] requestBody;
+ private final String accessIpAddress;
/**
* Constructor.
@@ -45,25 +46,58 @@ public final class Request {
* @param user Web user doing the request (if authenticated)
* @param headers Request headers Documentation
* @param requestBody Raw body as bytes, if present
+ * @deprecated Use newer constructor with IP address.
*/
+ @Deprecated
public Request(String method, URIPath path, URIQuery query, WebUser user, Map headers, byte[] requestBody) {
+ this(method, path, query, user, headers, requestBody, null);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param method HTTP method, GET, PUT, POST, etc
+ * @param path Requested path /example/target
+ * @param query Request parameters ?param=value etc
+ * @param user Web user doing the request (if authenticated)
+ * @param headers Request headers Documentation
+ * @param requestBody Raw body as bytes, if present
+ * @param accessIpAddress IP address this request is coming from.
+ */
+ public Request(String method, URIPath path, URIQuery query, WebUser user, Map headers, byte[] requestBody, String accessIpAddress) {
this.method = method;
this.path = path;
this.query = query;
this.user = user;
this.headers = headers;
this.requestBody = requestBody;
+ this.accessIpAddress = accessIpAddress;
}
/**
* Special constructor that figures out URIPath and URIQuery from "/path/and?query=params" and has no request body.
*
- * @param method HTTP requst method
+ * @param method HTTP request method
* @param target The requested path and query, e.g. "/path/and?query=params"
* @param user User that made the request
* @param headers HTTP request headers
+ * @deprecated Use newer constructor with IP address.
*/
+ @Deprecated
public Request(String method, String target, WebUser user, Map headers) {
+ this(method, target, user, headers, null);
+ }
+
+ /**
+ * Special constructor that figures out URIPath and URIQuery from "/path/and?query=params" and has no request body.
+ *
+ * @param method HTTP request method
+ * @param target The requested path and query, e.g. "/path/and?query=params"
+ * @param user User that made the request
+ * @param headers HTTP request headers
+ * @param accessIpAddress IP address this request is coming from.
+ */
+ public Request(String method, String target, WebUser user, Map headers, String accessIpAddress) {
this.method = method;
if (target.contains("?")) {
String[] halves = StringUtils.split(target, "?", 2);
@@ -76,6 +110,7 @@ public Request(String method, String target, WebUser user, Map h
this.user = user;
this.headers = headers;
this.requestBody = new byte[0];
+ this.accessIpAddress = accessIpAddress;
}
/**
@@ -134,7 +169,11 @@ public Optional getHeader(String key) {
}
public Request omitFirstInPath() {
- return new Request(method, path.omitFirst(), query, user, headers, requestBody);
+ return new Request(method, path.omitFirst(), query, user, headers, requestBody, accessIpAddress);
+ }
+
+ public String getAccessIpAddress() {
+ return accessIpAddress;
}
@Override
diff --git a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/request/URIPath.java b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/request/URIPath.java
index 2f251cfcb4..7839056ebb 100644
--- a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/request/URIPath.java
+++ b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/request/URIPath.java
@@ -20,6 +20,7 @@
public final class URIPath {
+ // Example: /target/path/url
private final String path;
public URIPath(String path) {
@@ -99,6 +100,8 @@ public boolean endsWith(String suffix) {
return path.endsWith(suffix);
}
+ public boolean startsWith(String prefix) {return path.startsWith(prefix);}
+
/**
* Immutable modification, removes first part of the path string.
*
diff --git a/Plan/common/build.gradle b/Plan/common/build.gradle
index 637bc6369f..20df29d401 100644
--- a/Plan/common/build.gradle
+++ b/Plan/common/build.gradle
@@ -190,6 +190,7 @@ tasks.register("determineAssetModifications") {
inputs.files(fileTree("$rootDir/react/dashboard/build"))
inputs.files(fileTree(dir: "src/main/resources/assets/plan/web"))
inputs.files(fileTree(dir: "src/main/resources/assets/plan/locale"))
+ inputs.files(fileTree(dir: "src/main/resources/assets/plan/themes"))
outputs.file("build/resources/main/assets/plan/AssetVersion.yml")
doLast {
@@ -228,6 +229,22 @@ tasks.register("determineAssetModifications") {
)
}
+ tree = fileTree(dir: "src/main/resources/assets/plan/themes")
+ tree.forEach { File f ->
+ def gitModified = new ByteArrayOutputStream()
+ exec {
+ commandLine "git", "log", "-1", "--pretty=%ct", f.toString()
+ standardOutput = gitModified
+ }
+ def gitModifiedAsString = gitModified.toString().strip()
+ // git returns UNIX time in seconds, but most things in Java use UNIX time in milliseconds
+ def modified = gitModifiedAsString.isEmpty() ? System.currentTimeMillis() : Long.parseLong(gitModifiedAsString) * 1000
+ def relativePath = tree.getDir().toPath().relativize(f.toPath()) // File path relative to the tree
+ versionFile.text += String.format(
+ "themes/%s: %s\n", relativePath.toString().replace(".", ",").replace("\\", "/"), modified
+ )
+ }
+
tree = fileTree("$rootDir/react/dashboard/build")
tree.forEach { File f ->
if (f.getName().endsWith(".map")) return
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermission.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermission.java
index a67e1a7533..e5a8825bca 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermission.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermission.java
@@ -126,9 +126,11 @@ public enum WebPermission implements Supplier, Lang {
ACCESS_QUERY("Allows accessing /query and Query results pages"),
ACCESS_ERRORS("Allows accessing /errors page"),
ACCESS_DOCS("Allows accessing /docs page"),
+ ACCESS_THEME_EDITOR("Allows accessing /theme-editor page"),
MANAGE_GROUPS("Allows modifying group permissions & Access to /manage/groups page"),
- MANAGE_USERS("Allows modifying what users belong to what group");
+ MANAGE_USERS("Allows modifying what users belong to what group"),
+ MANAGE_THEMES("Allows saving or deleting themes via theme-editor for everyone");
private final String description;
private final boolean deprecated;
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/ThemeDto.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/ThemeDto.java
new file mode 100644
index 0000000000..bf9ebd5923
--- /dev/null
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/ThemeDto.java
@@ -0,0 +1,94 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.domain.datatransfer;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @author AuroraLS3
+ */
+public class ThemeDto {
+
+ private String name;
+ private Map colors;
+ private Map nightColors;
+ private Map useCases;
+ private Map nightModeUseCases;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Map getColors() {
+ return colors;
+ }
+
+ public void setColors(Map colors) {
+ this.colors = colors;
+ }
+
+ public Map getNightColors() {
+ return nightColors;
+ }
+
+ public void setNightColors(Map nightColors) {
+ this.nightColors = nightColors;
+ }
+
+ public Map getUseCases() {
+ return useCases;
+ }
+
+ public void setUseCases(Map useCases) {
+ this.useCases = useCases;
+ }
+
+ public Map getNightModeUseCases() {
+ return nightModeUseCases;
+ }
+
+ public void setNightModeUseCases(Map nightModeUseCases) {
+ this.nightModeUseCases = nightModeUseCases;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) return false;
+ ThemeDto themeDto = (ThemeDto) o;
+ return Objects.equals(getColors(), themeDto.getColors()) && Objects.equals(getNightColors(), themeDto.getNightColors()) && Objects.equals(getUseCases(), themeDto.getUseCases()) && Objects.equals(getNightModeUseCases(), themeDto.getNightModeUseCases());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getColors(), getNightColors(), getUseCases(), getNightModeUseCases());
+ }
+
+ @Override
+ public String toString() {
+ return "ThemeDto{" +
+ "colors=" + colors +
+ ", nightColors=" + nightColors +
+ ", useCases=" + useCases +
+ ", nightModeUseCases=" + nightModeUseCases +
+ '}';
+ }
+}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/extension/ExtensionTabDataDto.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/extension/ExtensionTabDataDto.java
index 8a1129d39e..b610925047 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/extension/ExtensionTabDataDto.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/extension/ExtensionTabDataDto.java
@@ -47,7 +47,7 @@ public static Optional mapToValue(ExtensionTabData tabDat
if (doubleValue.isPresent()) return doubleValue;
Optional percentage = tabData.getPercentage(key).map(data -> new ExtensionValueDataDto(data.getDescription(), "PERCENTAGE", data.getFormattedValue(formatters.percentage())));
if (percentage.isPresent()) return percentage;
- Optional number = tabData.getNumber(key).map(data -> new ExtensionValueDataDto(data.getDescription(), data.getFormatType() == FormatType.NONE ? "NUMBER" : data.getFormatType().name(), data.getFormattedValue(formatters.getNumberFormatter(data.getFormatType()))));
+ Optional number = tabData.getNumber(key).map(data -> new ExtensionValueDataDto(data.getDescription(), data.getFormatType() == FormatType.NONE ? "NUMBER" : data.getFormatType().name(), data.getRawValue()));
if (number.isPresent()) return number;
Optional string = tabData.getString(key).map(data -> new ExtensionValueDataDto(data.getDescription(), data.isPlayerName() ? "LINK" : "STRING", data.getFormattedValue()));
if (string.isPresent()) return string;
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/extension/TableCellDto.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/extension/TableCellDto.java
index 440e6e1490..8d9a1a9321 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/extension/TableCellDto.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/extension/TableCellDto.java
@@ -16,7 +16,7 @@
*/
package com.djrapitops.plan.delivery.domain.datatransfer.extension;
-import org.jetbrains.annotations.Nullable;
+import com.djrapitops.plan.extension.table.TableColumnFormat;
import java.util.Objects;
@@ -25,47 +25,39 @@
*/
public class TableCellDto {
- private final String value;
- @Nullable
- private final Object valueUnformatted;
+ private final Object value;
+ private final TableColumnFormat format;
- public TableCellDto(String value) {
+ public TableCellDto(Object value, TableColumnFormat format) {
this.value = value;
- this.valueUnformatted = null;
+ this.format = format;
}
- public TableCellDto(String value, @Nullable Object valueUnformatted) {
- this.value = value;
- this.valueUnformatted = valueUnformatted;
- }
-
- public String getValue() {
+ public Object getValue() {
return value;
}
- @Nullable
- public Object getValueUnformatted() {
- return valueUnformatted;
+ public TableColumnFormat getFormat() {
+ return format;
}
@Override
public boolean equals(Object o) {
- if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TableCellDto that = (TableCellDto) o;
- return Objects.equals(getValue(), that.getValue()) && Objects.equals(getValueUnformatted(), that.getValueUnformatted());
+ return Objects.equals(getValue(), that.getValue()) && getFormat() == that.getFormat();
}
@Override
public int hashCode() {
- return Objects.hash(getValue(), getValueUnformatted());
+ return Objects.hash(getValue(), getFormat());
}
@Override
public String toString() {
return "TableCellDto{" +
- "value='" + value + '\'' +
- ", valueUnformatted=" + valueUnformatted +
+ "value=" + value +
+ ", format=" + format +
'}';
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/extension/TableDto.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/extension/TableDto.java
index 2f333fc61f..6f17c38c32 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/extension/TableDto.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/extension/TableDto.java
@@ -16,11 +16,8 @@
*/
package com.djrapitops.plan.delivery.domain.datatransfer.extension;
-import com.djrapitops.plan.delivery.formatting.Formatters;
-import com.djrapitops.plan.delivery.rendering.html.Html;
import com.djrapitops.plan.extension.table.Table;
import com.djrapitops.plan.extension.table.TableColumnFormat;
-import org.apache.commons.text.StringEscapeUtils;
import java.util.ArrayList;
import java.util.Arrays;
@@ -59,7 +56,7 @@ public static List mapToRows(List rows, TableColumnFor
mapped.add(null);
} else {
TableColumnFormat format = tableColumnFormats[i];
- mapped.add(new TableCellDto(applyFormat(format, value), value));
+ mapped.add(new TableCellDto(value, format));
}
}
return mapped.toArray(new TableCellDto[0]);
@@ -67,25 +64,6 @@ public static List mapToRows(List rows, TableColumnFor
.collect(Collectors.toList());
}
- public static String applyFormat(TableColumnFormat format, Object value) {
- try {
- switch (format) {
- case TIME_MILLISECONDS:
- return Formatters.getInstance().timeAmount().apply(Long.parseLong(value.toString()));
- case DATE_YEAR:
- return Formatters.getInstance().yearLong().apply(Long.parseLong(value.toString()));
- case DATE_SECOND:
- return Formatters.getInstance().secondLong().apply(Long.parseLong(value.toString()));
- case PLAYER_NAME:
- return Html.LINK.create("../player/" + Html.encodeToURL(value.toString()), StringEscapeUtils.escapeHtml4(value.toString()));
- default:
- return value.toString();
- }
- } catch (Exception e) {
- return Objects.toString(value);
- }
- }
-
private List constructRow(List columns, TableCellDto[] row) {
List constructedRow = new ArrayList<>();
@@ -93,10 +71,10 @@ private List constructRow(List columns, TableCellDto[] row
int columnCount = columns.size();
for (int i = 0; i < columnCount; i++) {
if (i > headerLength) {
- constructedRow.add(new TableCellDto("-"));
+ constructedRow.add(new TableCellDto("-", null));
} else {
TableCellDto cell = row[i];
- constructedRow.add(cell != null ? cell : new TableCellDto("-"));
+ constructedRow.add(cell != null ? cell : new TableCellDto("-", null));
}
}
return constructedRow;
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/mutators/ActivityIndex.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/mutators/ActivityIndex.java
index 69d7c5c781..aaa8813110 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/mutators/ActivityIndex.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/mutators/ActivityIndex.java
@@ -120,6 +120,16 @@ public static String[] getGroups(Locale locale) {
};
}
+ public static String[] getGroupLocaleKeys() {
+ return new String[]{
+ HtmlLang.INDEX_VERY_ACTIVE.getKey(),
+ HtmlLang.INDEX_ACTIVE.getKey(),
+ HtmlLang.INDEX_REGULAR.getKey(),
+ HtmlLang.INDEX_IRREGULAR.getKey(),
+ HtmlLang.INDEX_INACTIVE.getKey()
+ };
+ }
+
private double calculate(DataContainer container) {
return calculate(SessionsMutator.forContainer(container));
}
@@ -208,4 +218,18 @@ public String getGroup(Locale locale) {
return locale.getString(HtmlLang.INDEX_INACTIVE);
}
}
+
+ public String getGroupLocaleKey() {
+ if (value >= VERY_ACTIVE) {
+ return HtmlLang.INDEX_VERY_ACTIVE.getKey();
+ } else if (value >= ACTIVE) {
+ return HtmlLang.INDEX_ACTIVE.getKey();
+ } else if (value >= REGULAR) {
+ return HtmlLang.INDEX_REGULAR.getKey();
+ } else if (value >= IRREGULAR) {
+ return HtmlLang.INDEX_IRREGULAR.getKey();
+ } else {
+ return HtmlLang.INDEX_INACTIVE.getKey();
+ }
+ }
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/mutators/PlayerKillMutator.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/mutators/PlayerKillMutator.java
index 793c5bcef9..7fb6ed72a3 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/mutators/PlayerKillMutator.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/mutators/PlayerKillMutator.java
@@ -50,7 +50,7 @@ public List> toJSONAsMap(Formatters formatters) {
ServerIdentifier server = kill.getServer();
Map killMap = new HashMap<>();
- killMap.put("date", formatters.secondLong().apply(kill.getDate()));
+ killMap.put("date", kill.getDate());
killMap.put("killer", killer.getName());
killMap.put("victim", victim.getName());
killMap.put("killerUUID", killer.getUuid().toString());
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/mutators/SessionsMutator.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/mutators/SessionsMutator.java
index eb56755d08..8ea0620d71 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/mutators/SessionsMutator.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/mutators/SessionsMutator.java
@@ -280,12 +280,12 @@ private List> toJSONMaps(
sessionMap.put("server_url_name", Html.encodeToURL(serverName));
sessionMap.put("server_uuid", serverUUID);
sessionMap.put("name", nameFunction.apply(sessionMap));
- sessionMap.put("start", formatters.yearLong().apply(session.getStart()) +
- (session.getExtraData(ActiveSession.class).isPresent() ? " (Online)" : ""));
- sessionMap.put("end", formatters.yearLong().apply(session.getEnd()));
+ sessionMap.put("online", session.getExtraData(ActiveSession.class).isPresent());
+ sessionMap.put("start", session.getStart());
+ sessionMap.put("end", session.getEnd());
sessionMap.put("most_used_world", worldAliasSettings.getLongestWorldPlayed(session));
- sessionMap.put("length", formatters.timeAmount().apply(session.getLength()));
- sessionMap.put("afk_time", formatters.timeAmount().apply(session.getAfkTime()));
+ sessionMap.put("length", session.getLength());
+ sessionMap.put("afk_time", session.getAfkTime());
sessionMap.put("mob_kills", session.getMobKillCount());
sessionMap.put("deaths", session.getDeathCount());
sessionMap.put("player_kills", session.getExtraData(PlayerKills.class)
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/NetworkPageExporter.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/NetworkPageExporter.java
index 7625c76d74..6dd07f2342 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/NetworkPageExporter.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/NetworkPageExporter.java
@@ -164,7 +164,7 @@ private String toJSONResourceName(String resource) {
private Optional getJSONResponse(String resource) {
try {
- return jsonHandler.getResolver().resolve(new Request("GET", "/v1/" + resource, null, Collections.emptyMap()));
+ return jsonHandler.getResolver().resolve(new Request("GET", "/v1/" + resource, null, Collections.emptyMap(), null));
} catch (WebUserAuthException e) {
// The rest of the exceptions should not be thrown
throw new IllegalStateException("Unexpected exception thrown: " + e, e);
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/PlayerPageExporter.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/PlayerPageExporter.java
index 2db3a04120..70a62d34ed 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/PlayerPageExporter.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/PlayerPageExporter.java
@@ -122,7 +122,7 @@ private String toJSONResourceName(String resource) {
private Optional getJSONResponse(String resource) {
try {
- return jsonHandler.getResolver().resolve(new Request("GET", "/v1/" + resource, null, Collections.emptyMap()));
+ return jsonHandler.getResolver().resolve(new Request("GET", "/v1/" + resource, null, Collections.emptyMap(), null));
} catch (WebUserAuthException e) {
// The rest of the exceptions should not be thrown
throw new IllegalStateException("Unexpected exception thrown: " + e, e);
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/PlayersPageExporter.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/PlayersPageExporter.java
index 6edf89c77c..3f577fd03b 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/PlayersPageExporter.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/PlayersPageExporter.java
@@ -102,7 +102,7 @@ private String toJSONResourceName() {
private Optional getJSONResponse() {
try {
- return jsonHandler.getResolver().resolve(new Request("GET", "/v1/" + PLAYERS_TABLE, null, Collections.emptyMap()));
+ return jsonHandler.getResolver().resolve(new Request("GET", "/v1/" + PLAYERS_TABLE, null, Collections.emptyMap(), null));
} catch (WebUserAuthException e) {
// The rest of the exceptions should not be thrown
throw new IllegalStateException("Unexpected exception thrown: " + e.toString(), e);
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/ReactExporter.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/ReactExporter.java
index 0ca33a5fe8..c9ba0f7457 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/ReactExporter.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/ReactExporter.java
@@ -81,6 +81,19 @@ public void exportReactFiles(Path toDirectory) throws IOException {
exportStaticBundle(toDirectory);
exportLocaleJson(toDirectory.resolve("locale"));
exportMetadataJson(toDirectory.resolve("metadata"));
+ exportThemeJson(toDirectory.resolve("theme"));
+ exportReactRedirects(toDirectory, files, config, new String[]{
+ "theme-editor",
+ "theme-editor/new",
+ "theme-editor/delete",
+ });
+ }
+
+ private void exportThemeJson(Path toDirectory) throws IOException {
+ List themeNames = assetVersions.getThemeNames();
+ for (String themeName : themeNames) {
+ exportJson(toDirectory, "theme?theme=" + themeName, themeName);
+ }
}
private void exportMetadataJson(Path toDirectory) throws IOException {
@@ -172,7 +185,7 @@ private String toJsonResourceName(String resource) {
private Optional getJsonResponse(String resource) {
try {
- return jsonHandler.getResolver().resolve(new Request("GET", "/v1/" + resource, null, Collections.emptyMap()));
+ return jsonHandler.getResolver().resolve(new Request("GET", "/v1/" + resource, null, Collections.emptyMap(), null));
} catch (WebUserAuthException e) {
// The rest of the exceptions should not be thrown
throw new IllegalStateException("Unexpected exception thrown: " + e, e);
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/ServerPageExporter.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/ServerPageExporter.java
index 48150bad78..0538a6843a 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/ServerPageExporter.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/ServerPageExporter.java
@@ -182,7 +182,7 @@ private String toJSONResourceName(String resource) {
private Optional getJSONResponse(String resource) {
try {
- return jsonHandler.getResolver().resolve(new Request("GET", "/v1/" + resource, null, Collections.emptyMap()));
+ return jsonHandler.getResolver().resolve(new Request("GET", "/v1/" + resource, null, Collections.emptyMap(), null));
} catch (WebUserAuthException e) {
// The rest of the exceptions should not be thrown
throw new IllegalStateException("Unexpected exception thrown: " + e, e);
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/html/icon/Color.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/html/icon/Color.java
index dbf9bb3a62..1466620aa3 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/html/icon/Color.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/html/icon/Color.java
@@ -21,26 +21,26 @@
import java.util.Optional;
public enum Color {
- RED("col-red"),
- PINK("col-pink"),
- PURPLE("col-purple"),
- DEEP_PURPLE("col-deep-purple"),
- INDIGO("col-indigo"),
- BLUE("col-blue"),
- LIGHT_BLUE("col-light-blue"),
- CYAN("col-cyan"),
- TEAL("col-teal"),
- GREEN("col-green"),
- LIGHT_GREEN("col-light-green"),
- LIME("col-lime"),
- YELLOW("col-yellow"),
- AMBER("col-amber"),
- ORANGE("col-orange"),
- DEEP_ORANGE("col-deep-orange"),
- BROWN("col-brown"),
- GREY("col-grey"),
- BLUE_GREY("col-blue-grey"),
- BLACK("col-black"),
+ RED("col-plugin-red"),
+ PINK("col-plugin-pink"),
+ PURPLE("col-plugin-purple"),
+ DEEP_PURPLE("col-plugin-deep-purple"),
+ INDIGO("col-plugin-indigo"),
+ BLUE("col-plugin-blue"),
+ LIGHT_BLUE("col-plugin-light-blue"),
+ CYAN("col-plugin-cyan"),
+ TEAL("col-plugin-teal"),
+ GREEN("col-plugin-green"),
+ LIGHT_GREEN("col-plugin-light-green"),
+ LIME("col-plugin-lime"),
+ YELLOW("col-plugin-yellow"),
+ AMBER("col-plugin-amber"),
+ ORANGE("col-plugin-orange"),
+ DEEP_ORANGE("col-plugin-deep-orange"),
+ BROWN("col-plugin-brown"),
+ GREY("col-plugin-grey"),
+ BLUE_GREY("col-plugin-blue-grey"),
+ BLACK("col-plugin-black"),
NONE("");
private final String htmlClass;
@@ -65,6 +65,6 @@ public String getHtmlClass() {
}
public String getBackgroundColorClass() {
- return StringUtils.replace(htmlClass, "col-", "bg-");
+ return StringUtils.replace(htmlClass, "col-plugin-", "bg-plugin-");
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/JSONFactory.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/JSONFactory.java
index 397aeec22b..fa2ceb54bf 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/JSONFactory.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/JSONFactory.java
@@ -41,7 +41,6 @@
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.GenericLang;
import com.djrapitops.plan.settings.locale.lang.HtmlLang;
-import com.djrapitops.plan.settings.theme.Theme;
import com.djrapitops.plan.settings.theme.ThemeVal;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.Database;
@@ -74,7 +73,6 @@ public class JSONFactory {
private final DBSystem dbSystem;
private final ServerInfo serverInfo;
private final ServerUptimeCalculator serverUptimeCalculator;
- private final Theme theme;
private final Graphs graphs;
private final Formatters formatters;
@@ -85,7 +83,6 @@ public JSONFactory(
DBSystem dbSystem,
ServerInfo serverInfo,
ServerUptimeCalculator serverUptimeCalculator,
- Theme theme,
Graphs graphs,
Formatters formatters
) {
@@ -94,7 +91,6 @@ public JSONFactory(
this.dbSystem = dbSystem;
this.serverInfo = serverInfo;
this.serverUptimeCalculator = serverUptimeCalculator;
- this.theme = theme;
this.graphs = graphs;
this.formatters = formatters;
}
@@ -260,7 +256,7 @@ public void addActiveSessions(List sessions) {
public List> serverPlayerKillsAsJSONMaps(ServerUUID serverUUID) {
Database db = dbSystem.getDatabase();
- List kills = db.query(KillQueries.fetchPlayerKillsOnServer(serverUUID, 100));
+ List kills = db.query(KillQueries.fetchPlayerKillsOnServer(serverUUID, 50000));
return new PlayerKillMutator(kills).toJSONAsMap(formatters);
}
@@ -275,7 +271,6 @@ public Map serversAsJSONMaps() {
long now = System.currentTimeMillis();
long weekAgo = now - TimeUnit.DAYS.toMillis(7L);
- Formatter year = formatters.yearLong();
Formatter decimals = formatters.decimals();
Formatter timeAmount = formatters.timeAmount();
@@ -307,12 +302,12 @@ public Map serversAsJSONMaps() {
Map server = new HashMap<>();
server.put("name", entry.getValue().getIdentifiableName());
server.put("serverUUID", entry.getValue().getUuid().toString());
- server.put("playersOnlineColor", theme.getValue(ThemeVal.GRAPH_PLAYERS_ONLINE));
+ server.put("playersOnlineColor", ThemeVal.GRAPH_PLAYERS_ONLINE.getDefaultValue());
Optional> recentPeak = db.query(TPSQueries.fetchPeakPlayerCount(serverUUID, now - TimeUnit.DAYS.toMillis(2L)));
Optional> allTimePeak = db.query(TPSQueries.fetchAllTimePeakPlayerCount(serverUUID));
- server.put("last_peak_date", recentPeak.map(DateObj::getDate).map(year).orElse("-"));
- server.put("best_peak_date", allTimePeak.map(DateObj::getDate).map(year).orElse("-"));
+ server.put("last_peak_date", recentPeak.map(DateObj::getDate).map(Object.class::cast).orElse("-"));
+ server.put("best_peak_date", allTimePeak.map(DateObj::getDate).map(Object.class::cast).orElse("-"));
server.put("last_peak_players", recentPeak.map(DateObj::getValue).orElse(0));
server.put("best_peak_players", allTimePeak.map(DateObj::getValue).orElse(0));
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/PlayerJSONCreator.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/PlayerJSONCreator.java
index 3a036491c7..805c946e2b 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/PlayerJSONCreator.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/PlayerJSONCreator.java
@@ -64,7 +64,6 @@ public class PlayerJSONCreator {
private final Graphs graphs;
private final Formatters formatters;
- private final Formatter timeAmount;
private final Formatter decimals;
private final Formatter year;
@@ -81,7 +80,6 @@ public PlayerJSONCreator(
this.dbSystem = dbSystem;
this.formatters = formatters;
- timeAmount = formatters.timeAmount();
decimals = formatters.decimals();
year = formatters.yearLong();
this.graphs = graphs;
@@ -106,10 +104,10 @@ public Map createJSONAsMap(UUID playerUUID, Predicate Nickname.fromDataNicknames(nicks, serverNames, year))
+ .map(nicks -> Nickname.fromDataNicknames(nicks, serverNames))
.orElse(Collections.emptyList()));
data.put("connections", player.getValue(PlayerKeys.GEO_INFO)
- .map(geoInfo -> ConnectionInfo.fromGeoInfo(geoInfo, year))
+ .map(ConnectionInfo::fromGeoInfo)
.orElse(Collections.emptyList()));
data.put("punchcard_series", graphs.special().punchCard(sessionsMutator).getDots());
} else {
@@ -133,9 +131,9 @@ public Map createJSONAsMap(UUID playerUUID, Predicate> serverAccordion = new ServerAccordion(player, serverNames, graphs, year, timeAmount, GenericLang.UNKNOWN.getKey()).asMaps();
+ List> serverAccordion = new ServerAccordion(player, serverNames, graphs, GenericLang.UNKNOWN.getKey()).asMaps();
Map worldTimesPerServer = PerServerMutator.forContainer(player).worldTimesPerServer();
- String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE);
+ String[] pieColors = theme.getWorldPieColors();
data.put("ping_graph", createPingGraphJson(player));
data.put("servers", serverAccordion);
@@ -158,9 +156,9 @@ private Map createPingGraphJson(PlayerContainer player) {
.put("avg_ping_series", pingGraph.getAvgGraph().getPointArrays())
.put("max_ping_series", pingGraph.getMaxGraph().getPointArrays())
.put("colors", Maps.builder(String.class, String.class)
- .put("min", theme.getValue(ThemeVal.GRAPH_MIN_PING))
- .put("avg", theme.getValue(ThemeVal.GRAPH_AVG_PING))
- .put("max", theme.getValue(ThemeVal.GRAPH_MAX_PING))
+ .put("min", ThemeVal.GRAPH_MIN_PING.getDefaultValue())
+ .put("avg", ThemeVal.GRAPH_AVG_PING.getDefaultValue())
+ .put("max", ThemeVal.GRAPH_MAX_PING.getDefaultValue())
.build())
.build();
}
@@ -174,21 +172,21 @@ private Map createOnlineActivityJSONMap(SessionsMutator sessions
Map onlineActivity = new HashMap<>();
- onlineActivity.put("playtime_30d", timeAmount.apply(sessions30d.toPlaytime()));
- onlineActivity.put("active_playtime_30d", timeAmount.apply(sessions30d.toActivePlaytime()));
- onlineActivity.put("afk_time_30d", timeAmount.apply(sessions30d.toAfkTime()));
- onlineActivity.put("average_session_length_30d", timeAmount.apply(sessions30d.toAverageSessionLength()));
- onlineActivity.put("median_session_length_30d", timeAmount.apply(sessions30d.toMedianSessionLength()));
+ onlineActivity.put("playtime_30d", sessions30d.toPlaytime());
+ onlineActivity.put("active_playtime_30d", sessions30d.toActivePlaytime());
+ onlineActivity.put("afk_time_30d", sessions30d.toAfkTime());
+ onlineActivity.put("average_session_length_30d", sessions30d.toAverageSessionLength());
+ onlineActivity.put("median_session_length_30d", sessions30d.toMedianSessionLength());
onlineActivity.put("session_count_30d", sessions30d.count());
onlineActivity.put("player_kill_count_30d", sessions30d.toPlayerKillCount());
onlineActivity.put("mob_kill_count_30d", sessions30d.toMobKillCount());
onlineActivity.put("death_count_30d", sessions30d.toDeathCount());
- onlineActivity.put("playtime_7d", timeAmount.apply(sessions7d.toPlaytime()));
- onlineActivity.put("active_playtime_7d", timeAmount.apply(sessions7d.toActivePlaytime()));
- onlineActivity.put("afk_time_7d", timeAmount.apply(sessions7d.toAfkTime()));
- onlineActivity.put("average_session_length_7d", timeAmount.apply(sessions7d.toAverageSessionLength()));
- onlineActivity.put("median_session_length_7d", timeAmount.apply(sessions7d.toMedianSessionLength()));
+ onlineActivity.put("playtime_7d", sessions7d.toPlaytime());
+ onlineActivity.put("active_playtime_7d", sessions7d.toActivePlaytime());
+ onlineActivity.put("afk_time_7d", sessions7d.toAfkTime());
+ onlineActivity.put("average_session_length_7d", sessions7d.toAverageSessionLength());
+ onlineActivity.put("median_session_length_7d", sessions7d.toMedianSessionLength());
onlineActivity.put("session_count_7d", sessions7d.count());
onlineActivity.put("player_kill_count_7d", sessions7d.toPlayerKillCount());
onlineActivity.put("mob_kill_count_7d", sessions7d.toMobKillCount());
@@ -214,13 +212,13 @@ private Map createInfoJSONMap(PlayerContainer player, Map serverNames.getOrDefault(favoriteServer, favoriteServer.toString())).orElse(GenericLang.UNKNOWN.getKey()));
info.put("latest_join_address", sessions.latestSession()
@@ -235,8 +233,8 @@ private Map createInfoJSONMap(PlayerContainer player, Map playerExtensionData(UUID playerUUID) {
public static class Nickname {
final String nickname;
final String server;
- final String date;
+ final long date;
- public Nickname(String nickname, String server, String date) {
+ public Nickname(String nickname, String server, long date) {
this.nickname = nickname;
this.server = server;
this.date = date;
@@ -354,8 +352,7 @@ public Nickname(String nickname, String server, String date) {
public static List fromDataNicknames(
List nicknames,
- Map serverNames,
- Formatter dateFormatter
+ Map serverNames
) {
nicknames.sort(new DateHolderRecentComparator());
List mapped = new ArrayList<>();
@@ -363,7 +360,7 @@ public static List fromDataNicknames(
mapped.add(new Nickname(
nickname.getName(),
serverNames.getOrDefault(nickname.getServerUUID(), nickname.getServerUUID().toString()),
- dateFormatter.apply(nickname.getDate())
+ nickname.getDate()
));
}
return mapped;
@@ -372,15 +369,15 @@ public static List fromDataNicknames(
public static class ConnectionInfo {
final String geolocation;
- final String date;
+ final long date;
- public ConnectionInfo(String geolocation, String date) {
+ public ConnectionInfo(String geolocation, long date) {
this.geolocation = geolocation;
this.date = date;
}
- public static List fromGeoInfo(List geoInfo, Formatter dateFormatter) {
- return Lists.map(geoInfo, i -> new ConnectionInfo(i.getGeolocation(), dateFormatter.apply(i.getDate())));
+ public static List fromGeoInfo(List geoInfo) {
+ return Lists.map(geoInfo, i -> new ConnectionInfo(i.getGeolocation(), i.getDate()));
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/ServerAccordion.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/ServerAccordion.java
index d534d9a741..ada682197b 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/ServerAccordion.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/ServerAccordion.java
@@ -22,7 +22,6 @@
import com.djrapitops.plan.delivery.domain.keys.PerServerKeys;
import com.djrapitops.plan.delivery.domain.keys.PlayerKeys;
import com.djrapitops.plan.delivery.domain.mutators.SessionsMutator;
-import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.rendering.json.graphs.Graphs;
import com.djrapitops.plan.delivery.rendering.json.graphs.pie.WorldPie;
import com.djrapitops.plan.gathering.domain.WorldTimes;
@@ -45,19 +44,13 @@ public class ServerAccordion {
private final String unknown;
private final Graphs graphs;
- private final Formatter year;
- private final Formatter timeAmount;
public ServerAccordion(
PlayerContainer container, Map serverNames,
Graphs graphs,
- Formatter year,
- Formatter timeAmount,
String unknown
) {
this.graphs = graphs;
- this.year = year;
- this.timeAmount = timeAmount;
this.serverNames = serverNames;
perServer = container.getValue(PlayerKeys.PER_SERVER)
@@ -82,15 +75,15 @@ public List> asMaps() {
server.put("banned", ofServer.getValue(PerServerKeys.BANNED).orElse(false));
server.put("operator", ofServer.getValue(PerServerKeys.OPERATOR).orElse(false));
- server.put("registered", year.apply(ofServer.getValue(PerServerKeys.REGISTERED).orElse(0L)));
- server.put("last_seen", year.apply(sessionsMutator.toLastSeen()));
+ server.put("registered", ofServer.getValue(PerServerKeys.REGISTERED).orElse(0L));
+ server.put("last_seen", sessionsMutator.toLastSeen());
server.put("join_address", ofServer.getValue(PerServerKeys.JOIN_ADDRESS).orElse("-"));
server.put("session_count", sessionsMutator.count());
- server.put("playtime", timeAmount.apply(sessionsMutator.toPlaytime()));
- server.put("afk_time", timeAmount.apply(sessionsMutator.toAfkTime()));
- server.put("session_median", timeAmount.apply(sessionsMutator.toMedianSessionLength()));
- server.put("longest_session_length", timeAmount.apply(sessionsMutator.toLongestSessionLength()));
+ server.put("playtime", sessionsMutator.toPlaytime());
+ server.put("afk_time", sessionsMutator.toAfkTime());
+ server.put("session_median", sessionsMutator.toMedianSessionLength());
+ server.put("longest_session_length", sessionsMutator.toLongestSessionLength());
server.put("mob_kills", sessionsMutator.toMobKillCount());
server.put("player_kills", sessionsMutator.toPlayerKillCount());
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/GraphJSONCreator.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/GraphJSONCreator.java
index d6160f0a3c..75372f0bd6 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/GraphJSONCreator.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/GraphJSONCreator.java
@@ -44,7 +44,6 @@
import com.djrapitops.plan.settings.config.paths.DataGatheringSettings;
import com.djrapitops.plan.settings.config.paths.DisplaySettings;
import com.djrapitops.plan.settings.config.paths.TimeSettings;
-import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.GenericLang;
import com.djrapitops.plan.settings.theme.Theme;
import com.djrapitops.plan.settings.theme.ThemeVal;
@@ -76,7 +75,6 @@
public class GraphJSONCreator {
private final PlanConfig config;
- private final Locale locale;
private final Theme theme;
private final DBSystem dbSystem;
private final Graphs graphs;
@@ -84,13 +82,11 @@ public class GraphJSONCreator {
@Inject
public GraphJSONCreator(
PlanConfig config,
- Locale locale,
Theme theme,
DBSystem dbSystem,
Graphs graphs
) {
this.config = config;
- this.locale = locale;
this.theme = theme;
this.dbSystem = dbSystem;
this.graphs = graphs;
@@ -111,14 +107,14 @@ public String performanceGraphJSON(ServerUUID serverUUID) {
",\"chunks\":" + lineGraphs.chunkGraph(tpsMutator).toHighChartsSeries() +
",\"disk\":" + lineGraphs.diskGraph(tpsMutator).toHighChartsSeries() +
",\"colors\":{" +
- "\"playersOnline\":\"" + theme.getValue(ThemeVal.GRAPH_PLAYERS_ONLINE) + "\"," +
- "\"cpu\":\"" + theme.getValue(ThemeVal.GRAPH_CPU) + "\"," +
- "\"ram\":\"" + theme.getValue(ThemeVal.GRAPH_RAM) + "\"," +
- "\"entities\":\"" + theme.getValue(ThemeVal.GRAPH_ENTITIES) + "\"," +
- "\"chunks\":\"" + theme.getValue(ThemeVal.GRAPH_CHUNKS) + "\"," +
- "\"low\":\"" + theme.getValue(ThemeVal.GRAPH_TPS_LOW) + "\"," +
- "\"med\":\"" + theme.getValue(ThemeVal.GRAPH_TPS_MED) + "\"," +
- "\"high\":\"" + theme.getValue(ThemeVal.GRAPH_TPS_HIGH) + "\"}" +
+ "\"playersOnline\":\"" + ThemeVal.GRAPH_PLAYERS_ONLINE.getDefaultValue() + "\"," +
+ "\"cpu\":\"" + ThemeVal.GRAPH_CPU.getDefaultValue() + "\"," +
+ "\"ram\":\"" + ThemeVal.GRAPH_RAM.getDefaultValue() + "\"," +
+ "\"entities\":\"" + ThemeVal.GRAPH_ENTITIES.getDefaultValue() + "\"," +
+ "\"chunks\":\"" + ThemeVal.GRAPH_CHUNKS.getDefaultValue() + "\"," +
+ "\"low\":\"" + ThemeVal.GRAPH_TPS_LOW.getDefaultValue() + "\"," +
+ "\"med\":\"" + ThemeVal.GRAPH_TPS_MED.getDefaultValue() + "\"," +
+ "\"high\":\"" + ThemeVal.GRAPH_TPS_HIGH.getDefaultValue() + "\"}" +
",\"zones\":{" +
"\"tpsThresholdMed\":" + config.get(DisplaySettings.GRAPH_TPS_THRESHOLD_MED) + ',' +
"\"tpsThresholdHigh\":" + config.get(DisplaySettings.GRAPH_TPS_THRESHOLD_HIGH) + ',' +
@@ -169,14 +165,14 @@ public Map optimizedPerformanceGraphJSON(ServerUUID serverUUID)
.put("keys", new String[]{"date", "playersOnline", "tps", "cpu", "ram", "entities", "chunks", "disk"})
.put("values", values)
.put("colors", Maps.builder(String.class, Object.class)
- .put("playersOnline", theme.getValue(ThemeVal.GRAPH_PLAYERS_ONLINE))
- .put("cpu", theme.getValue(ThemeVal.GRAPH_CPU))
- .put("ram", theme.getValue(ThemeVal.GRAPH_RAM))
- .put("entities", theme.getValue(ThemeVal.GRAPH_ENTITIES))
- .put("chunks", theme.getValue(ThemeVal.GRAPH_CHUNKS))
- .put("low", theme.getValue(ThemeVal.GRAPH_TPS_LOW))
- .put("med", theme.getValue(ThemeVal.GRAPH_TPS_MED))
- .put("high", theme.getValue(ThemeVal.GRAPH_TPS_HIGH))
+ .put("playersOnline", ThemeVal.GRAPH_PLAYERS_ONLINE.getDefaultValue())
+ .put("cpu", ThemeVal.GRAPH_CPU.getDefaultValue())
+ .put("ram", ThemeVal.GRAPH_RAM.getDefaultValue())
+ .put("entities", ThemeVal.GRAPH_ENTITIES.getDefaultValue())
+ .put("chunks", ThemeVal.GRAPH_CHUNKS.getDefaultValue())
+ .put("low", ThemeVal.GRAPH_TPS_LOW.getDefaultValue())
+ .put("med", ThemeVal.GRAPH_TPS_MED.getDefaultValue())
+ .put("high", ThemeVal.GRAPH_TPS_HIGH.getDefaultValue())
.build())
.put("zones", Maps.builder(String.class, Object.class)
.put("tpsThresholdMed", config.get(DisplaySettings.GRAPH_TPS_THRESHOLD_MED))
@@ -199,7 +195,7 @@ public String playersOnlineGraph(ServerUUID serverUUID) {
Point::fromDateObj
);
return "{\"playersOnline\":" + graphs.line().lineGraph(points).toHighChartsSeries() +
- ",\"color\":\"" + theme.getValue(ThemeVal.GRAPH_PLAYERS_ONLINE) + "\"}";
+ ",\"color\":\"" + ThemeVal.GRAPH_PLAYERS_ONLINE.getDefaultValue() + "\"}";
}
public String uniqueAndNewGraphJSON(ServerUUID serverUUID) {
@@ -245,8 +241,8 @@ public String createUniqueAndNewJSON(LineGraphFactory lineGraphs, NavigableMap createGeolocationJSON(Map geolocatio
.put("geolocation_series", worldMap.getEntries())
.put("geolocation_bar_series", geolocationBarGraph.getBars())
.put("colors", Maps.builder(String.class, String.class)
- .put("low", theme.getValue(ThemeVal.WORLD_MAP_LOW))
- .put("high", theme.getValue(ThemeVal.WORLD_MAP_HIGH))
- .put("bars", theme.getValue(ThemeVal.GREEN))
+ .put("low", ThemeVal.WORLD_MAP_LOW.getDefaultValue())
+ .put("high", ThemeVal.WORLD_MAP_HIGH.getDefaultValue())
+ .put("bars", ThemeVal.GREEN.getDefaultValue())
.build())
.build();
}
@@ -426,9 +422,9 @@ public String pingGraphsJSON(ServerUUID serverUUID) {
",\"avg_ping_series\":" + pingGraph.getAvgGraph().toHighChartsSeries() +
",\"max_ping_series\":" + pingGraph.getMaxGraph().toHighChartsSeries() +
",\"colors\":{" +
- "\"min\":\"" + theme.getValue(ThemeVal.GRAPH_MIN_PING) + "\"," +
- "\"avg\":\"" + theme.getValue(ThemeVal.GRAPH_AVG_PING) + "\"," +
- "\"max\":\"" + theme.getValue(ThemeVal.GRAPH_MAX_PING) + "\"" +
+ "\"min\":\"" + ThemeVal.GRAPH_MIN_PING.getDefaultValue() + "\"," +
+ "\"avg\":\"" + ThemeVal.GRAPH_AVG_PING.getDefaultValue() + "\"," +
+ "\"max\":\"" + ThemeVal.GRAPH_MAX_PING.getDefaultValue() + "\"" +
"}}";
}
@@ -440,14 +436,14 @@ public Map punchCardJSONAsMap(ServerUUID serverUUID) {
);
return Maps.builder(String.class, Object.class)
.put("punchCard", graphs.special().punchCard(sessions).getDots())
- .put("color", theme.getValue(ThemeVal.GRAPH_PUNCHCARD))
+ .put("color", ThemeVal.GRAPH_PUNCHCARD.getDefaultValue())
.build();
}
public Map serverPreferencePieJSONAsMap() {
long now = System.currentTimeMillis();
long monthAgo = now - TimeUnit.DAYS.toMillis(30L);
- String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE);
+ String[] pieColors = theme.getWorldPieColors();
Map playtimePerServer = dbSystem.getDatabase().query(SessionQueries.playtimePerServer(monthAgo, now));
return Maps.builder(String.class, Object.class)
@@ -465,14 +461,14 @@ public void translateUnknown(Map joinAddresses) {
}
public Map joinAddressesByDay(ServerUUID serverUUID, long after, long before, @Untrusted List addressFilter) {
- String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE);
+ String[] pieColors = theme.getWorldPieColors();
List>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(serverUUID, config.getTimeZone().getOffset(System.currentTimeMillis()), after, before, addressFilter));
return mapToJson(pieColors, joinAddresses);
}
public Map joinAddressesByDay(long after, long before, @Untrusted List addressFilter) {
- String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE);
+ String[] pieColors = theme.getWorldPieColors();
List>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(config.getTimeZone().getOffset(System.currentTimeMillis()), after, before, addressFilter));
return mapToJson(pieColors, joinAddresses);
@@ -547,6 +543,6 @@ public GraphCollection proxyPlayersOnlineGraphs() {
proxyGraphs.add(new ServerSpecificLineGraph(points, ServerDto.fromServer(proxy)));
}
- return new GraphCollection<>(proxyGraphs, theme.getValue(ThemeVal.GRAPH_PLAYERS_ONLINE));
+ return new GraphCollection<>(proxyGraphs, ThemeVal.GRAPH_PLAYERS_ONLINE.getDefaultValue());
}
}
\ No newline at end of file
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/calendar/CalendarFactory.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/calendar/CalendarFactory.java
index eaf1917d3f..cc8dd8f0d3 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/calendar/CalendarFactory.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/calendar/CalendarFactory.java
@@ -19,8 +19,6 @@
import com.djrapitops.plan.delivery.domain.container.PlayerContainer;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.settings.config.PlanConfig;
-import com.djrapitops.plan.settings.locale.Locale;
-import com.djrapitops.plan.settings.theme.Theme;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -34,28 +32,22 @@
*/
@Singleton
public class CalendarFactory {
- private final Theme theme;
private final PlanConfig config;
- private final Locale locale;
private final Formatters formatters;
@Inject
public CalendarFactory(
PlanConfig config,
- Locale locale,
- Formatters formatters,
- Theme theme
+ Formatters formatters
) {
this.config = config;
- this.locale = locale;
this.formatters = formatters;
- this.theme = theme;
}
public PlayerCalendar playerCalendar(PlayerContainer player) {
return new PlayerCalendar(
player,
- formatters.timeAmount(), formatters.yearLong(), formatters.iso8601NoClockLong(), theme, locale,
+ formatters.iso8601NoClockLong(),
config.getTimeZone()
);
}
@@ -68,7 +60,7 @@ public ServerCalendar serverCalendar(
) {
return new ServerCalendar(
uniquePerDay, newPerDay, playtimePerDay, sessionsPerDay,
- formatters.iso8601NoClockTZIndependentLong(), theme
+ formatters.iso8601NoClockTZIndependentLong()
);
}
}
\ No newline at end of file
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/calendar/PlayerCalendar.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/calendar/PlayerCalendar.java
index d3b1b62c20..893cbb300f 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/calendar/PlayerCalendar.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/calendar/PlayerCalendar.java
@@ -22,9 +22,7 @@
import com.djrapitops.plan.gathering.domain.FinishedSession;
import com.djrapitops.plan.gathering.domain.PlayerKill;
import com.djrapitops.plan.gathering.domain.PlayerKills;
-import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.HtmlLang;
-import com.djrapitops.plan.settings.theme.Theme;
import com.djrapitops.plan.settings.theme.ThemeVal;
import com.djrapitops.plan.utilities.java.Lists;
@@ -38,11 +36,7 @@
*/
public class PlayerCalendar {
- private final Formatter timeAmount;
- private final Formatter year;
private final Formatter iso8601Formatter;
- private final Theme theme;
- private final Locale locale;
private final TimeZone timeZone;
private final List allSessions;
@@ -50,21 +44,13 @@ public class PlayerCalendar {
PlayerCalendar(
PlayerContainer container,
- Formatter timeAmount,
- Formatter year,
Formatter iso8601Formatter,
- Theme theme,
- Locale locale,
TimeZone timeZone
) {
this.allSessions = container.getValue(PlayerKeys.SESSIONS).orElse(new ArrayList<>());
this.registered = container.getValue(PlayerKeys.REGISTERED).orElse(0L);
- this.timeAmount = timeAmount;
- this.year = year;
this.iso8601Formatter = iso8601Formatter;
- this.theme = theme;
- this.locale = locale;
this.timeZone = timeZone;
}
@@ -73,7 +59,7 @@ public List getEntries() {
entries.add(CalendarEntry
.of(HtmlLang.LABEL_REGISTERED.getKey(), registered, registered + timeZone.getOffset(registered))
- .withColor(theme.getValue(ThemeVal.LIGHT_GREEN))
+ .withColor(ThemeVal.LIGHT_GREEN.getDefaultValue())
);
Map> sessionsByDay = getSessionsByDay();
@@ -87,10 +73,10 @@ public List getEntries() {
entries.add(CalendarEntry
.of(HtmlLang.LABEL_PLAYTIME.getKey(), playtime, day)
- .withColor(theme.getValue(ThemeVal.GREEN))
+ .withColor(ThemeVal.GREEN.getDefaultValue())
);
entries.add(CalendarEntry.of(HtmlLang.SIDE_SESSIONS.getKey(), sessionCount, day)
- .withColor(theme.getValue(ThemeVal.TEAL)));
+ .withColor(ThemeVal.TEAL.getDefaultValue()));
}
long fiveMinutes = TimeUnit.MINUTES.toMillis(5L);
@@ -102,7 +88,7 @@ public List getEntries() {
entries.add(CalendarEntry
.of(HtmlLang.SESSION.getKey(), session.getLength(), start + timeZone.getOffset(start))
.withEnd(end + timeZone.getOffset(end))
- .withColor(theme.getValue(ThemeVal.TEAL))
+ .withColor(ThemeVal.TEAL.getDefaultValue())
);
for (PlayerKill kill : session.getExtraData(PlayerKills.class).map(PlayerKills::asList).orElseGet(ArrayList::new)) {
@@ -111,7 +97,7 @@ public List getEntries() {
entries.add(CalendarEntry
.of(HtmlLang.KILLED.getKey(), victim, time)
.withEnd(time + fiveMinutes)
- .withColor(theme.getValue(ThemeVal.RED))
+ .withColor(ThemeVal.RED.getDefaultValue())
);
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/calendar/ServerCalendar.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/calendar/ServerCalendar.java
index bd76a6bbe4..da32cff669 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/calendar/ServerCalendar.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/calendar/ServerCalendar.java
@@ -18,7 +18,6 @@
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.settings.locale.lang.HtmlLang;
-import com.djrapitops.plan.settings.theme.Theme;
import com.djrapitops.plan.settings.theme.ThemeVal;
import java.util.*;
@@ -36,22 +35,19 @@ public class ServerCalendar {
private final SortedMap playtimePerDay;
private final Formatter iso8601TZIndependent;
- private final Theme theme;
ServerCalendar(
SortedMap uniquePerDay,
SortedMap newPerDay,
SortedMap playtimePerDay,
NavigableMap sessionsPerDay,
- Formatter iso8601TZIndependent,
- Theme theme
+ Formatter iso8601TZIndependent
) {
this.uniquePerDay = uniquePerDay;
this.newPerDay = newPerDay;
this.iso8601TZIndependent = iso8601TZIndependent;
this.sessionsPerDay = sessionsPerDay;
this.playtimePerDay = playtimePerDay;
- this.theme = theme;
}
public List getEntries() {
@@ -74,7 +70,7 @@ private void appendNewPlayers(List entries) {
String day = iso8601TZIndependent.apply(key);
entries.add(CalendarEntry.of(HtmlLang.NEW_CALENDAR.getKey(), newPlayers, day)
- .withColor(theme.getValue(ThemeVal.LIGHT_GREEN)));
+ .withColor(ThemeVal.LIGHT_GREEN.getDefaultValue()));
}
}
@@ -102,7 +98,7 @@ private void appendPlaytime(List entries) {
String day = iso8601TZIndependent.apply(key);
entries.add(CalendarEntry.of(HtmlLang.LABEL_PLAYTIME.getKey(), playtime, day)
- .withColor(theme.getValue(ThemeVal.GREEN)));
+ .withColor(ThemeVal.GREEN.getDefaultValue()));
}
}
@@ -116,7 +112,7 @@ private void appendSessionCounts(List entries) {
String day = iso8601TZIndependent.apply(key);
entries.add(CalendarEntry.of(HtmlLang.SIDE_SESSIONS.getKey(), sessionCount, day)
- .withColor(theme.getValue(ThemeVal.TEAL)));
+ .withColor(ThemeVal.TEAL.getDefaultValue()));
}
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/pie/PieGraphFactory.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/pie/PieGraphFactory.java
index 4dd589dece..c41c2c296e 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/pie/PieGraphFactory.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/pie/PieGraphFactory.java
@@ -56,7 +56,7 @@ public PieGraphFactory(
}
public Pie activityPie(Map activityData) {
- String[] colors = theme.getPieColors(ThemeVal.GRAPH_ACTIVITY_PIE);
+ String[] colors = theme.getDefaultPieColors(ThemeVal.GRAPH_ACTIVITY_PIE);
return new ActivityPie(activityData, colors, ActivityIndex.getDefaultGroupLangKeys());
}
@@ -72,7 +72,7 @@ public WorldPie worldPie(WorldTimes worldTimes) {
WorldAliasSettings worldAliasSettings = config.getWorldAliasSettings();
Map playtimePerAlias = worldAliasSettings.getPlaytimePerAlias(worldTimes);
Map gmTimesPerAlias = worldAliasSettings.getGMTimesPerAlias(worldTimes);
- String[] colors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE);
+ String[] colors = theme.getWorldPieColors();
boolean orderByPercentage = config.isTrue(DisplaySettings.ORDER_WORLD_PIE_BY_PERCENTAGE);
return new WorldPie(playtimePerAlias, gmTimesPerAlias, colors, orderByPercentage);
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/stack/ActivityStackGraph.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/stack/ActivityStackGraph.java
index 1a23435cf7..7c097ecf9a 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/stack/ActivityStackGraph.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/stack/ActivityStackGraph.java
@@ -18,8 +18,8 @@
import com.djrapitops.plan.delivery.domain.DateMap;
import com.djrapitops.plan.delivery.domain.mutators.ActivityIndex;
-import com.djrapitops.plan.delivery.formatting.Formatter;
+import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
@@ -32,14 +32,12 @@
*/
class ActivityStackGraph extends StackGraph {
- ActivityStackGraph(DateMap> activityData, String[] colors, Formatter dayFormatter, String[] groups) {
- super(getLabels(activityData.navigableKeySet(), dayFormatter), getDataSets(activityData, colors, groups));
+ ActivityStackGraph(DateMap> activityData, String[] colors, String[] groups) {
+ super(getLabels(activityData.navigableKeySet()), getDataSets(activityData, colors, groups));
}
- private static String[] getLabels(Collection dates, Formatter dayFormatter) {
- return dates.stream()
- .map(dayFormatter)
- .toArray(String[]::new);
+ private static Serializable[] getLabels(Collection dates) {
+ return dates.toArray(Serializable[]::new);
}
private static StackDataSet[] initializeDataSet(String[] groups, String[] colors) {
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/stack/StackGraph.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/stack/StackGraph.java
index 50c7c72c9d..506f584dcd 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/stack/StackGraph.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/stack/StackGraph.java
@@ -16,6 +16,8 @@
*/
package com.djrapitops.plan.delivery.rendering.json.graphs.stack;
+import java.io.Serializable;
+
/**
* Utility for creating HighCharts Stack graphs.
*
@@ -24,14 +26,14 @@
public class StackGraph {
private final StackDataSet[] dataSets;
- private final String[] labels;
+ private final Serializable[] labels;
- public StackGraph(String[] labels, StackDataSet... dataSets) {
+ public StackGraph(Serializable[] labels, StackDataSet... dataSets) {
this.dataSets = dataSets;
this.labels = labels;
}
- public String[] getLabels() {
+ public Serializable[] getLabels() {
return labels;
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/stack/StackGraphFactory.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/stack/StackGraphFactory.java
index 7e70a2d5ee..1fdc4e0243 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/stack/StackGraphFactory.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/stack/StackGraphFactory.java
@@ -18,9 +18,6 @@
import com.djrapitops.plan.delivery.domain.DateMap;
import com.djrapitops.plan.delivery.domain.mutators.ActivityIndex;
-import com.djrapitops.plan.delivery.formatting.Formatter;
-import com.djrapitops.plan.delivery.formatting.Formatters;
-import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.theme.Theme;
import com.djrapitops.plan.settings.theme.ThemeVal;
@@ -36,23 +33,17 @@
@Singleton
public class StackGraphFactory {
- private final Locale locale;
private final Theme theme;
- private final Formatter dayFormatter;
@Inject
public StackGraphFactory(
- Locale locale,
- Formatters formatters,
Theme theme
) {
- this.locale = locale;
this.theme = theme;
- this.dayFormatter = formatters.dayLong();
}
public StackGraph activityStackGraph(DateMap> activityData) {
- String[] colors = theme.getPieColors(ThemeVal.GRAPH_ACTIVITY_PIE);
- return new ActivityStackGraph(activityData, colors, dayFormatter, ActivityIndex.getDefaultGroupLangKeys());
+ String[] colors = theme.getDefaultPieColors(ThemeVal.GRAPH_ACTIVITY_PIE);
+ return new ActivityStackGraph(activityData, colors, ActivityIndex.getDefaultGroupLangKeys());
}
}
\ No newline at end of file
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/ErrorMessagePage.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/ErrorMessagePage.java
index 0f881b74b9..5f63836760 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/ErrorMessagePage.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/ErrorMessagePage.java
@@ -18,7 +18,6 @@
import com.djrapitops.plan.delivery.formatting.PlaceholderReplacer;
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
-import com.djrapitops.plan.settings.theme.Theme;
import com.djrapitops.plan.utilities.java.UnaryChain;
import com.djrapitops.plan.version.VersionChecker;
@@ -34,26 +33,23 @@ public class ErrorMessagePage implements Page {
private final String errorTitle;
private final String errorMsg;
- private final Theme theme;
private final VersionChecker versionChecker;
public ErrorMessagePage(
String template, Icon icon, String errorTitle, String errorMsg,
- Theme theme, VersionChecker versionChecker
+ VersionChecker versionChecker
) {
this.template = template;
this.icon = icon;
this.errorTitle = errorTitle;
this.errorMsg = errorMsg;
- this.theme = theme;
this.versionChecker = versionChecker;
}
public ErrorMessagePage(
String template, String errorTitle, String errorMsg,
- VersionChecker versionChecker,
- Theme theme) {
- this(template, Icon.called("exclamation-circle").build(), errorTitle, errorMsg, theme, versionChecker);
+ VersionChecker versionChecker) {
+ this(template, Icon.called("exclamation-circle").build(), errorTitle, errorMsg, versionChecker);
}
@Override
@@ -64,7 +60,6 @@ public String toHtml() {
placeholders.put("paragraph", errorMsg);
placeholders.put("version", versionChecker.getCurrentVersion());
return UnaryChain.of(template)
- .chain(theme::replaceThemeColors)
.chain(placeholders::apply)
.apply();
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/PageFactory.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/PageFactory.java
index 030698da5e..8bc3f08e96 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/PageFactory.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/PageFactory.java
@@ -19,13 +19,9 @@
import com.djrapitops.plan.delivery.rendering.BundleAddressCorrection;
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
import com.djrapitops.plan.delivery.web.ResourceService;
-import com.djrapitops.plan.delivery.web.resolver.exception.NotFoundException;
import com.djrapitops.plan.delivery.web.resource.WebResource;
import com.djrapitops.plan.identification.ServerInfo;
-import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.settings.theme.Theme;
-import com.djrapitops.plan.storage.database.DBSystem;
-import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
import com.djrapitops.plan.storage.file.PlanFiles;
import com.djrapitops.plan.storage.file.PublicHtmlFiles;
import com.djrapitops.plan.utilities.dev.Untrusted;
@@ -49,7 +45,6 @@ public class PageFactory {
private final Lazy files;
private final Lazy publicHtmlFiles;
private final Lazy theme;
- private final Lazy dbSystem;
private final Lazy bundleAddressCorrection;
private static final String ERROR_HTML_FILE = "error.html";
@@ -59,7 +54,6 @@ public PageFactory(
Lazy files,
Lazy publicHtmlFiles,
Lazy theme,
- Lazy dbSystem,
Lazy serverInfo,
Lazy bundleAddressCorrection
) {
@@ -67,14 +61,9 @@ public PageFactory(
this.files = files;
this.publicHtmlFiles = publicHtmlFiles;
this.theme = theme;
- this.dbSystem = dbSystem;
this.bundleAddressCorrection = bundleAddressCorrection;
}
- public Page playersPage() throws IOException {
- return reactPage();
- }
-
public Page reactPage() throws IOException {
try {
String fileName = "index.html";
@@ -87,29 +76,6 @@ public Page reactPage() throws IOException {
}
}
- /**
- * Create a server page.
- *
- * @param serverUUID UUID of the server
- * @return {@link Page} that matches the server page.
- * @throws NotFoundException If the server can not be found in the database.
- * @throws IOException If the template files can not be read.
- */
- public Page serverPage(ServerUUID serverUUID) throws IOException {
- if (dbSystem.get().getDatabase().query(ServerQueries.fetchServerMatchingIdentifier(serverUUID)).isEmpty()) {
- throw new NotFoundException("Server not found in the database");
- }
- return reactPage();
- }
-
- public Page playerPage() throws IOException {
- return reactPage();
- }
-
- public Page networkPage() throws IOException {
- return reactPage();
- }
-
public Page internalErrorPage(String message, @Untrusted Throwable error) {
try {
return new InternalErrorPage(
@@ -124,13 +90,12 @@ public Page internalErrorPage(String message, @Untrusted Throwable error) {
public Page errorPage(String title, String error) throws IOException {
return new ErrorMessagePage(
- getResourceAsString(ERROR_HTML_FILE), title, error,
- versionChecker.get(), theme.get());
+ getResourceAsString(ERROR_HTML_FILE), title, error, versionChecker.get());
}
public Page errorPage(Icon icon, String title, String error) throws IOException {
return new ErrorMessagePage(
- getResourceAsString(ERROR_HTML_FILE), icon, title, error, theme.get(), versionChecker.get());
+ getResourceAsString(ERROR_HTML_FILE), icon, title, error, versionChecker.get());
}
public String getResourceAsString(String name) throws IOException {
@@ -152,20 +117,4 @@ public WebResource getPublicHtmlOrJarResource(String resourceName) {
.orElseGet(() -> files.get().getResourceFromJar("web/" + resourceName))
.asWebResource();
}
-
- public Page loginPage() throws IOException {
- return reactPage();
- }
-
- public Page registerPage() throws IOException {
- return reactPage();
- }
-
- public Page queryPage() throws IOException {
- return reactPage();
- }
-
- public Page errorsPage() throws IOException {
- return reactPage();
- }
}
\ No newline at end of file
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/AssetVersions.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/AssetVersions.java
index dbdbd56a80..2509555198 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/AssetVersions.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/AssetVersions.java
@@ -27,6 +27,7 @@
import java.io.IOException;
import java.util.List;
import java.util.Optional;
+import java.util.stream.Collectors;
@Singleton
public class AssetVersions {
@@ -68,4 +69,13 @@ public List getAssetPaths() throws IOException {
if (webAssetConfig == null) prepare();
return webAssetConfig.getConfigPaths();
}
+
+ public List getThemeNames() throws IOException {
+ return getAssetPaths().stream()
+ .filter(path -> path.startsWith("themes"))
+ .filter(path -> path.endsWith("json"))
+ .map(path -> path.substring(7, path.indexOf(",")))
+ .sorted()
+ .collect(Collectors.toList());
+ }
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseFactory.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseFactory.java
index 1ed0080673..e4b9ca8b98 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseFactory.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseFactory.java
@@ -28,7 +28,6 @@
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.ResponseBuilder;
-import com.djrapitops.plan.delivery.web.resolver.exception.NotFoundException;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resource.WebResource;
import com.djrapitops.plan.identification.Identifiers;
@@ -36,10 +35,11 @@
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.ErrorPageLang;
-import com.djrapitops.plan.settings.theme.Theme;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.queries.containers.ContainerFetchQueries;
+import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
+import com.djrapitops.plan.storage.file.FileResource;
import com.djrapitops.plan.storage.file.PlanFiles;
import com.djrapitops.plan.storage.file.PublicHtmlFiles;
import com.djrapitops.plan.storage.file.Resource;
@@ -55,8 +55,8 @@
import javax.inject.Singleton;
import java.io.IOException;
import java.io.UncheckedIOException;
-import java.util.Optional;
-import java.util.UUID;
+import java.nio.file.Path;
+import java.util.*;
import java.util.function.Function;
/**
@@ -74,7 +74,6 @@ public class ResponseFactory {
private final PageFactory pageFactory;
private final Locale locale;
private final DBSystem dbSystem;
- private final Theme theme;
private final Lazy addresses;
private final Lazy bundleAddressCorrection;
private final Formatter httpLastModifiedFormatter;
@@ -87,7 +86,6 @@ public ResponseFactory(
Locale locale,
DBSystem dbSystem,
Formatters formatters,
- Theme theme,
Lazy addresses,
Lazy bundleAddressCorrection
) {
@@ -96,7 +94,6 @@ public ResponseFactory(
this.pageFactory = pageFactory;
this.locale = locale;
this.dbSystem = dbSystem;
- this.theme = theme;
this.addresses = addresses;
httpLastModifiedFormatter = formatters.httpLastModifiedLong();
@@ -149,16 +146,6 @@ private Response forInternalError(@Untrusted Throwable error, String cause) {
.build();
}
- public Response playersPageResponse(@Untrusted Request request) {
- try {
- Optional error = checkDbClosedError();
- if (error.isPresent()) return error.get();
- return forPage(request, pageFactory.playersPage());
- } catch (IOException e) {
- return forInternalError(e, "Failed to generate players page");
- }
- }
-
private Optional checkDbClosedError() {
Database.State dbState = dbSystem.getDatabase().getState();
if (dbState != Database.State.OPEN) {
@@ -196,23 +183,14 @@ public Response internalErrorResponse(Throwable e, String cause) {
return forInternalError(e, cause);
}
- public Response networkPageResponse(@Untrusted Request request) {
- Optional error = checkDbClosedError();
- if (error.isPresent()) return error.get();
- try {
- return forPage(request, pageFactory.networkPage());
- } catch (IOException e) {
- return forInternalError(e, "Failed to generate network page");
- }
- }
-
public Response serverPageResponse(@Untrusted Request request, ServerUUID serverUUID) {
Optional error = checkDbClosedError();
if (error.isPresent()) return error.get();
try {
- return forPage(request, pageFactory.serverPage(serverUUID));
- } catch (NotFoundException e) {
- return notFound404(e.getMessage());
+ if (dbSystem.getDatabase().query(ServerQueries.fetchServerMatchingIdentifier(serverUUID)).isEmpty()) {
+ return notFound404("Server not found in the database");
+ }
+ return forPage(request, pageFactory.reactPage());
} catch (IOException e) {
return forInternalError(e, "Failed to generate server page");
}
@@ -235,7 +213,6 @@ public Response javaScriptResponse(@Untrusted String fileName) {
WebResource resource = getPublicOrJarResource(fileName);
String content = UnaryChain.of(resource.asString())
.chain(this::replaceMainAddressPlaceholder)
- .chain(theme::replaceThemeColors)
.chain(contents -> bundleAddressCorrection.get().correctAddressForWebserver(contents, fileName))
.apply();
ResponseBuilder responseBuilder = Response.builder()
@@ -270,7 +247,6 @@ public Response cssResponse(@Untrusted String fileName) {
try {
WebResource resource = getPublicOrJarResource(fileName);
String content = UnaryChain.of(resource.asString())
- .chain(theme::replaceThemeColors)
.chain(contents -> bundleAddressCorrection.get().correctAddressForWebserver(contents, fileName))
.apply();
@@ -447,9 +423,21 @@ public Response notFound404(String message) {
}
}
+ public Response notFound404Json(String message) {
+ return Response.builder()
+ .setMimeType(MimeType.JSON)
+ .setJSONContent(Map.of("status", "404", "message", message))
+ .setStatus(404)
+ .build();
+ }
+
public Response forbidden403() {
return forbidden403("Your user is not authorized to view this page. "
- + "If you believe this is an error contact staff to change your access level.");
+ + "If you believe this is an error contact staff to change your access.");
+ }
+
+ public Response forbidden403Json() {
+ return forbidden403Json("Your user is not authorized to view this page. If you believe this is an error contact staff to change your access.");
}
public Response forbidden403(String message) {
@@ -464,6 +452,19 @@ public Response forbidden403(String message) {
}
}
+ public Response forbidden403Json(String message) {
+ return Response.builder()
+ .setMimeType(MimeType.JSON)
+ .setJSONContent(Maps.builder(String.class, Object.class)
+ .put("status", 403)
+ .put("message", message)
+ .put("icon", List.of("far", "fa-hand-paper"))
+ .put("title", "403 Forbidden")
+ .build())
+ .setStatus(403)
+ .build();
+ }
+
public Response failedLoginAttempts403() {
return Response.builder()
.setMimeType(MimeType.HTML)
@@ -504,12 +505,24 @@ public Response badRequest(String errorMessage, String target) {
.build();
}
+ public Response methodNotAllowed405(String target, String... allowedMethods) {
+ return Response.builder()
+ .setMimeType(MimeType.JSON)
+ .setJSONContent(Maps.builder(String.class, Object.class)
+ .put("status", 405)
+ .put("error", "HTTP method not allowed, allowed: " + Arrays.toString(allowedMethods))
+ .put("requestedTarget", target)
+ .build())
+ .setStatus(405)
+ .build();
+ }
+
public Response playerPageResponse(@Untrusted Request request, UUID playerUUID) {
try {
Database db = dbSystem.getDatabase();
PlayerContainer player = db.query(ContainerFetchQueries.fetchPlayerContainer(playerUUID));
if (player.getValue(PlayerKeys.REGISTERED).isPresent()) {
- return forPage(request, pageFactory.playerPage());
+ return forPage(request, pageFactory.reactPage());
} else {
return forPage(request, pageFactory.reactPage(), 404);
}
@@ -520,38 +533,6 @@ public Response playerPageResponse(@Untrusted Request request, UUID playerUUID)
}
}
- public Response loginPageResponse(@Untrusted Request request) {
- try {
- return forPage(request, pageFactory.loginPage());
- } catch (IOException e) {
- return forInternalError(e, "Failed to generate login page");
- }
- }
-
- public Response registerPageResponse(@Untrusted Request request) {
- try {
- return forPage(request, pageFactory.registerPage());
- } catch (IOException e) {
- return forInternalError(e, "Failed to generate register page");
- }
- }
-
- public Response queryPageResponse(@Untrusted Request request) {
- try {
- return forPage(request, pageFactory.queryPage());
- } catch (IOException e) {
- return forInternalError(e, "Failed to generate query page");
- }
- }
-
- public Response errorsPageResponse(@Untrusted Request request) {
- try {
- return forPage(request, pageFactory.errorsPage());
- } catch (IOException e) {
- return forInternalError(e, "Failed to generate errors page");
- }
- }
-
public Response jsonFileResponse(String file) {
try {
return Response.builder()
@@ -570,4 +551,38 @@ public Response reactPageResponse(Request request) {
return forInternalError(e, "Could not read index.html");
}
}
+
+ public Response themeResponse(@Untrusted String themeName, Request request) {
+ Path themeDirectory = files.getThemeDirectory();
+ try {
+ String resourceName = themeName + ".json";
+ WebResource foundTheme = files.attemptToFind(themeDirectory, resourceName)
+ .map(file -> (Resource) new FileResource(file.getName(), file))
+ .orElseGet(() -> files.getResourceFromJar("themes/" + themeName + ".json"))
+ .asWebResource();
+
+ Optional lastModified = foundTheme.getLastModified();
+ @Untrusted Optional tag = Identifiers.getEtag(request);
+ if (tag.isPresent() && lastModified.isPresent() && tag.get() >= lastModified.get()) {
+ return browserCachedNotChangedResponse();
+ } else {
+ long date = lastModified.orElseGet(System::currentTimeMillis);
+ return Response.builder()
+ .setHeader(HttpHeader.CACHE_CONTROL.asString(), CacheStrategy.CHECK_ETAG)
+ .setHeader(HttpHeader.LAST_MODIFIED.asString(), httpLastModifiedFormatter.apply(date))
+ .setHeader(HttpHeader.ETAG.asString(), date)
+ .setJSONContent(foundTheme.asString())
+ .build();
+ }
+ } catch (UncheckedIOException e) {
+ return notFound404Json("Theme file by that name doesn't exist");
+ }
+ }
+
+ public Response successResponse() {
+ return Response.builder()
+ .setMimeType(MimeType.JSON)
+ .setJSONContent(Map.of("success", true))
+ .build();
+ }
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseResolver.java
index 10a9aa3a2f..abe51356a6 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseResolver.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseResolver.java
@@ -22,6 +22,7 @@
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
+import com.djrapitops.plan.delivery.web.resolver.exception.MethodNotAllowedException;
import com.djrapitops.plan.delivery.web.resolver.exception.NotFoundException;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
@@ -83,6 +84,7 @@ public class ResponseResolver {
private final SwaggerJsonResolver swaggerJsonResolver;
private final SwaggerPageResolver swaggerPageResolver;
private final ManagePageResolver managePageResolver;
+ private final ThemeEditorResolver themeEditorResolver;
private final ErrorLogger errorLogger;
private final ResolverService resolverService;
@@ -105,6 +107,7 @@ public ResponseResolver(
RootPageResolver rootPageResolver,
RootJSONResolver rootJSONResolver,
StaticResourceResolver staticResourceResolver,
+ ThemeEditorResolver themeEditorResolver,
PublicHtmlResolver publicHtmlResolver,
LoginPageResolver loginPageResolver,
@@ -130,6 +133,7 @@ public ResponseResolver(
this.rootPageResolver = rootPageResolver;
this.rootJSONResolver = rootJSONResolver;
this.staticResourceResolver = staticResourceResolver;
+ this.themeEditorResolver = themeEditorResolver;
this.publicHtmlResolver = publicHtmlResolver;
this.loginPageResolver = loginPageResolver;
this.registerPageResolver = registerPageResolver;
@@ -157,6 +161,7 @@ public void registerPages() {
resolverService.registerResolver(plugin, "/player", playerPageResolver);
resolverService.registerResolver(plugin, "/network", serverPageResolver);
resolverService.registerResolver(plugin, "/server", serverPageResolver);
+ resolverService.registerResolver(plugin, "/theme-editor", themeEditorResolver);
if (webServer.get().isAuthRequired()) {
resolverService.registerResolver(plugin, "/login", loginPageResolver);
resolverService.registerResolver(plugin, "/register", registerPageResolver);
@@ -186,10 +191,12 @@ private NoAuthResolver fileResolver(Supplier response) {
public Response getResponse(@Untrusted Request request) {
try {
return tryToGetResponse(request);
- } catch (NotFoundException e) {
- return responseFactory.notFound404(e.getMessage());
} catch (BadRequestException e) {
return responseFactory.badRequest(e.getMessage(), request.getPath().asString());
+ } catch (NotFoundException e) {
+ return responseFactory.notFound404(e.getMessage());
+ } catch (MethodNotAllowedException e) {
+ return responseFactory.methodNotAllowed405(e.getMessage(), e.getAllowedMethods());
} catch (WebUserAuthException e) {
throw e; // Pass along
} catch (Exception e) {
@@ -228,7 +235,11 @@ private Response tryToGetResponse(@Untrusted Request request) {
Optional resolved = resolver.resolve(request);
if (resolved.isPresent()) return resolved.get();
} else {
- return responseFactory.forbidden403();
+ if (request.getPath().startsWith("/v1/")) {
+ return responseFactory.forbidden403Json();
+ } else {
+ return responseFactory.forbidden403();
+ }
}
} else {
Optional resolved = resolver.resolve(request);
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/ActiveCookieStore.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/ActiveCookieStore.java
index 565daa3cb1..433e066525 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/ActiveCookieStore.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/ActiveCookieStore.java
@@ -31,6 +31,7 @@
import javax.inject.Inject;
import javax.inject.Singleton;
+import java.security.SecureRandom;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
@@ -40,7 +41,7 @@
@Singleton
public class ActiveCookieStore implements SubSystem {
- private static final Map USERS_BY_COOKIE = new ConcurrentHashMap<>();
+ private static final Map USERS_BY_COOKIE = new ConcurrentHashMap<>();
private static long cookieExpiresAfterMs = TimeUnit.HOURS.toMillis(2L);
private final ActiveCookieExpiryCleanupTask activeCookieExpiryCleanupTask;
@@ -76,7 +77,7 @@ private static void removeCookieStatic(String cookie) {
}
public static void removeUserCookie(@Untrusted String username) {
- USERS_BY_COOKIE.entrySet().stream().filter(entry -> entry.getValue().getUsername().equals(username))
+ USERS_BY_COOKIE.entrySet().stream().filter(entry -> entry.getValue().getUser().getUsername().equals(username))
.findAny()
.map(Map.Entry::getKey)
.ifPresent(ActiveCookieStore::removeCookieStatic);
@@ -94,11 +95,11 @@ public void enable() {
public void reloadActiveCookies() {
try {
- Map cookies = dbSystem.getDatabase().query(WebUserQueries.fetchActiveCookies());
+ Map cookies = dbSystem.getDatabase().query(WebUserQueries.fetchActiveCookies());
USERS_BY_COOKIE.clear();
USERS_BY_COOKIE.putAll(cookies);
- for (Map.Entry entry : dbSystem.getDatabase().query(WebUserQueries.getCookieExpiryTimes()).entrySet()) {
- long timeToExpiry = Math.max(entry.getValue() - System.currentTimeMillis(), 0L);
+ for (Map.Entry entry : cookies.entrySet()) {
+ long timeToExpiry = Math.max(entry.getValue().getExpires() - System.currentTimeMillis(), 0L);
activeCookieExpiryCleanupTask.addExpiry(entry.getKey(), System.currentTimeMillis() + timeToExpiry);
}
} catch (DBOpException databaseClosedUnexpectedly) {
@@ -112,26 +113,30 @@ public void disable() {
USERS_BY_COOKIE.clear();
}
- public Optional checkCookie(@Untrusted String cookie) {
+ public Optional findCookie(@Untrusted String cookie) {
return Optional.ofNullable(USERS_BY_COOKIE.get(cookie));
}
- public String generateNewCookie(User user) {
- String cookie = DigestUtils.sha256Hex(user.getUsername() + UUID.randomUUID() + System.currentTimeMillis());
- USERS_BY_COOKIE.put(cookie, user);
- saveNewCookie(user, cookie, System.currentTimeMillis());
+ public String generateNewCookie(User user, String ipAddress) {
+ SecureRandom secureRandom = new SecureRandom();
+ String cookie = DigestUtils.sha256Hex(user.getUsername() + UUID.randomUUID() + System.currentTimeMillis() + secureRandom.nextLong());
+ long expiresAt = System.currentTimeMillis() + cookieExpiresAfterMs;
+ USERS_BY_COOKIE.put(cookie, new CookieMetadata(user, expiresAt, ipAddress));
+ saveNewCookie(user, cookie, System.currentTimeMillis(), ipAddress);
activeCookieExpiryCleanupTask.addExpiry(cookie, System.currentTimeMillis() + cookieExpiresAfterMs);
return cookie;
}
- private void saveNewCookie(User user, String cookie, long now) {
+ private void saveNewCookie(User user, String cookie, long now, String ipAddress) {
dbSystem.getDatabase().executeTransaction(CookieChangeTransaction.storeCookie(
- user.getUsername(), cookie, now + cookieExpiresAfterMs
+ user.getUsername(), cookie, now + cookieExpiresAfterMs, ipAddress
));
}
public void removeCookie(@Untrusted String cookie) {
- checkCookie(cookie).map(User::getUsername)
+ findCookie(cookie)
+ .map(CookieMetadata::getUser)
+ .map(User::getUsername)
.ifPresent(this::deleteCookieByUser);
USERS_BY_COOKIE.remove(cookie);
deleteCookie(cookie);
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/AuthenticationExtractor.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/AuthenticationExtractor.java
index d650fe0759..d22bcb687c 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/AuthenticationExtractor.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/AuthenticationExtractor.java
@@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.delivery.webserver.auth;
+import com.djrapitops.plan.delivery.webserver.configuration.WebserverConfiguration;
import com.djrapitops.plan.delivery.webserver.http.InternalRequest;
import com.djrapitops.plan.utilities.dev.Untrusted;
@@ -28,20 +29,22 @@
public class AuthenticationExtractor {
private final ActiveCookieStore activeCookieStore;
+ private final WebserverConfiguration webserverConfiguration;
@Inject
- public AuthenticationExtractor(ActiveCookieStore activeCookieStore) {
+ public AuthenticationExtractor(ActiveCookieStore activeCookieStore, WebserverConfiguration webserverConfiguration) {
this.activeCookieStore = activeCookieStore;
+ this.webserverConfiguration = webserverConfiguration;
}
public Optional extractAuthentication(InternalRequest internalRequest) {
- return getCookieAuthentication(internalRequest.getCookies());
+ return getCookieAuthentication(internalRequest.getCookies(), internalRequest.getAccessAddress(webserverConfiguration));
}
- private Optional getCookieAuthentication(@Untrusted List cookies) {
+ private Optional getCookieAuthentication(@Untrusted List cookies, @Untrusted String accessAddress) {
for (@Untrusted Cookie cookie : cookies) {
if ("auth".equals(cookie.getName())) {
- return Optional.of(new CookieAuthentication(activeCookieStore, cookie.getValue()));
+ return Optional.of(new CookieAuthentication(activeCookieStore, cookie.getValue(), accessAddress));
}
}
return Optional.empty();
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/CookieAuthentication.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/CookieAuthentication.java
index 2da05fc7ec..6eac52f990 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/CookieAuthentication.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/CookieAuthentication.java
@@ -19,19 +19,27 @@
import com.djrapitops.plan.delivery.domain.auth.User;
import com.djrapitops.plan.utilities.dev.Untrusted;
+import java.util.Objects;
+
public class CookieAuthentication implements Authentication {
private final ActiveCookieStore activeCookieStore;
@Untrusted
private final String cookie;
+ @Untrusted
+ private final String accessAddress;
- public CookieAuthentication(ActiveCookieStore activeCookieStore, @Untrusted String cookie) {
+ public CookieAuthentication(ActiveCookieStore activeCookieStore, @Untrusted String cookie, @Untrusted String accessAddress) {
this.activeCookieStore = activeCookieStore;
this.cookie = cookie;
+ this.accessAddress = accessAddress;
}
@Override
public User getUser() {
- return activeCookieStore.checkCookie(cookie).orElse(null);
+ return activeCookieStore.findCookie(cookie)
+ // Prevents another IP from using a cookie granted to one IP
+ .filter(cookieMetadata -> Objects.equals(cookieMetadata.getIpAddress(), accessAddress))
+ .map(CookieMetadata::getUser).orElse(null);
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/CookieMetadata.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/CookieMetadata.java
new file mode 100644
index 0000000000..d611d7ccf3
--- /dev/null
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/auth/CookieMetadata.java
@@ -0,0 +1,74 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.webserver.auth;
+
+import com.djrapitops.plan.delivery.domain.auth.User;
+
+import java.util.Objects;
+
+/**
+ * @author AuroraLS3
+ */
+public class CookieMetadata {
+
+ private final User user;
+ private final String ipAddress;
+ private long expires;
+
+ public CookieMetadata(User user, long expires, String ipAddress) {
+ this.user = user;
+ this.expires = expires;
+ this.ipAddress = ipAddress;
+ }
+
+ public User getUser() {
+ return user;
+ }
+
+ public long getExpires() {
+ return expires;
+ }
+
+ public void setExpires(long expires) {
+ this.expires = expires;
+ }
+
+ public String getIpAddress() {
+ return ipAddress;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) return false;
+ CookieMetadata that = (CookieMetadata) o;
+ return getExpires() == that.getExpires() && Objects.equals(getUser(), that.getUser()) && Objects.equals(getIpAddress(), that.getIpAddress());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getUser(), getExpires(), getIpAddress());
+ }
+
+ @Override
+ public String toString() {
+ return "CookieMetadata{" +
+ "user=" + user +
+ ", expires=" + expires +
+ ", ipAddress='" + ipAddress + '\'' +
+ '}';
+ }
+}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/InternalRequest.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/InternalRequest.java
index 63c839c5a9..2d7e643a4a 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/InternalRequest.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/InternalRequest.java
@@ -54,7 +54,7 @@ default String getAccessAddress(WebserverConfiguration webserverConfiguration) {
return getAccessAddressFromSocketIp();
}
- Request toRequest();
+ Request toRequest(@Untrusted(reason = "from header sometimes") String accessAddress);
Map getRequestHeaders();
@@ -68,7 +68,7 @@ default String getAccessAddress(WebserverConfiguration webserverConfiguration) {
String getRequestedURIString();
- default WebUser getWebUser(WebserverConfiguration webserverConfiguration, AuthenticationExtractor authenticationExtractor) {
+ default WebUser getWebUser(WebserverConfiguration webserverConfiguration, AuthenticationExtractor authenticationExtractor, @Untrusted String accessAddress) {
return getAuthentication(webserverConfiguration, authenticationExtractor)
.map(Authentication::getUser) // Can throw WebUserAuthException
.map(User::toWebUser)
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/JettyInternalRequest.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/JettyInternalRequest.java
index 52855fdd81..a680217ece 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/JettyInternalRequest.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/JettyInternalRequest.java
@@ -71,14 +71,14 @@ public String getAccessAddressFromHeader() {
}
@Override
- public com.djrapitops.plan.delivery.web.resolver.request.Request toRequest() {
+ public com.djrapitops.plan.delivery.web.resolver.request.Request toRequest(@Untrusted String accessAddress) {
String requestMethod = baseRequest.getMethod();
@Untrusted URIPath path = new URIPath(baseRequest.getHttpURI().getDecodedPath());
@Untrusted URIQuery query = new URIQuery(baseRequest.getHttpURI().getQuery());
@Untrusted byte[] requestBody = readRequestBody();
- WebUser user = getWebUser(webserverConfiguration, authenticationExtractor);
+ WebUser user = getWebUser(webserverConfiguration, authenticationExtractor, accessAddress);
@Untrusted Map headers = getRequestHeaders();
- return new com.djrapitops.plan.delivery.web.resolver.request.Request(requestMethod, path, query, user, headers, requestBody);
+ return new com.djrapitops.plan.delivery.web.resolver.request.Request(requestMethod, path, query, user, headers, requestBody, accessAddress);
}
@Override
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/RequestHandler.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/RequestHandler.java
index 3cb1a99954..c7839b3cdb 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/RequestHandler.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/RequestHandler.java
@@ -56,7 +56,7 @@ public RequestHandler(WebserverConfiguration webserverConfiguration, ResponseFac
}
public Response getResponse(InternalRequest internalRequest) {
- @Untrusted String accessAddress = internalRequest.getAccessAddress(webserverConfiguration);
+ @Untrusted(reason = "from header") String accessAddress = internalRequest.getAccessAddress(webserverConfiguration);
@Untrusted String requestedPath = internalRequest.getRequestedPath();
boolean blocked = false;
@@ -74,7 +74,7 @@ public Response getResponse(InternalRequest internalRequest) {
response = responseFactory.ipWhitelist403(accessAddress);
} else {
try {
- request = internalRequest.toRequest();
+ request = internalRequest.toRequest(accessAddress);
response = attemptToResolve(request, accessAddress);
} catch (WebUserAuthException thrownByAuthentication) {
response = processFailedAuthentication(internalRequest, accessAddress, thrownByAuthentication);
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ErrorsPageResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ErrorsPageResolver.java
index 549c4e0b8f..e130f8f6fc 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ErrorsPageResolver.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ErrorsPageResolver.java
@@ -41,7 +41,7 @@ public boolean canAccess(Request request) {
@Override
public Optional resolve(Request request) {
- return Optional.of(responseFactory.errorsPageResponse(request));
+ return Optional.of(responseFactory.reactPageResponse(request));
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/PlayersPageResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/PlayersPageResolver.java
index 7af5f3c635..87e87e8d5f 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/PlayersPageResolver.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/PlayersPageResolver.java
@@ -52,6 +52,6 @@ public boolean canAccess(Request request) {
public Optional resolve(Request request) {
// Redirect /players/ to /players
if (request.getPath().getPart(1).isPresent()) return Optional.of(responseFactory.redirectResponse("/players"));
- return Optional.of(responseFactory.playersPageResponse(request));
+ return Optional.of(responseFactory.reactPageResponse(request));
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/QueryPageResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/QueryPageResolver.java
index e6651b5851..74cba264b6 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/QueryPageResolver.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/QueryPageResolver.java
@@ -43,6 +43,6 @@ public boolean canAccess(Request request) {
@Override
public Optional resolve(Request request) {
- return Optional.of(responseFactory.queryPageResponse(request));
+ return Optional.of(responseFactory.reactPageResponse(request));
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ServerPageResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ServerPageResolver.java
index ee4477fc9c..19229480bc 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ServerPageResolver.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ServerPageResolver.java
@@ -87,7 +87,7 @@ private Optional getServerPage(ServerUUID serverUUID, @Untrusted Reque
boolean toNetworkPage = serverInfo.getServer().isProxy() && serverInfo.getServerUUID().equals(serverUUID);
if (toNetworkPage) {
if (request.getPath().getPart(0).map(NETWORK_PAGE::equals).orElse(false)) {
- return Optional.of(responseFactory.networkPageResponse(request));
+ return Optional.of(responseFactory.reactPageResponse(request));
} else {
// Accessing /server/Server which should be redirected to /network
return redirectToCurrentServer();
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ThemeEditorResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ThemeEditorResolver.java
new file mode 100644
index 0000000000..77804a49bd
--- /dev/null
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/ThemeEditorResolver.java
@@ -0,0 +1,51 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.webserver.resolver;
+
+import com.djrapitops.plan.delivery.domain.auth.WebPermission;
+import com.djrapitops.plan.delivery.web.resolver.Resolver;
+import com.djrapitops.plan.delivery.web.resolver.Response;
+import com.djrapitops.plan.delivery.web.resolver.request.Request;
+import com.djrapitops.plan.delivery.webserver.ResponseFactory;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.util.Optional;
+
+/**
+ * Resolver endpoint for /theme-editor pages.
+ *
+ * @author AuroraLS3
+ */
+@Singleton
+public class ThemeEditorResolver implements Resolver {
+
+ private final ResponseFactory responseFactory;
+
+ @Inject
+ public ThemeEditorResolver(ResponseFactory responseFactory) {this.responseFactory = responseFactory;}
+
+ @Override
+ public boolean canAccess(Request request) {
+ return request.getUser().map(user -> user.hasPermission(WebPermission.ACCESS_THEME_EDITOR)).orElse(false);
+ }
+
+ @Override
+ public Optional resolve(Request request) {
+ return Optional.of(responseFactory.reactPageResponse(request));
+ }
+}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/auth/LoginPageResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/auth/LoginPageResolver.java
index 13c5179041..ddeccc3b52 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/auth/LoginPageResolver.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/auth/LoginPageResolver.java
@@ -52,6 +52,6 @@ public Optional resolve(@Untrusted Request request) {
.filter(redirectBackTo -> !redirectBackTo.startsWith("http"));
return Optional.of(responseFactory.redirectResponse(from.orElse("/")));
}
- return Optional.of(responseFactory.loginPageResponse(request));
+ return Optional.of(responseFactory.reactPageResponse(request));
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/auth/LoginResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/auth/LoginResolver.java
index 8910f332c2..1136019e1b 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/auth/LoginResolver.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/auth/LoginResolver.java
@@ -78,7 +78,7 @@ public LoginResolver(
@Override
public Optional resolve(Request request) {
try {
- String cookie = activeCookieStore.generateNewCookie(getUser(request));
+ String cookie = activeCookieStore.generateNewCookie(getUser(request), request.getAccessIpAddress());
return Optional.of(getResponse(cookie));
} catch (DBOpException | PassEncryptException e) {
throw new WebUserAuthException(e);
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/auth/RegisterPageResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/auth/RegisterPageResolver.java
index 37c3fd32ee..1015fb3fcf 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/auth/RegisterPageResolver.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/auth/RegisterPageResolver.java
@@ -50,6 +50,6 @@ public Optional resolve(@Untrusted Request request) {
if (user.isPresent() || !webServer.get().isAuthRequired()) {
return Optional.of(responseFactory.redirectResponse("/"));
}
- return Optional.of(responseFactory.registerPageResponse(request));
+ return Optional.of(responseFactory.reactPageResponse(request));
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RootJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RootJSONResolver.java
index 497ad31375..78936c4e56 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RootJSONResolver.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RootJSONResolver.java
@@ -27,6 +27,9 @@
import com.djrapitops.plan.delivery.webserver.resolver.json.plugins.PluginHistoryJSONResolver;
import com.djrapitops.plan.delivery.webserver.resolver.json.query.FiltersJSONResolver;
import com.djrapitops.plan.delivery.webserver.resolver.json.query.QueryJSONResolver;
+import com.djrapitops.plan.delivery.webserver.resolver.json.theme.DeleteThemeJSONResolver;
+import com.djrapitops.plan.delivery.webserver.resolver.json.theme.SaveThemeJSONResolver;
+import com.djrapitops.plan.delivery.webserver.resolver.json.theme.ThemeJSONResolver;
import com.djrapitops.plan.delivery.webserver.resolver.json.webgroup.*;
import com.djrapitops.plan.identification.Identifiers;
import dagger.Lazy;
@@ -54,6 +57,8 @@ public class RootJSONResolver {
private final CompositeResolver.Builder readOnlyResourcesBuilder;
private final StorePreferencesJSONResolver storePreferencesJSONResolver;
private final PluginHistoryJSONResolver pluginHistoryJSONResolver;
+ private final SaveThemeJSONResolver saveThemeJSONResolver;
+ private final DeleteThemeJSONResolver deleteThemeJSONResolver;
private CompositeResolver resolver;
@Inject
@@ -92,8 +97,13 @@ public RootJSONResolver(
PluginHistoryJSONResolver pluginHistoryJSONResolver,
AllowlistJSONResolver allowlistJSONResolver,
+ ThemeJSONResolver themeJSONResolver,
+ SaveThemeJSONResolver saveThemeJSONResolver,
+ DeleteThemeJSONResolver deleteThemeJSONResolver,
+
PreferencesJSONResolver preferencesJSONResolver,
StorePreferencesJSONResolver storePreferencesJSONResolver,
+
WebGroupJSONResolver webGroupJSONResolver,
WebGroupPermissionJSONResolver webGroupPermissionJSONResolver,
WebPermissionJSONResolver webPermissionJSONResolver,
@@ -131,7 +141,8 @@ public RootJSONResolver(
.add("retention", retentionJSONResolver)
.add("joinAddresses", playerJoinAddressJSONResolver)
.add("preferences", preferencesJSONResolver)
- .add("gameAllowlistBounces", allowlistJSONResolver);
+ .add("gameAllowlistBounces", allowlistJSONResolver)
+ .add("theme", themeJSONResolver);
this.webServer = webServer;
// These endpoints require authentication to be enabled.
@@ -142,6 +153,8 @@ public RootJSONResolver(
this.webGroupSaveJSONResolver = webGroupSaveJSONResolver;
this.webGroupDeleteJSONResolver = webGroupDeleteJSONResolver;
this.storePreferencesJSONResolver = storePreferencesJSONResolver;
+ this.saveThemeJSONResolver = saveThemeJSONResolver;
+ this.deleteThemeJSONResolver = deleteThemeJSONResolver;
}
private ServerTabJSONResolver forJSON(DataID dataID, ServerTabJSONCreator tabJSONCreator, WebPermission permission) {
@@ -159,6 +172,8 @@ public CompositeResolver getResolver() {
.add("deleteGroup", webGroupDeleteJSONResolver)
.add("storePreferences", storePreferencesJSONResolver)
.add("pluginHistory", pluginHistoryJSONResolver)
+ .add("saveTheme", saveThemeJSONResolver)
+ .add("deleteTheme", deleteThemeJSONResolver)
.build();
} else {
resolver = readOnlyResourcesBuilder.build();
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/metadata/MetadataJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/metadata/MetadataJSONResolver.java
index bcb62e84ad..a8b5b9dad4 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/metadata/MetadataJSONResolver.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/metadata/MetadataJSONResolver.java
@@ -17,6 +17,7 @@
package com.djrapitops.plan.delivery.webserver.resolver.json.metadata;
import com.djrapitops.plan.delivery.rendering.html.Contributors;
+import com.djrapitops.plan.delivery.web.AssetVersions;
import com.djrapitops.plan.delivery.web.resolver.NoAuthResolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
@@ -27,6 +28,7 @@
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import com.djrapitops.plan.settings.theme.Theme;
import com.djrapitops.plan.settings.theme.ThemeVal;
+import com.djrapitops.plan.storage.file.PlanFiles;
import com.djrapitops.plan.utilities.java.Maps;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
@@ -34,33 +36,46 @@
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
+import net.playeranalytics.plugin.server.PluginLogger;
+import org.apache.commons.lang3.StringUtils;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
-import java.util.Optional;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.*;
+import java.util.stream.Stream;
@Singleton
@Path("/v1/metadata")
public class MetadataJSONResolver implements NoAuthResolver {
private final String mainCommand;
+ private final PlanFiles files;
private final PlanConfig config;
private final Theme theme;
private final ServerInfo serverInfo;
+ private final AssetVersions assetVersions;
+ private final PluginLogger logger;
@Inject
public MetadataJSONResolver(
@Named("mainCommandName") String mainCommand,
+ PlanFiles files,
PlanConfig config,
Theme theme,
- ServerInfo serverInfo
+ ServerInfo serverInfo,
+ AssetVersions assetVersions,
+ PluginLogger logger
) {
this.mainCommand = mainCommand;
+ this.files = files;
this.config = config;
- // Dagger inject constructor
this.theme = theme;
this.serverInfo = serverInfo;
+ this.assetVersions = assetVersions;
+ this.logger = logger;
}
@GET
@@ -82,7 +97,8 @@ private Response getResponse() {
.put("timeZoneOffsetMinutes", config.getTimeZoneOffsetHours() * 60)
.put("contributors", Contributors.getContributors())
.put("defaultTheme", config.get(DisplaySettings.THEME))
- .put("gmPieColors", theme.getPieColors(ThemeVal.GRAPH_GM_PIE))
+ .put("availableThemes", getAvailableThemes())
+ .put("gmPieColors", theme.getDefaultPieColors(ThemeVal.GRAPH_GM_PIE))
.put("playerHeadImageUrl", config.get(DisplaySettings.PLAYER_HEAD_IMG_URL))
.put("isProxy", serverInfo.getServer().isProxy())
.put("serverName", serverInfo.getServer().getIdentifiableName())
@@ -94,4 +110,25 @@ private Response getResponse() {
.build())
.build();
}
+
+ private List getAvailableThemes() {
+ Set foundThemes = new HashSet<>();
+ try {
+ // Add the themes in the jar
+ foundThemes.addAll(assetVersions.getThemeNames());
+ } catch (IOException e) {
+ logger.warn("Could not read themes from jar: " + e.toString());
+ }
+ try (Stream found = Files.list(files.getThemeDirectory())) {
+ found.filter(file -> file.toFile().getAbsolutePath().endsWith(".json"))
+ .map(file -> file.getFileName().toString())
+ .map(fileName -> StringUtils.split(fileName, '.')[0])
+ .forEach(foundThemes::add);
+ } catch (IOException e) {
+ logger.warn("Could not read web_themes directory: " + e.toString());
+ }
+ List asList = new ArrayList<>(foundThemes);
+ asList.sort(String.CASE_INSENSITIVE_ORDER);
+ return asList;
+ }
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/metadata/StorePreferencesJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/metadata/StorePreferencesJSONResolver.java
index 711ce33080..bcc6a144b4 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/metadata/StorePreferencesJSONResolver.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/metadata/StorePreferencesJSONResolver.java
@@ -20,6 +20,7 @@
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
+import com.djrapitops.plan.delivery.web.resolver.exception.MethodNotAllowedException;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.storage.database.DBSystem;
@@ -32,6 +33,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
@@ -62,18 +64,19 @@ public boolean canAccess(Request request) {
description = "Update user preferences",
responses = {
@ApiResponse(responseCode = "200", description = "Storage was successful"),
- @ApiResponse(responseCode = "400", description = "Not logged in (This endpoint only accepts requests if logged in)"),
@ApiResponse(responseCode = "400", description = "Request body does not match json format of preferences"),
+ @ApiResponse(responseCode = "403", description = "Not logged in (This endpoint only accepts requests if logged in)"),
+ @ApiResponse(responseCode = "405", description = "Not POST request"),
},
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = Preferences.class)))
)
@Override
public Optional resolve(Request request) {
- if (!request.getMethod().equals("POST")) {
- throw new BadRequestException("This endpoint only accepts POST requests.");
+ if (!"POST".equals(request.getMethod())) {
+ throw new MethodNotAllowedException("POST");
}
WebUser user = request.getUser()
- .orElseThrow(() -> new BadRequestException("This endpoint only accepts requests if logged in."));
+ .orElseThrow(() -> new ForbiddenException("This endpoint only accepts requests if logged in."));
String preferencesBody = new String(request.getRequestBody(), StandardCharsets.UTF_8);
try {
Gson gson = new Gson();
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/theme/DeleteThemeJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/theme/DeleteThemeJSONResolver.java
new file mode 100644
index 0000000000..5c5ec925a4
--- /dev/null
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/theme/DeleteThemeJSONResolver.java
@@ -0,0 +1,114 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.webserver.resolver.json.theme;
+
+import com.djrapitops.plan.delivery.domain.auth.WebPermission;
+import com.djrapitops.plan.delivery.web.resolver.MimeType;
+import com.djrapitops.plan.delivery.web.resolver.Resolver;
+import com.djrapitops.plan.delivery.web.resolver.Response;
+import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
+import com.djrapitops.plan.delivery.web.resolver.exception.MethodNotAllowedException;
+import com.djrapitops.plan.delivery.web.resolver.request.Request;
+import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
+import com.djrapitops.plan.delivery.webserver.ResponseFactory;
+import com.djrapitops.plan.storage.file.PlanFiles;
+import com.djrapitops.plan.utilities.dev.Untrusted;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.ExampleObject;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Optional;
+import java.util.regex.Pattern;
+
+/**
+ * Endpoint to store themes from theme editor.
+ *
+ * @author AuroraLS3
+ */
+@Singleton
+@Path("/v1/deleteTheme")
+public class DeleteThemeJSONResolver implements Resolver {
+
+ private static final Pattern themeFilePattern = Pattern.compile("[a-zA-Z0-9-]*");
+
+ private final PlanFiles files;
+ private final ResponseFactory responseFactory;
+
+ @Inject
+ public DeleteThemeJSONResolver(PlanFiles files, ResponseFactory responseFactory) {
+ this.files = files;
+ this.responseFactory = responseFactory;
+ }
+
+ @Override
+ public boolean canAccess(Request request) {
+ WebUser user = request.getUser().orElse(new WebUser(""));
+ return user.hasPermission(WebPermission.MANAGE_THEMES);
+ }
+
+ @POST
+ @Operation(
+ description = "Delete theme json with a name",
+ responses = {
+ @ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON)),
+ @ApiResponse(responseCode = "400", description = "If 'theme' parameter is not specified or invalid"),
+ },
+ parameters = @Parameter(in = ParameterIn.QUERY, name = "theme", description = "Name of the theme, alphanumeric with dashes", examples = {
+ @ExampleObject("default"),
+ @ExampleObject("color-blind")
+ }),
+ requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
+ )
+ @Override
+ public Optional resolve(Request request) {
+ if (!"DELETE".equals(request.getMethod())) {
+ throw new MethodNotAllowedException("DELETE");
+ }
+ @Untrusted Optional theme = request.getQuery().get("theme");
+ if (theme.isEmpty()) {
+ throw new BadRequestException("'theme' parameter is required");
+ }
+ return Optional.of(getResponse(theme.get().toLowerCase()));
+ }
+
+ private Response getResponse(@Untrusted String themeName) {
+ if (!themeFilePattern.matcher(themeName).matches()) {
+ throw new BadRequestException("'theme' parameter was invalid");
+ }
+
+ try {
+ Optional found = files.attemptToFind(files.getThemeDirectory(), themeName + ".json");
+ if (found.isPresent()) {
+ Files.deleteIfExists(found.get().toPath());
+ }
+ return responseFactory.successResponse();
+ } catch (IOException e) {
+ return responseFactory.internalErrorResponse(e, e.getMessage());
+ }
+ }
+}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/theme/SaveThemeJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/theme/SaveThemeJSONResolver.java
new file mode 100644
index 0000000000..8c05916ced
--- /dev/null
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/theme/SaveThemeJSONResolver.java
@@ -0,0 +1,228 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.webserver.resolver.json.theme;
+
+import com.djrapitops.plan.delivery.domain.auth.WebPermission;
+import com.djrapitops.plan.delivery.domain.datatransfer.ThemeDto;
+import com.djrapitops.plan.delivery.web.resolver.MimeType;
+import com.djrapitops.plan.delivery.web.resolver.Resolver;
+import com.djrapitops.plan.delivery.web.resolver.Response;
+import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
+import com.djrapitops.plan.delivery.web.resolver.exception.MethodNotAllowedException;
+import com.djrapitops.plan.delivery.web.resolver.request.Request;
+import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
+import com.djrapitops.plan.delivery.webserver.ResponseFactory;
+import com.djrapitops.plan.storage.file.PlanFiles;
+import com.djrapitops.plan.utilities.dev.Untrusted;
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.ExampleObject;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.regex.Pattern;
+
+/**
+ * Endpoint to store themes from theme editor.
+ *
+ * @author AuroraLS3
+ */
+@Singleton
+@Path("/v1/saveTheme")
+public class SaveThemeJSONResolver implements Resolver {
+
+ private static final Pattern themeFilePattern = Pattern.compile("[a-zA-Z0-9-]*");
+ private static final Pattern colorPattern = Pattern.compile("[a-zA-Z0-9-)(]*");
+
+ private final PlanFiles files;
+ private final ResponseFactory responseFactory;
+ private final Gson gson;
+
+ @Inject
+ public SaveThemeJSONResolver(PlanFiles files, ResponseFactory responseFactory, Gson gson) {
+ this.files = files;
+ this.responseFactory = responseFactory;
+ this.gson = gson;
+ }
+
+ @Override
+ public boolean canAccess(Request request) {
+ WebUser user = request.getUser().orElse(new WebUser(""));
+ return user.hasPermission(WebPermission.MANAGE_THEMES);
+ }
+
+ @POST
+ @Operation(
+ description = "Save theme json with a name",
+ responses = {
+ @ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON)),
+ @ApiResponse(responseCode = "400", description = "If 'theme' parameter is not specified or invalid"),
+ @ApiResponse(responseCode = "400", description = "If request body is invalid")
+ },
+ parameters = @Parameter(in = ParameterIn.QUERY, name = "theme", description = "Name of the theme, alphanumeric with dashes", examples = {
+ @ExampleObject("default"),
+ @ExampleObject("color-blind")
+ }),
+ requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = ThemeDto.class)))
+ )
+ @Override
+ public Optional resolve(Request request) {
+ if (!"POST".equals(request.getMethod())) {
+ throw new MethodNotAllowedException("POST");
+ }
+ @Untrusted Optional theme = request.getQuery().get("theme");
+ if (theme.isEmpty()) {
+ throw new BadRequestException("'theme' parameter is required");
+ }
+ return Optional.of(getResponse(theme.get().toLowerCase(), request));
+ }
+
+ private static boolean isInvalid(ThemeDto result) {
+ return result == null
+ || result.getName() == null || result.getName().isEmpty()
+ || !themeFilePattern.matcher(result.getName()).matches()
+ || result.getColors() == null || result.getColors().isEmpty()
+ || result.getNightColors() == null || result.getNightColors().isEmpty()
+ || result.getUseCases() == null || result.getUseCases().isEmpty()
+ || result.getNightModeUseCases() == null || result.getNightModeUseCases().isEmpty();
+ }
+
+ private Response getResponse(@Untrusted String themeName, Request request) {
+ if (!themeFilePattern.matcher(themeName).matches() || StringUtils.containsAny(themeName, '\n', '\t')) {
+ throw new BadRequestException("'theme' parameter was invalid");
+ }
+ if (themeName.isEmpty()) {
+ throw new BadRequestException("'theme' name can not be empty");
+ }
+ if (themeName.length() > 100) {
+ throw new BadRequestException("'theme' name was too long");
+ }
+
+ @Untrusted byte[] requestBody = request.getRequestBody();
+ if (requestBody == null) throw new BadRequestException("Request body is required");
+ try {
+ @Untrusted ThemeDto result = gson.fromJson(new String(requestBody, StandardCharsets.UTF_8), ThemeDto.class);
+ if (isInvalid(result)) {
+ throw new BadRequestException("Body needs to be a valid theme file");
+ }
+ if (!result.getName().equals(themeName)) {
+ throw new BadRequestException("name in the body must match 'theme' parameter");
+ }
+
+ List issues = new ArrayList<>();
+
+ validateUseCases("", result.getUseCases(), issues);
+ validateUseCases("", result.getNightModeUseCases(), issues);
+
+ if (!issues.isEmpty()) {
+ throw new BadRequestException("Invalid request body: " + issues.toString());
+ }
+
+ java.nio.file.Path themeDirectory = files.getThemeDirectory();
+ java.nio.file.Path themeFile = themeDirectory.resolve(themeName + ".json");
+ if (!themeFile.startsWith(themeDirectory)) {
+ throw new BadRequestException("'theme' parameter was invalid");
+ }
+
+ Files.write(themeFile, gson.toJson(result).getBytes(StandardCharsets.UTF_8), PlanFiles.replaceIfExists());
+
+ request.getQuery().get("originalName")
+ .ifPresent(originalName -> deleteOriginal(themeName, originalName, themeDirectory));
+ return responseFactory.successResponse();
+ } catch (JsonSyntaxException e) {
+ throw new BadRequestException("Request body was invalid json");
+ } catch (IOException e) {
+ return responseFactory.internalErrorResponse(e, e.getMessage());
+ }
+ }
+
+ void deleteOriginal(@Untrusted String themeName, @Untrusted String originalName, java.nio.file.Path themeDirectory) {
+ if (originalName.equals(themeName)) return; // Theme was not renamed.
+
+ // All these checks are against trying to delete other themes than the one they're editing.
+ if (!themeFilePattern.matcher(originalName).matches()) {
+ throw new BadRequestException("'originalTheme' parameter was invalid, theme could have been renamed by another user");
+ }
+ java.nio.file.Path originalThemeFile = themeDirectory.resolve(originalName + ".json");
+ if (!originalThemeFile.startsWith(themeDirectory)) {
+ throw new BadRequestException("'originalTheme' parameter was invalid, theme could have been renamed by another user");
+ }
+ if (!Files.exists(originalThemeFile)) {
+ throw new BadRequestException("'originalTheme' parameter was invalid, theme could have been renamed by another user");
+ }
+ try {
+ ThemeDto original = gson.fromJson(Files.readString(originalThemeFile), ThemeDto.class);
+ if (!original.getName().equals(originalName)) {
+ throw new BadRequestException("'originalTheme' parameter was invalid, doesn't match original file");
+ }
+
+ Files.delete(originalThemeFile);
+ } catch (JsonSyntaxException | IOException e) {
+ throw new BadRequestException("'originalTheme' parameter was invalid, could not parse or delete original file. Delete manually.");
+ }
+ }
+
+ private void validateUseCases(String prefix, Map useCases, List issues) {
+ for (Map.Entry entry : useCases.entrySet()) {
+ String prefixedKey = prefix + entry.getKey();
+ Object value = entry.getValue();
+ try {
+ if (value instanceof String) {
+ if (!StringUtils.startsWith((String) value, "var(--color-")) {
+ issues.add(prefixedKey + " is not a color variable");
+ }
+ if (!StringUtils.endsWith((String) value, ")")) {
+ issues.add(prefixedKey + " is not a color variable");
+ }
+ if (!colorPattern.matcher((String) value).matches()) {
+ issues.add(prefixedKey + " has invalid character");
+ }
+ } else if (value instanceof List) {
+ for (String color : (List) value) {
+ if (!themeFilePattern.matcher(color).matches()) {
+ issues.add(prefixedKey + " has invalid character");
+ }
+ }
+ } else if (value instanceof Map) {
+ validateUseCases(prefixedKey + '.', (Map) value, issues);
+ } else {
+ issues.add(prefixedKey + " had unknown type (Can be object, string or string[])");
+ }
+ } catch (ClassCastException e) {
+ issues.add(prefixedKey + " had invalid type (Can be object, string or string[])");
+ }
+ }
+
+ }
+}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/theme/ThemeJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/theme/ThemeJSONResolver.java
new file mode 100644
index 0000000000..a1f72a04dd
--- /dev/null
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/theme/ThemeJSONResolver.java
@@ -0,0 +1,96 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.webserver.resolver.json.theme;
+
+import com.djrapitops.plan.delivery.domain.datatransfer.ThemeDto;
+import com.djrapitops.plan.delivery.web.resolver.MimeType;
+import com.djrapitops.plan.delivery.web.resolver.NoAuthResolver;
+import com.djrapitops.plan.delivery.web.resolver.Response;
+import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
+import com.djrapitops.plan.delivery.web.resolver.request.Request;
+import com.djrapitops.plan.delivery.webserver.ResponseFactory;
+import com.djrapitops.plan.utilities.dev.Untrusted;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.ExampleObject;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.util.Optional;
+import java.util.regex.Pattern;
+
+/**
+ * Represents /v1/theme endpoint.
+ *
+ * Used to read theme contents, always allowed.
+ *
+ * @author AuroraLS3
+ */
+@Singleton
+@Path("/v1/theme")
+public class ThemeJSONResolver implements NoAuthResolver {
+
+ private static final Pattern themeFilePattern = Pattern.compile("[a-zA-Z0-9-]*");
+
+ private final ResponseFactory responseFactory;
+
+ @Inject
+ public ThemeJSONResolver(ResponseFactory responseFactory) {
+ this.responseFactory = responseFactory;
+ }
+
+ @GET
+ @Operation(
+ description = "Get theme json for a name",
+ responses = {
+ @ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON, schema = @Schema(implementation = ThemeDto.class))),
+ @ApiResponse(responseCode = "400", description = "If 'theme' parameter is not specified or invalid")
+ },
+ parameters = @Parameter(in = ParameterIn.QUERY, name = "theme", description = "Name of the theme, alphanumeric with dashes", examples = {
+ @ExampleObject("default"),
+ @ExampleObject("color-blind")
+ }),
+ requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
+ )
+ @Override
+ public Optional resolve(Request request) {
+ Optional theme = request.getQuery().get("theme");
+ if (theme.isEmpty()) {
+ throw new BadRequestException("'theme' parameter is required");
+ }
+ return Optional.of(getThemeResponse(theme.get(), request));
+ }
+
+ private Response getThemeResponse(@Untrusted String themeName, Request request) {
+ if (themeName.isEmpty()) {
+ throw new BadRequestException("'theme' name can not be empty");
+ }
+ if (themeFilePattern.matcher(themeName).matches() || StringUtils.containsAny(themeName, '\n', '\t')) {
+ return responseFactory.themeResponse(themeName, request);
+ } else {
+ throw new BadRequestException("'theme' parameter was invalid");
+ }
+ }
+}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/webgroup/WebGroupDeleteJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/webgroup/WebGroupDeleteJSONResolver.java
index c6c6384ca6..54ee2aad04 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/webgroup/WebGroupDeleteJSONResolver.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/webgroup/WebGroupDeleteJSONResolver.java
@@ -21,6 +21,7 @@
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
+import com.djrapitops.plan.delivery.web.resolver.exception.MethodNotAllowedException;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieStore;
import com.djrapitops.plan.storage.database.DBSystem;
@@ -78,8 +79,8 @@ public boolean canAccess(Request request) {
)
@Override
public Optional resolve(Request request) {
- if (!request.getMethod().equals("DELETE")) {
- throw new BadRequestException("Endpoint needs to be sent a DELETE request.");
+ if (!"DELETE".equals(request.getMethod())) {
+ throw new MethodNotAllowedException("DELETE");
}
@Untrusted String groupName = request.getQuery().get("group")
.orElseThrow(() -> new BadRequestException("'group' parameter not given."));
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/webgroup/WebGroupSaveJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/webgroup/WebGroupSaveJSONResolver.java
index a35621acca..4e7e17077d 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/webgroup/WebGroupSaveJSONResolver.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/webgroup/WebGroupSaveJSONResolver.java
@@ -22,6 +22,7 @@
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
+import com.djrapitops.plan.delivery.web.resolver.exception.MethodNotAllowedException;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieStore;
import com.djrapitops.plan.storage.database.DBSystem;
@@ -83,8 +84,8 @@ public boolean canAccess(Request request) {
)
@Override
public Optional resolve(Request request) {
- if (!request.getMethod().equals("POST")) {
- throw new BadRequestException("Endpoint needs to be sent a POST request.");
+ if (!"POST".equals(request.getMethod())) {
+ throw new MethodNotAllowedException("POST");
}
String groupName = request.getQuery().get("group")
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/config/Config.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/config/Config.java
index 126b4d2057..917a2e6ebb 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/settings/config/Config.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/config/Config.java
@@ -68,6 +68,10 @@ public Config(File configFile, ConfigNode defaults) {
configFilePath = null;
}
+ public boolean fileExists() {
+ return Files.exists(configFilePath);
+ }
+
public void read() throws IOException {
try (ConfigReader reader = new ConfigReader(Files.newInputStream(configFilePath))) {
copyAll(reader.read());
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/config/paths/DisplaySettings.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/config/paths/DisplaySettings.java
index d06435e780..27c0536bd1 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/settings/config/paths/DisplaySettings.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/config/paths/DisplaySettings.java
@@ -41,6 +41,7 @@ public class DisplaySettings {
public static final Setting CMD_COLOR_MAIN = new StringSetting("Display_options.Command_colors.Main");
public static final Setting CMD_COLOR_SECONDARY = new StringSetting("Display_options.Command_colors.Secondary");
public static final Setting CMD_COLOR_TERTIARY = new StringSetting("Display_options.Command_colors.Highlight");
+ public static final Setting WORLD_PIE = new StringSetting("Display_options.WorldPie");
public static final Setting WORLD_ALIASES = new Setting<>("World_aliases.List", ConfigNode.class) {
@Override
public ConfigNode getValueFrom(ConfigNode node) {
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleFileReader.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleFileReader.java
index 99cb8712b0..7092f88d72 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleFileReader.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleFileReader.java
@@ -50,6 +50,8 @@ public Locale load(LangCode code) throws IOException {
locale.put(msg, new Message(config.getString(key)));
}
});
+
+ LocaleModifications.apply(locale);
return locale;
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleModifications.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleModifications.java
new file mode 100644
index 0000000000..d539d8ec7a
--- /dev/null
+++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleModifications.java
@@ -0,0 +1,73 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.settings.locale;
+
+import com.djrapitops.plan.settings.locale.lang.HtmlLang;
+import com.djrapitops.plan.settings.locale.lang.Lang;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.function.UnaryOperator;
+
+/**
+ * @author AuroraLS3
+ */
+public class LocaleModifications {
+
+ private LocaleModifications() {
+ /* Static utility class */
+ }
+
+ public static void apply(Locale locale) {
+ apply(HtmlLang.QUERY_ARE_PLUGIN_GROUP, locale, new ReplaceString("${group}", "{{group}}"));
+ apply(HtmlLang.QUERY_ARE_PLUGIN_GROUP, locale, new ReplaceString("${plugin}", "{{plugin}}"));
+ apply(HtmlLang.QUERY_RESULTS_MATCH, locale, new ReplaceString("${resultCount}", "{{resultCount}}"));
+ apply(HtmlLang.QUERY, locale, new ReplaceString("<", ""));
+ apply(HtmlLang.QUERY_RESULTS, locale, new ReplaceString("<", ""));
+ apply(HtmlLang.QUERY_TIME_TO, locale, new ReplaceString("", ""));
+ apply(HtmlLang.QUERY_TIME_TO, locale, new ReplaceString(">", ""));
+ apply(HtmlLang.QUERY_TIME_FROM, locale, new ReplaceString("", ""));
+ apply(HtmlLang.QUERY_TIME_FROM, locale, new ReplaceString(">", ""));
+ apply(HtmlLang.QUERY_ACTIVITY_ON, locale, new ReplaceString(" ", "{{activityDate}}"));
+ apply(HtmlLang.TEXT_CONTRIBUTORS_THANKS, locale, "", "<1>");
+ apply(HtmlLang.TEXT_CONTRIBUTORS_THANKS, locale, " ", "1>");
+ apply(HtmlLang.QUERY_SERVERS_MANY, locale, " {number} ", "{{number}}");
+ apply(HtmlLang.HELP_ACTIVITY_INDEX_WEEK, locale, " {}", " {{number}}");
+ }
+
+ private static void apply(Lang appliesTo, Locale locale, String replace, String with) {
+ apply(appliesTo, locale, new ReplaceString(replace, with));
+ }
+
+ private static void apply(Lang appliesTo, Locale locale, UnaryOperator function) {
+ locale.put(appliesTo, new Message(function.apply(locale.get(appliesTo).toString())));
+ }
+
+ static class ReplaceString implements UnaryOperator {
+ private final String replace;
+ private final String with;
+
+ public ReplaceString(String replace, String with) {
+ this.replace = replace;
+ this.with = with;
+ }
+
+ @Override
+ public String apply(String s) {
+ return StringUtils.replace(s, replace, with);
+ }
+ }
+}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleSystem.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleSystem.java
index ca5f60efb1..316abb04d5 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleSystem.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleSystem.java
@@ -35,9 +35,7 @@
import javax.inject.Singleton;
import java.io.File;
import java.io.IOException;
-import java.util.Arrays;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -106,7 +104,6 @@ private static Lang[][] getValuesArray() {
GenericLang.values(),
HelpLang.values(),
HtmlLang.values(),
- JSLang.values(),
PluginLang.values(),
WebPermission.nonDeprecatedValues(),
};
@@ -142,14 +139,31 @@ public void enable() {
}
private void logDefaultKeys(Locale locale) {
+ Set ignoredKeys = new HashSet<>(Arrays.asList(
+ "command.general.webUserList",
+ "command.header.info",
+ "html.label.geoProjection.mercator",
+ "html.label.geoProjection.miller",
+ "html.label.pvpPve",
+ "html.label.afk",
+ "html.label.totalAfk",
+ "html.label.tps",
+ "html.label.kdr"
+ ));
Map keys = getKeys();
+ List untranslatedKeys = new ArrayList<>();
for (Map.Entry entry : keys.entrySet()) {
String key = entry.getKey();
+ if (ignoredKeys.contains(key)) {continue;}
Lang lang = entry.getValue();
if (lang.getDefault().equals(locale.getString(lang))) {
- logger.info("Untranslated line: " + key);
+ untranslatedKeys.add(key);
}
}
+ untranslatedKeys.sort(String.CASE_INSENSITIVE_ORDER);
+ for (String key : untranslatedKeys) {
+ logger.info("Untranslated line: " + key);
+ }
}
public FileWatcher prepareFileWatcher(File localeFile) {
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java
index 92c82c317e..a2e29bc30b 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java
@@ -55,7 +55,7 @@ public enum HtmlLang implements Lang {
LINK_DISCORD("html.modal.info.discord", "General Support on Discord"),
AND_BUG_REPORTERS("html.modal.info.contributors.bugreporters", "& Bug reporters!"),
TEXT_DEVELOPED_BY("html.modal.info.developer", "is developed by"),
- TEXT_CONTRIBUTORS_THANKS("html.modal.info.contributors.text", "In addition following awesome people have contributed:"),
+ TEXT_CONTRIBUTORS_THANKS("html.modal.info.contributors.text", "In addition following <1>awesome people1> have contributed:"),
TEXT_CONTRIBUTORS_CODE("html.modal.info.contributors.code", "code contributor"),
TEXT_CONTRIBUTORS_LOCALE("html.modal.info.contributors.translator", "translator"),
TEXT_CONTRIBUTORS_MONEY("html.modal.info.contributors.donate", "Extra special thanks to those who have monetarily supported the development."),
@@ -136,6 +136,7 @@ public enum HtmlLang implements Lang {
TITLE_CURRENT_PLAYERBASE("html.label.currentPlayerbase", "Current Playerbase"),
TITLE_JOIN_ADDRESSES("html.label.joinAddresses", "Join Addresses"),
TITLE_LATEST_JOIN_ADDRESSES("html.label.latestJoinAddresses", "Latest Join Addresses"),
+ LABEL_SELECT_SOME_ADDRESSES("html.label.selectSomeAddresses", "Select some addresses"),
COMPARING_60_DAYS("html.text.comparing30daysAgo", "Comparing 30d ago to Now"),
TITLE_30_DAYS_AGO("html.label.thirtyDaysAgo", "30 days ago"),
TITLE_NOW("html.label.now", "Now"),
@@ -174,6 +175,7 @@ public enum HtmlLang implements Lang {
LABEL_3RD_WEAPON("html.label.thirdDeadliestWeapon", "3rd PvP Weapon"),
LABEL_AVG_KDR("html.label.averageKdr", "Average KDR"),
LABEL_PLAYER_KILLS("html.label.playerKills", "Player Kills"),
+ LABEL_WEAPON("html.label.weapon", "Weapon"),
LABEL_PLAYER_KILLS_VICTIM_INDICATOR("html.label.playerKillsVictimIndicator", "Player was killed within 24h of first time they were seen (Time since registered: <>)."),
LABEL_AVG_MOB_KDR("html.label.averageMobKdr", "Average Mob KDR"),
LABEL_MOB_KILLS("html.label.mobKills", "Mob Kills"),
@@ -328,15 +330,15 @@ public enum HtmlLang implements Lang {
QUERY_PERFORM_QUERY("html.query.performQuery", "Perform Query!"),
QUERY_LOADING_FILTERS("html.query.filters.loading", "Loading filters.."),
QUERY_ADD_FILTER("html.query.filters.add", "Add a filter.."),
- QUERY_TIME_TO("html.query.label.to", ">to"),
- QUERY_TIME_FROM("html.query.label.from", ">from"),
+ QUERY_TIME_TO("html.query.label.to", "to"),
+ QUERY_TIME_FROM("html.query.label.from", "from"),
QUERY_SHOW_VIEW("html.query.label.view", "Show a view"),
- QUERY("html.query.title.text", "Query<"),
+ QUERY("html.query.title.text", "Query"),
QUERY_MAKE_ANOTHER("html.query.label.makeAnother", "Make another query"),
QUERY_SERVERS_ALL("html.query.label.servers.all", "using data of all servers"),
QUERY_SERVERS_SINGLE("html.query.label.servers.single", "using data of 1 server"),
QUERY_SERVERS_TWO("html.query.label.servers.two", "using data of 2 servers"),
- QUERY_SERVERS_MANY("html.query.label.servers.many", "using data of {number} servers"),
+ QUERY_SERVERS_MANY("html.query.label.servers.many", "using data of {{number}} servers"),
QUERY_SHOW_FULL_QUERY("html.query.label.showFullQuery", "Show Full Query"),
QUERY_EDIT_QUERY("html.query.label.editQuery", "Edit Query"),
@@ -346,7 +348,7 @@ public enum HtmlLang implements Lang {
HELP_RETENTION("html.label.help.retentionBasis", "New player retention is calculated based on session data. If a registered player has played within latter half of the timespan, they are considered retained."),
HELP_ACTIVITY_INDEX("html.label.help.activityIndexBasis", "Activity index is based on non-AFK playtime in the past 3 weeks (21 days). Each week is considered separately."),
HELP_ACTIVITY_INDEX_THRESHOLD("html.label.help.threshold", "Threshold"),
- HELP_ACTIVITY_INDEX_WEEK("html.label.help.activityIndexWeek", "Week {}"),
+ HELP_ACTIVITY_INDEX_WEEK("html.label.help.activityIndexWeek", "Week {{number}}"),
HELP_ACTIVITY_INDEX_THRESHOLD_UNIT("html.label.help.thresholdUnit", "hours / week"),
HELP_ACTIVITY_INDEX_PLAYTIME_UNIT("html.label.help.playtimeUnit", "hours"),
HELP_ACTIVITY_INDEX_EXAMPLE_1("html.label.help.activityIndexExample1", "If someone plays as much as threshold every week, they are given activity index ~3."),
@@ -410,6 +412,8 @@ public enum HtmlLang implements Lang {
RETENTION_LAST_730_DAYS("html.label.retention.inLast730d", "in the last 24 months"),
RETENTION_ANY_TIME("html.label.retention.inAnytime", "any time"),
TIME_SINCE_REGISTERED("html.label.retention.timeSinceRegistered", "Time since register date"),
+ SELECT_NO_OPTIONS("html.label.select.noOptions", "No options available"),
+ SELECT_SELECT("html.label.select.select", "Select.."),
DATE("html.label.time.date", "Date"),
DAY("html.label.time.day", "Day"),
WEEK("html.label.time.week", "Week"),
@@ -442,6 +446,61 @@ public enum HtmlLang implements Lang {
MANAGE_ALERT_SAVE_FAIL("html.label.managePage.alert.saveFail", "Failed to save changes: {{error}}"),
MANAGE_ALERT_SAVE_SUCCESS("html.label.managePage.alert.saveSuccess", "Changes saved successfully!"),
+ THEME_EDITOR_TITLE("html.label.themeEditor.title", "Theme Editor"),
+ THEME_EDITOR_COLORS("html.label.themeEditor.colors", "Colors"),
+ THEME_EDITOR_NIGHT_COLORS("html.label.themeEditor.nightColors", "Night mode"),
+ THEME_EDITOR_THEME_COLOR_OPTIONS("html.label.themeEditor.themeColorOptions", "User theme color options"),
+ THEME_EDITOR_USE_CASES("html.label.themeEditor.useCases", "Use cases"),
+ THEME_EDITOR_NIGHT_MODE_OVERRIDES("html.label.themeEditor.nightModeOverrides", "Night mode overrides"),
+ THEME_EDITOR_EXAMPLE("html.label.themeEditor.example", "Example"),
+ THEME_EDITOR_ADD_COLOR("html.label.themeEditor.addColor", "Add color"),
+ THEME_EDITOR_DELETE_COLORS("html.label.themeEditor.deleteColors", "Delete colors"),
+ THEME_EDITOR_FINISH("html.label.themeEditor.finish", "Finish"),
+ THEME_EDITOR_WARNING_ALREADY_EXISTS("html.label.themeEditor.alreadyExistsWarning", "Color with that name already exists - It will be overridden!"),
+ THEME_EDITOR_WARNING_GRADIENT("html.label.themeEditor.gradientWarning", "Gradients do not work with all elements."),
+ THEME_EDITOR_WARNING_THEME_NAME("html.label.themeEditor.nameWarning", "A valid name that doesn't already exist is needed."),
+ THEME_EDITOR_WARNING_LOCAL("html.label.themeEditor.themeStoredOnlyLocally", "Theme is currently only in Browser local storage (Only you can see it)."),
+ THEME_EDITOR_MISSING("html.label.themeEditor.missing", "Missing color"),
+ THEME_EDITOR_REMOVE_OVERRIDE("html.label.themeEditor.removeOverride", "Remove night mode override"),
+ THEME_EDITOR_NAME("html.label.themeEditor.themeName", "Theme name"),
+ THEME_EDITOR_INVALID_NAME("html.label.themeEditor.invalidName", "Name should be alphanumerical and must be unique. Max 100 characters."),
+ THEME_EDITOR_DEFAULT_NAME("html.label.themeEditor.defaultThemeNameFeedback", "Default theme can not be renamed. You can create a new theme based on Default instead."),
+ THEME_EDITOR_UNSAVED_CHANGES("html.label.themeEditor.unsavedChanges", "There are unsaved changes - do you still want to leave the page?"),
+ THEME_EDITOR_SHOW_HISTORY("html.label.themeEditor.showHistory", "Show history"),
+ THEME_EDITOR_HIDE_HISTORY("html.label.themeEditor.hideHistory", "Hide history"),
+ THEME_EDITOR_UNDO("html.label.themeEditor.undo", "Undo"),
+ THEME_EDITOR_REDO("html.label.themeEditor.redo", "Redo"),
+ THEME_EDITOR_OPEN_EDITOR("html.label.themeEditor.openEditor", "Open editor"),
+ THEME_EDITOR_ADD_THEME("html.label.themeEditor.addTheme", "Add theme"),
+ THEME_EDTIOR_BASED_ON_THEME("html.label.themeEditor.basedOnTheme", "Based on theme"),
+ THEME_EDITOR_UPLOAD_THEME("html.label.themeEditor.uploadTheme", "or Upload a previously downloaded theme:"),
+
+ THEME_EDITOR_DELETE_THEMES("html.label.themeEditor.deleteThemes", "Delete themes"),
+ THEME_EDITOR_CANNOT_DELETE_BUILT_IN("html.label.themeEditor.canNotDeleteBuiltIn", "Note that you can not delete built-in themes, only the modifications you have made to them."),
+ THEME_EDITOR_THEME_TO_DELETE("html.label.themeEditor.themeToDelete", "Theme to delete"),
+ THEME_EDITOR_DOWNLOAD_BEFORE_DELETE("html.label.themeEditor.downloadThemeBeforeDeleting", "Would you like to download the theme '{{theme}}' before deleting it?"),
+ THEME_EDITOR_CONFIRM_DELETE("html.label.themeEditor.confirmDelete", "I confirm that I want to delete theme called '{{theme}}' and that this is an irreversible action."),
+ THEME_EDITOR_DELETE_THEME("html.label.themeEditor.deleteTheme", "Delete theme"),
+ THEME_EDITOR_DELETE_LOCAL("html.label.themeEditor.deleteLocalTheme", "Delete theme (Only the locally stored one)"),
+ THEME_EDITOR_NO_PERMISSION_TO_DELETE("html.label.themeEditor.noPermissionToDelete", "You don't have access rights for deleting non-local themes. You may need to delete them from the plugin folder."),
+ THEME_EDITOR_FAILED_CLONE("html.label.themeEditor.failedToClone", "Failed to clone the original theme {{error}}"),
+ THEME_EDITOR_LIGHT_MODE_INFO("html.label.themeEditor.lightModeInfo", "Theme editor uses light-mode to see fully saturated colors."),
+
+ THEME_EDITOR_CHANGE_RENAME_COLOR("html.label.themeEditor.changes.renameColor", "Renamed {{previous}} to {{name}}, set color to {{color}}"),
+ THEME_EDITOR_CHANGE_SET_COLOR("html.label.themeEditor.changes.setColor", "Set {{name}} color to {{color}}"),
+ THEME_EDITOR_CHANGE_ADD_COLOR("html.label.themeEditor.changes.addColor", "Added {{name}} color {{color}}"),
+ THEME_EDITOR_CHANGE_DELETE_COLOR("html.label.themeEditor.changes.deleteColor", "Deleted color {{name}}"),
+ THEME_EDITOR_CHANGE_USE_CASE("html.label.themeEditor.changes.changeUseCase", "Changed {{path}} to {{name}}"),
+ THEME_EDITOR_CHANGE_USE_CASE_ARRAY("html.label.themeEditor.changes.changeUseCaseArray", "Changed {{path}} list"),
+ THEME_EDITOR_CHANGE_NIGHT_MODE("html.label.themeEditor.changes.changeNightMode", "Changed night mode {{path}} to {{name}}"),
+ THEME_EDITOR_CHANGE_NIGHT_MODE_ARRAY("html.label.themeEditor.changes.changeNightModeArray", "Changed night mode {{path}} list"),
+ THEME_EDITOR_CHANGE_REMOVE_NIGHT_MODE("html.label.themeEditor.changes.removeNightMode", "Removed night mode override {{path}}"),
+ THEME_EDITOR_CHANGE_DISCARDED_CHANGES("html.label.themeEditor.changes.discardedChanges", "Discarded changes:"),
+
+ THEME_EDITOR_ISSUE_PROBLEMS("html.label.themeEditor.issues.problems", "Problems"),
+ THEME_EDITOR_ISSUE_MISSING_USE_CASE_COLOR("html.label.themeEditor.issues.missingUseCase", "Use case {{name}} is missing color {{colorName}}"),
+ THEME_EDITOR_ISSUE_MISSING_NIGHT_MODE_COLOR("html.label.themeEditor.issues.missingNightCase", "Night mode {{name}} is missing color {{colorName}}"),
+
INFO_NO_UPTIME("html.description.noUptimeCalculation", "Server is offline, or has never restarted with Plan installed."),
WARNING_NO_GAME_SERVERS("html.description.noGameServers", "Some data requires Plan to be installed on game servers."),
WARNING_PERFORMANCE_NO_GAME_SERVERS("html.description.performanceNoGameServers", "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop."),
@@ -450,7 +509,51 @@ public enum HtmlLang implements Lang {
WARNING_NO_DATA_24H("html.description.noData24h", "Server has not sent data for over 24 hours."),
WARNING_NO_DATA_7D("html.description.noData7d", "Server has not sent data for over 7 days."),
WARNING_NO_DATA_30D("html.description.noData30d", "Server has not sent data for over 30 days."),
- EXPORTED_TITLE("html.label.exported", "Data export time");
+
+ EXPORTED_TITLE("html.label.exported", "Data export time"),
+ TEXT_PREDICTED_RETENTION("html.description.predictedNewPlayerRetention", "This value is a prediction based on previous players"),
+ TEXT_NO_SERVERS("html.description.noServers", "No servers found in the database"),
+ TEXT_SERVER_INSTRUCTIONS("html.description.noServersLong", "It appears that Plan is not installed on any game servers or not connected to the same database. See wiki for Network tutorial."),
+ TEXT_NO_SERVER("html.description.noServerOnlinActivity", "No server to display online activity for"),
+ LABEL_REGISTERED_PLAYERS("html.label.registeredPlayers", "Registered Players"),
+ LINK_SERVER_ANALYSIS("html.label.serverAnalysis", "Server Analysis"),
+ LINK_QUICK_VIEW("html.label.quickView", "Quick view"),
+ TEXT_FIRST_SESSION("html.label.firstSession", "First session"),
+ LABEL_SESSION_ENDED("html.label.sessionEnded", " Ended"),
+ LINK_PLAYER_PAGE("html.label.playerPage", "Player Page"),
+ LABEL_NO_SESSION_KILLS("html.generic.none", "None"),
+ // UNIT_ENTITIES("html.unit.entities", "Entities"),
+ UNIT_CHUNKS("html.unit.chunks", "Chunks"),
+
+ LABEL_RELATIVE_JOIN_ACTIVITY("html.label.relativeJoinActivity", "Relative Join Activity"),
+ LABEL_DAY_OF_WEEK("html.label.dayOfweek", "Day of the Week"),
+ LABEL_WEEK_DAYS("html.label.weekdays", "'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'"),
+ QUERY_ARE_ACTIVITY_GROUP("html.query.filter.activity.text", "are in Activity Groups"),
+ QUERY_JOINED_WITH_ADDRESS("html.query.filter.joinAddress.text", "joined with address"),
+ QUERY_JOINED_FROM_COUNTRY("html.query.filter.country.text", "have joined from country"),
+ QUERY_ARE_PLUGIN_GROUP("html.query.filter.pluginGroup.text", "are in {{plugin}}'s {{group}} Groups"),
+ QUERY_OF_PLAYERS("html.query.filter.generic.start", "of Players who "),
+ QUERY_AND("html.query.filter.generic.and", "and "),
+ QUERY_PLAYED_BETWEEN("html.query.filter.playedBetween.text", "Played between"),
+ QUERY_REGISTERED_BETWEEN("html.query.filter.registeredBetween.text", "Registered between"),
+ QUERY_ZERO_RESULTS("html.query.results.none", "Query produced 0 results"),
+ QUERY_RESULTS("html.query.results.title", "Query Results"),
+ QUERY_RESULTS_MATCH("html.query.results.match", "matched {{resultCount}} players"),
+ QUERY_VIEW("html.query.filter.view", " View:"),
+ QUERY_ACTIVITY_OF_MATCHED_PLAYERS("html.query.title.activity", "Activity of matched players"),
+ QUERY_ACTIVITY_ON("html.query.title.activityOnDate", "Activity on {{activityDate}}"),
+ QUERY_ARE("html.query.generic.are", "`are`"),
+ QUERY_SESSIONS_WITHIN_VIEW("html.query.title.sessionsWithinView", "Sessions within view"),
+ QUERY_HAS_PLUGIN_BOOLEAN_VALUE("html.query.filter.hasPluginBooleanValue.name", "Has plugin boolean value"),
+ QUERY_HAVE_PLUGIN_BOOLEAN_VALUE("html.query.filter.hasPluginBooleanValue.text", "have Plugin boolean value"),
+ QUERY_HAS_PLAYED_ON_SERVERS("html.query.filter.hasPlayedOnServers.name", "Has played on one of servers"),
+ QUERY_HAVE_PLAYED_ON_SERVERS("html.query.filter.hasPlayedOnServers.text", "have played on at least one of"),
+ FILTER_SKIPPED("html.query.filter.skipped", "Skipped"),
+ FILTER_GROUP("html.query.filter.pluginGroup.name", "Group: "),
+ FILTER_ALL_PLAYERS("html.query.filter.generic.allPlayers", "All players"),
+ FILTER_ACTIVITY_INDEX_NOW("html.query.filter.title.activityGroup", "Current activity group"),
+ FILTER_BANNED("html.query.filter.banStatus.name", "Ban status"),
+ FILTER_OPS("html.query.filter.operatorStatus.name", "Operator status");
private final String key;
private final String defaultValue;
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/JSLang.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/JSLang.java
deleted file mode 100644
index 2ba8337e62..0000000000
--- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/JSLang.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * This file is part of Player Analytics (Plan).
- *
- * Plan is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License v3 as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Plan is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with Plan. If not, see .
- */
-package com.djrapitops.plan.settings.locale.lang;
-
-/**
- * Lang enum for all text included in the javascript files.
- *
- * @author AuroraLS3
- */
-public enum JSLang implements Lang {
-
- TEXT_PREDICTED_RETENTION("html.description.predictedNewPlayerRetention", "This value is a prediction based on previous players"),
- TEXT_NO_SERVERS("html.description.noServers", "No servers found in the database"),
- TEXT_SERVER_INSTRUCTIONS("html.description.noServersLong", "It appears that Plan is not installed on any game servers or not connected to the same database. See wiki for Network tutorial."),
- TEXT_NO_SERVER("html.description.noServerOnlinActivity", "No server to display online activity for"),
- LABEL_REGISTERED_PLAYERS("html.label.registeredPlayers", "Registered Players"),
- LINK_SERVER_ANALYSIS("html.label.serverAnalysis", "Server Analysis"),
- LINK_QUICK_VIEW("html.label.quickView", "Quick view"),
- TEXT_FIRST_SESSION("html.label.firstSession", "First session"),
- LABEL_SESSION_ENDED("html.label.sessionEnded", " Ended"),
- LINK_PLAYER_PAGE("html.label.playerPage", "Player Page"),
- LABEL_NO_SESSION_KILLS("html.generic.none", "None"),
- // UNIT_ENTITIES("html.unit.entities", "Entities"),
- UNIT_CHUNKS("html.unit.chunks", "Chunks"),
- LABEL_RELATIVE_JOIN_ACTIVITY("html.label.relativeJoinActivity", "Relative Join Activity"),
- LABEL_DAY_OF_WEEK("html.label.dayOfweek", "Day of the Week"),
- LABEL_WEEK_DAYS("html.label.weekdays", "'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'"),
-
- QUERY_ARE_ACTIVITY_GROUP("html.query.filter.activity.text", "are in Activity Groups"),
- QUERY_JOINED_WITH_ADDRESS("html.query.filter.joinAddress.text", "joined with address"),
- QUERY_JOINED_FROM_COUNTRY("html.query.filter.country.text", "have joined from country"),
- QUERY_ARE_PLUGIN_GROUP("html.query.filter.pluginGroup.text", "are in ${plugin}'s ${group} Groups"),
- QUERY_OF_PLAYERS("html.query.filter.generic.start", "of Players who "),
- QUERY_AND("html.query.filter.generic.and", "and "),
- QUERY_PLAYED_BETWEEN("html.query.filter.playedBetween.text", "Played between"),
- QUERY_REGISTERED_BETWEEN("html.query.filter.registeredBetween.text", "Registered between"),
- QUERY_ZERO_RESULTS("html.query.results.none", "Query produced 0 results"),
- QUERY_RESULTS("html.query.results.title", "Query Results"),
- QUERY_RESULTS_MATCH("html.query.results.match", "matched ${resultCount} players"),
- QUERY_VIEW("html.query.filter.view", " View:"),
- QUERY_ACTIVITY_OF_MATCHED_PLAYERS("html.query.title.activity", "Activity of matched players"),
- QUERY_ACTIVITY_ON("html.query.title.activityOnDate", "Activity on "),
- QUERY_ARE("html.query.generic.are", "`are`"),
- QUERY_SESSIONS_WITHIN_VIEW("html.query.title.sessionsWithinView", "Sessions within view"),
- QUERY_HAS_PLUGIN_BOOLEAN_VALUE("html.query.filter.hasPluginBooleanValue.name", "Has plugin boolean value"),
- QUERY_HAVE_PLUGIN_BOOLEAN_VALUE("html.query.filter.hasPluginBooleanValue.text", "have Plugin boolean value"),
- QUERY_HAS_PLAYED_ON_SERVERS("html.query.filter.hasPlayedOnServers.name", "Has played on one of servers"),
- QUERY_HAVE_PLAYED_ON_SERVERS("html.query.filter.hasPlayedOnServers.text", "have played on at least one of"),
- FILTER_SKIPPED("html.query.filter.skipped", "Skipped"),
-
- FILTER_GROUP("html.query.filter.pluginGroup.name", "Group: "),
- FILTER_ALL_PLAYERS("html.query.filter.generic.allPlayers", "All players"),
- FILTER_ACTIVITY_INDEX_NOW("html.query.filter.title.activityGroup", "Current activity group"),
- FILTER_BANNED("html.query.filter.banStatus.name", "Ban status"),
- FILTER_OPS("html.query.filter.operatorStatus.name", "Operator status");
-
- private final String key;
- private final String defaultValue;
-
- JSLang(String key, String defaultValue) {
- this.key = key;
- this.defaultValue = defaultValue;
- }
-
- @Override
- public String getIdentifier() {
- return "HTML - " + name();
- }
-
- @Override
- public String getKey() { return key; }
-
- @Override
- public String getDefault() {
- return defaultValue;
- }
-}
\ No newline at end of file
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/theme/Theme.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/theme/Theme.java
index 76f0609d22..92f9a349fd 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/settings/theme/Theme.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/theme/Theme.java
@@ -17,20 +17,20 @@
package com.djrapitops.plan.settings.theme;
import com.djrapitops.plan.SubSystem;
-import com.djrapitops.plan.exceptions.EnableException;
+import com.djrapitops.plan.settings.config.ConfigNode;
import com.djrapitops.plan.settings.config.PlanConfig;
+import com.djrapitops.plan.settings.config.paths.DisplaySettings;
import com.djrapitops.plan.storage.file.PlanFiles;
import net.playeranalytics.plugin.server.PluginLogger;
import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.VisibleForTesting;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
-import java.util.ArrayList;
+import java.nio.file.Files;
import java.util.Arrays;
-import java.util.List;
-
-import static com.djrapitops.plan.settings.theme.ThemeVal.*;
+import java.util.Objects;
/**
* Enum that contains available themes.
@@ -46,8 +46,6 @@ public class Theme implements SubSystem {
private final PlanConfig config;
private final PluginLogger logger;
- private ThemeConfig themeConfig;
-
@Inject
public Theme(PlanFiles files, PlanConfig config, PluginLogger logger) {
this.files = files;
@@ -55,74 +53,53 @@ public Theme(PlanFiles files, PlanConfig config, PluginLogger logger) {
this.logger = logger;
}
- public String getValue(ThemeVal variable) {
- try {
- return getThemeValue(variable);
- } catch (NullPointerException | IllegalStateException e) {
- return variable.getDefaultValue();
- }
+ public String[] getWorldPieColors() {
+ return Arrays.stream(StringUtils.split(config.get(DisplaySettings.WORLD_PIE), ','))
+ .map(color -> StringUtils.remove(StringUtils.trim(color), '"'))
+ .toArray(String[]::new);
}
- public String[] getPieColors(ThemeVal variable) {
- return Arrays.stream(StringUtils.split(getValue(variable), ','))
+ public String[] getDefaultPieColors(ThemeVal val) {
+ return Arrays.stream(StringUtils.split(val.getDefaultValue(), ','))
.map(color -> StringUtils.remove(StringUtils.trim(color), '"'))
.toArray(String[]::new);
}
@Override
public void enable() {
- try {
- themeConfig = new ThemeConfig(files, config, logger);
- themeConfig.save();
- } catch (IOException e) {
- throw new EnableException("theme.yml could not be saved.", e);
- }
- }
-
- @Override
- public void disable() {
- // No need to save theme on disable
- }
+ ThemeConfig themeConfig = new ThemeConfig(files, config, logger);
+ if (themeConfig.fileExists()) {
+ if (themeConfig.contains("GraphColors.WorldPie")) {
+ logger.info("Copied theme.yml 'GraphColors.WorldPie' to config.yml '" + DisplaySettings.WORLD_PIE.getPath() + "'");
+ config.set(DisplaySettings.WORLD_PIE, themeConfig.getString("GraphColors.WorldPie"));
+ }
- private String getColor(ThemeVal variable) {
- String path = variable.getThemePath();
- try {
- return themeConfig.getString(path);
- } catch (Exception | NoSuchFieldError e) {
- logger.error("Something went wrong with getting variable " + variable.name() + " for: " + path);
+ if (containsNonDefaultValues(themeConfig)) {
+ logger.warn("'theme.yml' file has been deprecated in favor of theme-editor on the website. Please delete it manually after noting necessary details (modifications from default were detected.)");
+ } else {
+ try {
+ logger.info("Deleting deprecated 'theme.yml' file automatically since it contains only default values.");
+ Files.deleteIfExists(ThemeConfig.getConfigFile(files).toPath());
+ } catch (IOException e) {
+ logger.warn("'theme.yml' failed to be deleted automatically (" + e.getMessage() + "). Please delete it manually.");
+ }
+ }
}
- return variable.getDefaultValue();
- }
-
- public String replaceThemeColors(String resourceString) {
- return replaceVariables(resourceString,
- RED, PINK, PURPLE,
- DEEP_PURPLE, INDIGO, BLUE, LIGHT_BLUE, CYAN, TEAL, GREEN, LIGHT_GREEN, LIME,
- YELLOW, AMBER, ORANGE, DEEP_ORANGE, BROWN, GREY, BLUE_GREY, BLACK, WHITE,
- GRAPH_PUNCHCARD, GRAPH_PLAYERS_ONLINE, GRAPH_TPS_HIGH, GRAPH_TPS_MED, GRAPH_TPS_LOW,
- GRAPH_CPU, GRAPH_RAM, GRAPH_CHUNKS, GRAPH_ENTITIES, GRAPH_WORLD_PIE, FONT_STYLESHEET, FONT_FAMILY
- );
}
- private String replaceVariables(String resourceString, ThemeVal... themeVariables) {
- List replace = new ArrayList<>();
- List with = new ArrayList<>();
- for (ThemeVal variable : themeVariables) {
- String value = getColor(variable);
- String defaultValue = variable.getDefaultValue();
- if (defaultValue.equals(value)) {
- continue;
+ @VisibleForTesting
+ boolean containsNonDefaultValues(ThemeConfig themeConfig) {
+ ConfigNode defaults = ThemeConfig.getDefaults(files, config, logger);
+ for (ThemeVal value : ThemeVal.values()) {
+ if (!Objects.equals(defaults.getString(value.getThemePath()), themeConfig.getString(value.getThemePath()))) {
+ return true;
}
- replace.add(defaultValue);
- with.add(value);
}
- replace.add("${defaultTheme}");
- with.add(getValue(ThemeVal.THEME_DEFAULT));
-
- return StringUtils.replaceEach(resourceString, replace.toArray(new String[0]), with.toArray(new String[0]));
+ return false;
}
- private String getThemeValue(ThemeVal color) {
- return themeConfig.getString(color.getThemePath());
+ @Override
+ public void disable() {
+ // No need to save theme on disable
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/theme/ThemeConfig.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/theme/ThemeConfig.java
index 043ef54f20..99ec78b646 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/settings/theme/ThemeConfig.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/theme/ThemeConfig.java
@@ -50,7 +50,7 @@ private ThemeConfig(File configFile, ConfigNode defaults) {
}
}
- private static ConfigNode getDefaults(PlanFiles files, PlanConfig config, PluginLogger logger) {
+ public static ConfigNode getDefaults(PlanFiles files, PlanConfig config, PluginLogger logger) {
String fileName = config.get(DisplaySettings.THEME);
String fileLocation = getFileLocation(fileName);
@@ -89,7 +89,7 @@ private static String getFileLocation(String fileName) {
}
}
- private static File getConfigFile(PlanFiles files) {
+ public static File getConfigFile(PlanFiles files) {
return files.getFileFromPluginFolder("theme.yml");
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/theme/ThemeVal.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/theme/ThemeVal.java
index f5e7706bfc..b67f66230d 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/settings/theme/ThemeVal.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/theme/ThemeVal.java
@@ -20,7 +20,9 @@
* Enum class used for getting the Html colors that match the config settings.
*
* @author AuroraLS3
+ * @deprecated theme.yml is deprecated and with it any colors accessible in this enum. Use CSS color variables for use-cases instead, since they are only stable variables on the website.
*/
+@Deprecated(since = "2025-08-13, removal of theme.yml")
public enum ThemeVal {
THEME_DEFAULT("DefaultColor", "plan"),
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/SQLDB.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/SQLDB.java
index 1ce65b13f4..a90bfd39af 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/SQLDB.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/SQLDB.java
@@ -245,6 +245,7 @@ Patch[] patches() {
new LegacyPermissionLevelGroupsPatch(),
new SecurityTableGroupPatch(),
new ExtensionStringValueLengthPatch(),
+ new CookieTableIpAddressPatch()
};
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/SQLiteDB.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/SQLiteDB.java
index 2e760bb764..617b03e53c 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/SQLiteDB.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/SQLiteDB.java
@@ -37,7 +37,6 @@
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
-import java.net.URLConnection;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
@@ -149,16 +148,6 @@ private Connection tryToConnect(String dbFilePath, boolean withWAL) throws SQLEx
return tryToConnect(dbFilePath, false);
} catch (InstantiationException | IllegalAccessException e) {
throw new DBInitException("Failed to initialize SQLite Driver", e);
- } finally {
- new URLConnection(null) {
- @Override
- public void connect() {
- // Hack for fixing a class loading crash (https://github.com/plan-player-analytics/Plan/issues/2202)
- // Caused by https://github.com/xerial/sqlite-jdbc/issues/656
- // Where setDefaultUseCaches is set to false
- // TODO Remove after the underlying issue has been fixed in SQLite
- }
- }.setDefaultUseCaches(true);
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/ActivityIndexFilter.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/ActivityIndexFilter.java
index 434598e148..041af74bdf 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/ActivityIndexFilter.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/ActivityIndexFilter.java
@@ -20,7 +20,6 @@
import com.djrapitops.plan.delivery.domain.mutators.ActivityIndex;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.TimeSettings;
-import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.analysis.NetworkActivityIndexQueries;
import com.djrapitops.plan.storage.database.queries.filter.CompleteSetException;
@@ -38,18 +37,15 @@
public class ActivityIndexFilter extends MultiOptionFilter {
private final PlanConfig config;
- private final Locale locale;
private final DBSystem dbSystem;
@Inject
public ActivityIndexFilter(
PlanConfig config,
- Locale locale,
DBSystem dbSystem
) {
this.dbSystem = dbSystem;
this.config = config;
- this.locale = locale;
}
@Override
@@ -58,7 +54,7 @@ public String getKind() {
}
private String[] getOptionsArray() {
- return ActivityIndex.getGroups(locale);
+ return ActivityIndex.getGroupLocaleKeys();
}
@Override
@@ -85,7 +81,7 @@ public Set getMatchingUserIds(@Untrusted InputFilterDto query) {
Map indexes = dbSystem.getDatabase().query(NetworkActivityIndexQueries.activityIndexForAllPlayers(date, playtimeThreshold));
return indexes.entrySet().stream()
- .filter(entry -> selected.contains(entry.getValue().getGroup(locale)))
+ .filter(entry -> selected.contains(entry.getValue().getGroupLocaleKey()))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/BannedFilter.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/BannedFilter.java
index 803ce4a70d..278eeac1f7 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/BannedFilter.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/BannedFilter.java
@@ -17,7 +17,6 @@
package com.djrapitops.plan.storage.database.queries.filter.filters;
import com.djrapitops.plan.delivery.domain.datatransfer.InputFilterDto;
-import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.FilterLang;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.filter.CompleteSetException;
@@ -32,15 +31,12 @@
public class BannedFilter extends MultiOptionFilter {
private final DBSystem dbSystem;
- private final Locale locale;
@Inject
public BannedFilter(
- DBSystem dbSystem,
- Locale locale
+ DBSystem dbSystem
) {
this.dbSystem = dbSystem;
- this.locale = locale;
}
@Override
@@ -49,7 +45,7 @@ public String getKind() {
}
private String[] getOptionsArray() {
- return new String[]{locale.getString(FilterLang.BANNED), locale.getString(FilterLang.NOT_BANNED)};
+ return new String[]{FilterLang.BANNED.getKey(), FilterLang.NOT_BANNED.getKey()};
}
@Override
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/OperatorsFilter.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/OperatorsFilter.java
index 8c9bf65881..292315e195 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/OperatorsFilter.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/OperatorsFilter.java
@@ -17,7 +17,6 @@
package com.djrapitops.plan.storage.database.queries.filter.filters;
import com.djrapitops.plan.delivery.domain.datatransfer.InputFilterDto;
-import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.FilterLang;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.filter.CompleteSetException;
@@ -32,15 +31,12 @@
public class OperatorsFilter extends MultiOptionFilter {
private final DBSystem dbSystem;
- private final Locale locale;
@Inject
public OperatorsFilter(
- DBSystem dbSystem,
- Locale locale
+ DBSystem dbSystem
) {
this.dbSystem = dbSystem;
- this.locale = locale;
}
@Override
@@ -49,7 +45,7 @@ public String getKind() {
}
private String[] getOptionsArray() {
- return new String[]{locale.getString(FilterLang.OPERATORS), locale.getString(FilterLang.NON_OPERATORS)};
+ return new String[]{FilterLang.OPERATORS.getKey(), FilterLang.NON_OPERATORS.getKey()};
}
@Override
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/WebUserQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/WebUserQueries.java
index c4ef1951b6..cb18b1f907 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/WebUserQueries.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/WebUserQueries.java
@@ -19,6 +19,7 @@
import com.djrapitops.plan.delivery.domain.auth.User;
import com.djrapitops.plan.delivery.domain.datatransfer.preferences.Preferences;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
+import com.djrapitops.plan.delivery.webserver.auth.CookieMetadata;
import com.djrapitops.plan.storage.database.queries.Query;
import com.djrapitops.plan.storage.database.queries.QueryAllStatement;
import com.djrapitops.plan.storage.database.sql.building.Sql;
@@ -119,7 +120,7 @@ public static Query> fetchAllUsers() {
return db -> db.queryList(sql, WebUserQueries::extractUser);
}
- public static Query> fetchActiveCookies() {
+ public static Query> fetchActiveCookies() {
String sql = SELECT +
SecurityTable.USERNAME + ',' +
UsersTable.USER_NAME + ',' +
@@ -127,6 +128,8 @@ public static Query> fetchActiveCookies() {
SecurityTable.SALT_PASSWORD_HASH + ',' +
WebGroupTable.NAME + ',' +
CookieTable.COOKIE + ',' +
+ CookieTable.EXPIRES + ',' +
+ CookieTable.IP_ADDRESS + ',' +
"GROUP_CONCAT(" + WebPermissionTable.PERMISSION + ",',') as user_permissions" +
FROM + CookieTable.TABLE_NAME + " c" +
INNER_JOIN + SecurityTable.TABLE_NAME + " s on c." + CookieTable.WEB_USERNAME + "=s." + SecurityTable.USERNAME +
@@ -141,12 +144,22 @@ public static Query> fetchActiveCookies() {
SecurityTable.LINKED_TO + ',' +
SecurityTable.SALT_PASSWORD_HASH + ',' +
WebGroupTable.NAME + ',' +
- CookieTable.COOKIE;
+ CookieTable.COOKIE + ',' +
+ CookieTable.EXPIRES + ',' +
+ CookieTable.IP_ADDRESS;
- return db -> db.queryMap(sql, (set, byCookie) -> byCookie.put(set.getString(CookieTable.COOKIE), extractUser(set)),
+ return db -> db.queryMap(sql, (set, byCookie) -> byCookie.put(set.getString(CookieTable.COOKIE), extractCookieMetadata(set)),
System.currentTimeMillis());
}
+ private static CookieMetadata extractCookieMetadata(ResultSet set) throws SQLException {
+ return new CookieMetadata(
+ extractUser(set),
+ set.getLong(CookieTable.EXPIRES),
+ set.getString(CookieTable.IP_ADDRESS)
+ );
+ }
+
private static User extractUser(ResultSet set) throws SQLException {
String username = set.getString(SecurityTable.USERNAME);
String linkedTo = set.getString(UsersTable.USER_NAME);
@@ -160,11 +173,6 @@ private static User extractUser(ResultSet set) throws SQLException {
return new User(username, linkedTo != null ? linkedTo : "console", linkedToUUID, passwordHash, permissionGroup, new HashSet<>(permissions));
}
- public static Query> getCookieExpiryTimes() {
- String sql = SELECT + CookieTable.COOKIE + ',' + CookieTable.EXPIRES + FROM + CookieTable.TABLE_NAME;
- return db -> db.queryMap(sql, (set, expiryTimes) -> expiryTimes.put(set.getString(CookieTable.COOKIE), set.getLong(CookieTable.EXPIRES)));
- }
-
public static Query> fetchGroupNames() {
String sql = SELECT + WebGroupTable.NAME + FROM + WebGroupTable.TABLE_NAME;
return db -> db.queryList(sql, row -> row.getString(WebGroupTable.NAME));
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/CookieTable.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/CookieTable.java
index 88b701be79..7fe5ce30a5 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/CookieTable.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/sql/tables/CookieTable.java
@@ -35,12 +35,14 @@ public class CookieTable {
public static final String ID = "id";
public static final String WEB_USERNAME = "web_username";
public static final String COOKIE = "cookie";
+ public static final String IP_ADDRESS = "ip_address";
public static final String EXPIRES = "expires";
public static final String INSERT_STATEMENT = "INSERT INTO " + TABLE_NAME + " (" +
WEB_USERNAME + ',' +
COOKIE + ',' +
- EXPIRES + ") VALUES (?,?,?)";
+ EXPIRES + ',' +
+ IP_ADDRESS + ") VALUES (?,?,?,?)";
public static final String DELETE_BY_COOKIE_STATEMENT = DELETE_FROM + TABLE_NAME +
WHERE + COOKIE + "=?";
@@ -53,7 +55,6 @@ public class CookieTable {
public static final String DELETE_ALL_STATEMENT = DELETE_FROM + TABLE_NAME;
-
private CookieTable() {
/* Static information class */
}
@@ -64,6 +65,7 @@ public static String createTableSQL(DBType dbType) {
.column(WEB_USERNAME, Sql.varchar(100)).notNull()
.column(EXPIRES, Sql.LONG).notNull()
.column(COOKIE, Sql.varchar(64)).notNull()
+ .column(IP_ADDRESS, Sql.varchar(45)) // Max IPv6 text length 45 chars
.toString();
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/events/CookieChangeTransaction.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/events/CookieChangeTransaction.java
index 1b773ad430..c2fcc2b7aa 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/events/CookieChangeTransaction.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/events/CookieChangeTransaction.java
@@ -20,6 +20,7 @@
import com.djrapitops.plan.storage.database.transactions.ExecStatement;
import com.djrapitops.plan.storage.database.transactions.Transaction;
import com.djrapitops.plan.utilities.dev.Untrusted;
+import org.jspecify.annotations.Nullable;
import java.sql.PreparedStatement;
import java.sql.SQLException;
@@ -30,27 +31,30 @@ public class CookieChangeTransaction extends Transaction {
@Untrusted
private final String cookie; // Null if removing
private final Long expires;
+ @Nullable
+ private final String ipAddress;
- private CookieChangeTransaction(String username, @Untrusted String cookie, Long expires) {
+ private CookieChangeTransaction(String username, @Untrusted String cookie, Long expires, @Nullable String ipAddress) {
this.username = username;
this.cookie = cookie;
this.expires = expires;
+ this.ipAddress = ipAddress;
}
- public static CookieChangeTransaction storeCookie(String username, String cookie, long expires) {
- return new CookieChangeTransaction(username, cookie, expires);
+ public static CookieChangeTransaction storeCookie(String username, String cookie, long expires, String ipAddress) {
+ return new CookieChangeTransaction(username, cookie, expires, ipAddress);
}
public static CookieChangeTransaction removeCookieByUser(String username) {
- return new CookieChangeTransaction(username, null, null);
+ return new CookieChangeTransaction(username, null, null, null);
}
public static CookieChangeTransaction removeCookie(@Untrusted String cookie) {
- return new CookieChangeTransaction(null, cookie, null);
+ return new CookieChangeTransaction(null, cookie, null, null);
}
public static CookieChangeTransaction removeAll() {
- return new CookieChangeTransaction(null, null, null);
+ return new CookieChangeTransaction(null, null, null, null);
}
@Override
@@ -97,6 +101,7 @@ public void prepare(PreparedStatement statement) throws SQLException {
statement.setString(1, username);
statement.setString(2, cookie);
statement.setLong(3, expires);
+ statement.setString(4, ipAddress);
}
});
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/CookieTableIpAddressPatch.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/CookieTableIpAddressPatch.java
new file mode 100644
index 0000000000..8b79bd9ecf
--- /dev/null
+++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/CookieTableIpAddressPatch.java
@@ -0,0 +1,38 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.storage.database.transactions.patches;
+
+import com.djrapitops.plan.storage.database.sql.building.Sql;
+import com.djrapitops.plan.storage.database.sql.tables.CookieTable;
+
+/**
+ * Adds ip_address column to plan_cookies.
+ *
+ * @author AuroraLS3
+ */
+public class CookieTableIpAddressPatch extends Patch {
+
+ @Override
+ public boolean hasBeenApplied() {
+ return hasColumn(CookieTable.TABLE_NAME, CookieTable.IP_ADDRESS);
+ }
+
+ @Override
+ protected void applyPatch() {
+ addColumn(CookieTable.TABLE_NAME, CookieTable.IP_ADDRESS + " " + Sql.varchar(45));
+ }
+}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/WebGroupAddMissingAdminGroupPatch.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/WebGroupAddMissingAdminGroupPatch.java
index e7e748785e..1ad117590c 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/WebGroupAddMissingAdminGroupPatch.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/WebGroupAddMissingAdminGroupPatch.java
@@ -41,7 +41,8 @@ protected void applyPatch() {
WebPermission.PAGE,
WebPermission.ACCESS,
WebPermission.MANAGE_GROUPS,
- WebPermission.MANAGE_USERS
+ WebPermission.MANAGE_USERS,
+ WebPermission.MANAGE_THEMES
})
.map(WebPermission::getPermission)
.collect(Collectors.toList()))
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/WebGroupDefaultGroupsPatch.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/WebGroupDefaultGroupsPatch.java
index ef0a3d9ab3..e0a9991944 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/WebGroupDefaultGroupsPatch.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/WebGroupDefaultGroupsPatch.java
@@ -42,7 +42,8 @@ protected void applyPatch() {
WebPermission.PAGE,
WebPermission.ACCESS,
WebPermission.MANAGE_GROUPS,
- WebPermission.MANAGE_USERS
+ WebPermission.MANAGE_USERS,
+ WebPermission.MANAGE_THEMES
})
.map(WebPermission::getPermission)
.collect(Collectors.toList()))
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/file/PlanFiles.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/file/PlanFiles.java
index 6f661cc75e..6aec343348 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/storage/file/PlanFiles.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/file/PlanFiles.java
@@ -31,7 +31,9 @@
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
+import java.nio.file.OpenOption;
import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
import java.util.Optional;
/**
@@ -146,7 +148,7 @@ public Resource getResourceFromPluginFolder(String resourceName) {
}
public Optional attemptToFind(Path dir, @Untrusted String resourceName) {
- if (dir.toFile().exists() && dir.toFile().isDirectory()) {
+ if (Files.exists(dir) && Files.isDirectory(dir)) {
// Path may be absolute due to resolving untrusted path
@Untrusted Path asPath = dir.resolve(resourceName);
if (!asPath.startsWith(dir)) {
@@ -164,4 +166,22 @@ public Optional attemptToFind(Path dir, @Untrusted String resourceName) {
public Path getJSONStorageDirectory() {
return getDataDirectory().resolve("cached_json");
}
+
+ public static OpenOption[] replaceIfExists() {
+ return new OpenOption[]{
+ StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE
+ };
+ }
+
+ public Path getThemeDirectory() {
+ Path themeDirectory = getDataDirectory().resolve("web_themes");
+ if (!Files.exists(themeDirectory)) {
+ try {
+ Files.createDirectories(themeDirectory);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+ return themeDirectory;
+ }
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/utilities/dev/Untrusted.java b/Plan/common/src/main/java/com/djrapitops/plan/utilities/dev/Untrusted.java
index 922b0f7798..10b821e0cb 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/utilities/dev/Untrusted.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/utilities/dev/Untrusted.java
@@ -28,4 +28,6 @@
*/
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD, ElementType.LOCAL_VARIABLE, ElementType.TYPE_PARAMETER, ElementType.TYPE_USE, ElementType.CONSTRUCTOR, ElementType.TYPE})
-public @interface Untrusted {}
+public @interface Untrusted {
+ String reason() default "";
+}
diff --git a/Plan/common/src/main/resources/assets/plan/bungeeconfig.yml b/Plan/common/src/main/resources/assets/plan/bungeeconfig.yml
index f9cde87f9d..7f57e57119 100644
--- a/Plan/common/src/main/resources/assets/plan/bungeeconfig.yml
+++ b/Plan/common/src/main/resources/assets/plan/bungeeconfig.yml
@@ -199,6 +199,7 @@ Display_options:
Main: '&2'
Secondary: '&7'
Highlight: '&f'
+ WorldPie: '"#0099C6", "#66AA00", "#316395", "#994499", "#22AA99", "#AAAA11", "#6633CC", "#E67300", "#329262", "#5574A6"'
# -----------------------------------------------------
Formatting:
Decimal_points: '#.##'
diff --git a/Plan/common/src/main/resources/assets/plan/config.yml b/Plan/common/src/main/resources/assets/plan/config.yml
index 8481dce314..aaaf9e5ce1 100644
--- a/Plan/common/src/main/resources/assets/plan/config.yml
+++ b/Plan/common/src/main/resources/assets/plan/config.yml
@@ -203,6 +203,7 @@ Display_options:
Main: '&2'
Secondary: '&7'
Highlight: '&f'
+ WorldPie: '"#0099C6", "#66AA00", "#316395", "#994499", "#22AA99", "#AAAA11", "#6633CC", "#E67300", "#329262", "#5574A6"'
# -----------------------------------------------------
Formatting:
Decimal_points: '#.##'
diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml
index ffd2fb9c55..05425324da 100644
--- a/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml
+++ b/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml
@@ -564,6 +564,10 @@ html:
timeStep: "时间步长"
secondDeadliestWeapon: "第二致命的 PVP 武器"
seenNicknames: "用过的游戏名称"
+ select:
+ noOptions: "No options available"
+ select: "Select.."
+ selectSomeAddresses: "Select some addresses"
server: "服务器"
serverAnalysis: "服务器分析"
serverAsNumberse: "服务器数据"
@@ -590,6 +594,60 @@ html:
showNofM: "显示{{n}}条目中的{{m}}条"
showPerPage: "每页显示"
visibleColumns: "可见列"
+ themeEditor:
+ addColor: "Add color"
+ addTheme: "Add theme"
+ alreadyExistsWarning: "Color with that name already exists - It will be overridden!"
+ basedOnTheme: "Based on theme"
+ canNotDeleteBuiltIn: "Note that you can not delete built-in themes, only the modifications you have made to them."
+ changes:
+ addColor: "Added {{name}} color {{color}}"
+ changeNightMode: "Changed night mode {{path}} to {{name}}"
+ changeNightModeArray: "Changed night mode {{path}} list"
+ changeUseCase: "Changed {{path}} to {{name}}"
+ changeUseCaseArray: "Changed {{path}} list"
+ deleteColor: "Deleted color {{name}}"
+ discardedChanges: "Discarded changes:"
+ removeNightMode: "Removed night mode override {{path}}"
+ renameColor: "Renamed {{previous}} to {{name}}, set color to {{color}}"
+ setColor: "Set {{name}} color to {{color}}"
+ colors: "Colors"
+ confirmDelete: "I confirm that I want to delete theme called '{{theme}}' and that this is an irreversible action."
+ defaultThemeNameFeedback: "Default theme can not be renamed. You can create a new theme based on Default instead."
+ deleteColors: "Delete colors"
+ deleteLocalTheme: "Delete theme (Only the locally stored one)"
+ deleteTheme: "Delete theme"
+ deleteThemes: "Delete themes"
+ downloadThemeBeforeDeleting: "Would you like to download the theme '{{theme}}' before deleting it?"
+ example: "Example"
+ failedToClone: "Failed to clone the original theme {{error}}"
+ finish: "Finish"
+ gradientWarning: "Gradients do not work with all elements."
+ hideHistory: "Hide history"
+ invalidName: "Name should be alphanumerical and must be unique. Max 100 characters."
+ issues:
+ missingNightCase: "Night mode {{name}} is missing color {{colorName}}"
+ missingUseCase: "Use case {{name}} is missing color {{colorName}}"
+ problems: "Problems"
+ lightModeInfo: "Theme editor uses light-mode to see fully saturated colors."
+ missing: "Missing color"
+ nameWarning: "A valid name that doesn't already exist is needed."
+ nightColors: "Night mode"
+ nightModeOverrides: "Night mode overrides"
+ noPermissionToDelete: "You don't have access rights for deleting non-local themes. You may need to delete them from the plugin folder."
+ openEditor: "Open editor"
+ redo: "Redo"
+ removeOverride: "Remove night mode override"
+ showHistory: "Show history"
+ themeColorOptions: "User theme color options"
+ themeName: "Theme name"
+ themeStoredOnlyLocally: "Theme is currently only in Browser local storage (Only you can see it)."
+ themeToDelete: "Theme to delete"
+ title: "Theme Editor"
+ undo: "Undo"
+ unsavedChanges: "There are unsaved changes - do you still want to leave the page?"
+ uploadTheme: "or Upload a previously downloaded theme:"
+ useCases: "Use cases"
themeSelect: "主题选择"
thirdDeadliestWeapon: "第三致命的 PVP 武器"
thirtyDays: "30 天"
@@ -625,6 +683,7 @@ html:
users: "管理用户"
version: "版本"
veryActive: "非常活跃"
+ weapon: "Weapon"
weekComparison: "每周对比"
weekdays: "'星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'"
world: "世界加载"
@@ -657,7 +716,9 @@ html:
access_query: "允许访问/query和查询结果页面"
access_raw_player_data: "允许访问/player/{uuid}/raw json数据。遵循'access.player'权限。"
access_server: "允许访问所有/server页面"
+ access_theme_editor: "Allows accessing /theme-editor page"
manage_groups: "允许修改群组权限和访问/manage/groups页面"
+ manage_themes: "Allows saving or deleting themes via theme-editor for everyone"
manage_users: "允许修改用户所属群组"
page: "控制页面上可见的内容"
page_network: "查看所有网络页面"
@@ -714,6 +775,14 @@ html:
page_server_overview_players_online_graph: "查看在线玩家图表"
page_server_performance: "查看服务器性能 - 选项卡"
page_server_performance_graphs: "查看服务器性能图表"
+ page_server_performance_graphs_chunks: "See Chunk count data in Performance graphs"
+ page_server_performance_graphs_cpu: "See CPU usage in Performance graphs"
+ page_server_performance_graphs_disk: "See Disk Space usage Performance graphs"
+ page_server_performance_graphs_entities: "See Entity count data in Performance graphs"
+ page_server_performance_graphs_ping: "See Ping data in Performance graphs"
+ page_server_performance_graphs_players_online: "See Players Online data in Performance graphs"
+ page_server_performance_graphs_ram: "See Memory usage in Performance graphs"
+ page_server_performance_graphs_tps: "See TPS data in Performance graphs"
page_server_performance_overview: "查看服务器性能数字"
page_server_player_versus: "查看PvP和PvE - 选项卡"
page_server_player_versus_kill_list: "查看玩家杀死和死亡列表"
@@ -736,7 +805,7 @@ html:
bugreporters: "和其他问题报告者!"
code: "代码贡献者"
donate: "特别感谢那些在经济上支持开发的人们。"
- text: '以下 优秀人物 也做出了贡献:'
+ text: "以下 <1>优秀人物1> 也做出了贡献:"
translator: "翻译者"
developer: "的开发者是"
discord: "一般问题支持:Discord"
@@ -781,7 +850,7 @@ html:
text: "在此期间游玩过"
pluginGroup:
name: "分组:"
- text: "在 ${plugin} 插件的 ${group} 分组中"
+ text: "在 {{plugin}} 插件的 {{group}} 分组中"
registeredBetween:
text: "在此期间注册"
skipped: "已跳过"
@@ -795,26 +864,26 @@ html:
are: "`是`"
label:
editQuery: "修改查询"
- from: ">从 "
+ from: "从 "
makeAnother: "进行另一个查询"
servers:
all: "使用所有服务器的数据"
- many: "使用 {number} 台服务器的数据。"
+ many: "使用{{number}}台服务器的数据。"
single: "使用 1 台服务器的数据"
two: "使用 2 台服务器的数据"
showFullQuery: "显示全部查询"
- to: ">到 "
+ to: "到 "
view: "日期范围"
performQuery: "执行查询!"
results:
- match: "匹配到 ${resultCount} 个玩家"
+ match: "匹配到 {{resultCount}} 个玩家"
none: "查询到 0 个结果"
title: "查询结果"
title:
activity: "匹配玩家的活跃度"
- activityOnDate: '活跃在 '
+ activityOnDate: "活跃在 {{activityDate}}"
sessionsWithinView: "查看范围内的会话"
- text: "查询<"
+ text: "查询"
register:
completion: "注册完成"
completion1: "您现在可以完成用户注册流程。"
diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml
index 2275f6f1c3..c7dc18aa33 100644
--- a/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml
+++ b/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml
@@ -367,7 +367,7 @@ html:
activityIndexExample2: "Velmi aktivní je ~2x prahová hodnota (y ≥ 3,75)."
activityIndexExample3: "Index se neomezeně blíží hodnotě 5."
activityIndexVisual: "Zde je vizualizace křivky, kde y = index aktivity a x = doba hraní za týden / prahová hodnota."
- activityIndexWeek: "Týden {}"
+ activityIndexWeek: "Týden {{number}}"
examples: "Příklady"
graph:
labels: "Skupinu můžete skrýt/zobrazit kliknutím na štítek v dolní části."
@@ -564,6 +564,10 @@ html:
timeStep: "Časový posun"
secondDeadliestWeapon: "2. PvP Zbraň"
seenNicknames: "Viděné přezdívky"
+ select:
+ noOptions: "No options available"
+ select: "Select.."
+ selectSomeAddresses: "Select some addresses"
server: "Server"
serverAnalysis: "Analýza serveru"
serverAsNumberse: "Statistiky serveru"
@@ -590,6 +594,60 @@ html:
showNofM: "Showing {{n}} of {{m}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
+ themeEditor:
+ addColor: "Add color"
+ addTheme: "Add theme"
+ alreadyExistsWarning: "Color with that name already exists - It will be overridden!"
+ basedOnTheme: "Based on theme"
+ canNotDeleteBuiltIn: "Note that you can not delete built-in themes, only the modifications you have made to them."
+ changes:
+ addColor: "Added {{name}} color {{color}}"
+ changeNightMode: "Changed night mode {{path}} to {{name}}"
+ changeNightModeArray: "Changed night mode {{path}} list"
+ changeUseCase: "Changed {{path}} to {{name}}"
+ changeUseCaseArray: "Changed {{path}} list"
+ deleteColor: "Deleted color {{name}}"
+ discardedChanges: "Discarded changes:"
+ removeNightMode: "Removed night mode override {{path}}"
+ renameColor: "Renamed {{previous}} to {{name}}, set color to {{color}}"
+ setColor: "Set {{name}} color to {{color}}"
+ colors: "Colors"
+ confirmDelete: "I confirm that I want to delete theme called '{{theme}}' and that this is an irreversible action."
+ defaultThemeNameFeedback: "Default theme can not be renamed. You can create a new theme based on Default instead."
+ deleteColors: "Delete colors"
+ deleteLocalTheme: "Delete theme (Only the locally stored one)"
+ deleteTheme: "Delete theme"
+ deleteThemes: "Delete themes"
+ downloadThemeBeforeDeleting: "Would you like to download the theme '{{theme}}' before deleting it?"
+ example: "Example"
+ failedToClone: "Failed to clone the original theme {{error}}"
+ finish: "Finish"
+ gradientWarning: "Gradients do not work with all elements."
+ hideHistory: "Hide history"
+ invalidName: "Name should be alphanumerical and must be unique. Max 100 characters."
+ issues:
+ missingNightCase: "Night mode {{name}} is missing color {{colorName}}"
+ missingUseCase: "Use case {{name}} is missing color {{colorName}}"
+ problems: "Problems"
+ lightModeInfo: "Theme editor uses light-mode to see fully saturated colors."
+ missing: "Missing color"
+ nameWarning: "A valid name that doesn't already exist is needed."
+ nightColors: "Night mode"
+ nightModeOverrides: "Night mode overrides"
+ noPermissionToDelete: "You don't have access rights for deleting non-local themes. You may need to delete them from the plugin folder."
+ openEditor: "Open editor"
+ redo: "Redo"
+ removeOverride: "Remove night mode override"
+ showHistory: "Show history"
+ themeColorOptions: "User theme color options"
+ themeName: "Theme name"
+ themeStoredOnlyLocally: "Theme is currently only in Browser local storage (Only you can see it)."
+ themeToDelete: "Theme to delete"
+ title: "Theme Editor"
+ undo: "Undo"
+ unsavedChanges: "There are unsaved changes - do you still want to leave the page?"
+ uploadTheme: "or Upload a previously downloaded theme:"
+ useCases: "Use cases"
themeSelect: "Zvolené téma"
thirdDeadliestWeapon: "3. PvP Zbraň"
thirtyDays: "30 dní"
@@ -625,6 +683,7 @@ html:
users: "Manage Users"
version: "Version"
veryActive: "Velmi aktivní"
+ weapon: "Weapon"
weekComparison: "Týdenní srovnání"
weekdays: "'Pondělí', 'Úterý', 'Středa', 'Čtvrtek', 'Pátek', 'Sobota', 'Neděle'"
world: "Načtení světa"
@@ -657,7 +716,9 @@ html:
access_query: "Allows accessing /query and Query results pages"
access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions."
access_server: "Allows accessing all /server pages"
+ access_theme_editor: "Allows accessing /theme-editor page"
manage_groups: "Allows modifying group permissions & Access to /manage/groups page"
+ manage_themes: "Allows saving or deleting themes via theme-editor for everyone"
manage_users: "Allows modifying what users belong to what group"
page: "Controls what is visible on pages"
page_network: "See all of network page"
@@ -714,6 +775,14 @@ html:
page_server_overview_players_online_graph: "See Players Online graph"
page_server_performance: "See Performance tab"
page_server_performance_graphs: "See Performance graphs"
+ page_server_performance_graphs_chunks: "See Chunk count data in Performance graphs"
+ page_server_performance_graphs_cpu: "See CPU usage in Performance graphs"
+ page_server_performance_graphs_disk: "See Disk Space usage Performance graphs"
+ page_server_performance_graphs_entities: "See Entity count data in Performance graphs"
+ page_server_performance_graphs_ping: "See Ping data in Performance graphs"
+ page_server_performance_graphs_players_online: "See Players Online data in Performance graphs"
+ page_server_performance_graphs_ram: "See Memory usage in Performance graphs"
+ page_server_performance_graphs_tps: "See TPS data in Performance graphs"
page_server_performance_overview: "See Performance numbers"
page_server_player_versus: "See PvP & PvE -tab"
page_server_player_versus_kill_list: "See Player kill and death lists"
@@ -736,7 +805,7 @@ html:
bugreporters: "& Nahlášení bugu!"
code: "přispěvatel kódu"
donate: "Extra speciální poděkování těm, kteří peněžně pomohli vývoji."
- text: 'Nadále následující skvělí lidé , kteří přispěli:'
+ text: "Nadále následující <1>skvělí lidé1>, kteří přispěli:"
translator: "překladač"
developer: "je vyvíjena od"
discord: "Obecná podpora na Discordu"
@@ -781,7 +850,7 @@ html:
text: "Hrál mezi"
pluginGroup:
name: "Skupina: "
- text: "jsou v ${plugin}'s ${group} Skupiny"
+ text: "jsou v {{plugin}}'s {{group}} Skupiny"
registeredBetween:
text: "Registrován mezi"
skipped: "Skipped"
@@ -795,26 +864,26 @@ html:
are: "` jsou`"
label:
editQuery: "Edit Query"
- from: ">od"
+ from: "od"
makeAnother: "Vytvořit další dotaz"
servers:
all: "použití dat všech serverů"
- many: "použít data {number} serverů"
+ many: "použít data{{number}}serverů"
single: "použít data 1 serveru"
two: "použít data 2 serverů"
showFullQuery: "Show Full Query"
- to: ">do"
+ to: "do"
view: "Zobrazit pohled"
performQuery: "Provést dotaz!"
results:
- match: "nalezeno ${resultCount} hráčů"
+ match: "nalezeno {{resultCount}} hráčů"
none: "Nebyla nalezena žádná data."
title: "Výsledky dotazu"
title:
activity: "Aktivita dotyčných hráčů"
- activityOnDate: 'Aktivita '
+ activityOnDate: "Aktivita {{activityDate}}"
sessionsWithinView: "Relace v rámci pohledu"
- text: "Dotaz<"
+ text: "Dotaz"
register:
completion: "Dokončit registraci"
completion1: "Nyní můžete dokončit registraci uživatele."
diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml
index 857f5b0dc7..a589bf212b 100644
--- a/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml
+++ b/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml
@@ -367,7 +367,7 @@ html:
activityIndexExample2: "Very active is ~2x the threshold (y ≥ 3.75)."
activityIndexExample3: "The index approaches 5 indefinitely."
activityIndexVisual: "Here is a visualization of the curve where y = activity index, and x = playtime per week / threshold."
- activityIndexWeek: "Week {}"
+ activityIndexWeek: "Week {{number}}"
examples: "Examples"
graph:
labels: "You can hide/show a group by clicking on the label at the bottom."
@@ -564,6 +564,10 @@ html:
timeStep: "Time step"
secondDeadliestWeapon: "2. PvP Waffe"
seenNicknames: "Registrierte Nicknames"
+ select:
+ noOptions: "No options available"
+ select: "Select.."
+ selectSomeAddresses: "Select some addresses"
server: "Server"
serverAnalysis: "Server Analyse"
serverAsNumberse: "Server als Nummern"
@@ -590,6 +594,60 @@ html:
showNofM: "Showing {{n}} of {{m}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
+ themeEditor:
+ addColor: "Add color"
+ addTheme: "Add theme"
+ alreadyExistsWarning: "Color with that name already exists - It will be overridden!"
+ basedOnTheme: "Based on theme"
+ canNotDeleteBuiltIn: "Note that you can not delete built-in themes, only the modifications you have made to them."
+ changes:
+ addColor: "Added {{name}} color {{color}}"
+ changeNightMode: "Changed night mode {{path}} to {{name}}"
+ changeNightModeArray: "Changed night mode {{path}} list"
+ changeUseCase: "Changed {{path}} to {{name}}"
+ changeUseCaseArray: "Changed {{path}} list"
+ deleteColor: "Deleted color {{name}}"
+ discardedChanges: "Discarded changes:"
+ removeNightMode: "Removed night mode override {{path}}"
+ renameColor: "Renamed {{previous}} to {{name}}, set color to {{color}}"
+ setColor: "Set {{name}} color to {{color}}"
+ colors: "Colors"
+ confirmDelete: "I confirm that I want to delete theme called '{{theme}}' and that this is an irreversible action."
+ defaultThemeNameFeedback: "Default theme can not be renamed. You can create a new theme based on Default instead."
+ deleteColors: "Delete colors"
+ deleteLocalTheme: "Delete theme (Only the locally stored one)"
+ deleteTheme: "Delete theme"
+ deleteThemes: "Delete themes"
+ downloadThemeBeforeDeleting: "Would you like to download the theme '{{theme}}' before deleting it?"
+ example: "Example"
+ failedToClone: "Failed to clone the original theme {{error}}"
+ finish: "Finish"
+ gradientWarning: "Gradients do not work with all elements."
+ hideHistory: "Hide history"
+ invalidName: "Name should be alphanumerical and must be unique. Max 100 characters."
+ issues:
+ missingNightCase: "Night mode {{name}} is missing color {{colorName}}"
+ missingUseCase: "Use case {{name}} is missing color {{colorName}}"
+ problems: "Problems"
+ lightModeInfo: "Theme editor uses light-mode to see fully saturated colors."
+ missing: "Missing color"
+ nameWarning: "A valid name that doesn't already exist is needed."
+ nightColors: "Night mode"
+ nightModeOverrides: "Night mode overrides"
+ noPermissionToDelete: "You don't have access rights for deleting non-local themes. You may need to delete them from the plugin folder."
+ openEditor: "Open editor"
+ redo: "Redo"
+ removeOverride: "Remove night mode override"
+ showHistory: "Show history"
+ themeColorOptions: "User theme color options"
+ themeName: "Theme name"
+ themeStoredOnlyLocally: "Theme is currently only in Browser local storage (Only you can see it)."
+ themeToDelete: "Theme to delete"
+ title: "Theme Editor"
+ undo: "Undo"
+ unsavedChanges: "There are unsaved changes - do you still want to leave the page?"
+ uploadTheme: "or Upload a previously downloaded theme:"
+ useCases: "Use cases"
themeSelect: "Thema ausgewählt"
thirdDeadliestWeapon: "3. PvP Waffe"
thirtyDays: "30 Tage"
@@ -625,6 +683,7 @@ html:
users: "Manage Users"
version: "Version"
veryActive: "Sehr aktiv"
+ weapon: "Weapon"
weekComparison: "Wochenvergleich"
weekdays: "'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'"
world: "World Load"
@@ -657,7 +716,9 @@ html:
access_query: "Allows accessing /query and Query results pages"
access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions."
access_server: "Allows accessing all /server pages"
+ access_theme_editor: "Allows accessing /theme-editor page"
manage_groups: "Allows modifying group permissions & Access to /manage/groups page"
+ manage_themes: "Allows saving or deleting themes via theme-editor for everyone"
manage_users: "Allows modifying what users belong to what group"
page: "Controls what is visible on pages"
page_network: "See all of network page"
@@ -714,6 +775,14 @@ html:
page_server_overview_players_online_graph: "See Players Online graph"
page_server_performance: "See Performance tab"
page_server_performance_graphs: "See Performance graphs"
+ page_server_performance_graphs_chunks: "See Chunk count data in Performance graphs"
+ page_server_performance_graphs_cpu: "See CPU usage in Performance graphs"
+ page_server_performance_graphs_disk: "See Disk Space usage Performance graphs"
+ page_server_performance_graphs_entities: "See Entity count data in Performance graphs"
+ page_server_performance_graphs_ping: "See Ping data in Performance graphs"
+ page_server_performance_graphs_players_online: "See Players Online data in Performance graphs"
+ page_server_performance_graphs_ram: "See Memory usage in Performance graphs"
+ page_server_performance_graphs_tps: "See TPS data in Performance graphs"
page_server_performance_overview: "See Performance numbers"
page_server_player_versus: "See PvP & PvE -tab"
page_server_player_versus_kill_list: "See Player kill and death lists"
@@ -736,7 +805,7 @@ html:
bugreporters: "& Bug reporters!"
code: "Code Mitwirkender"
donate: "Extra Dank an die Leute, die das Projekt finanziell unterstützt haben."
- text: 'Außerdem haben die folgenden tollen Leute mitgewirkt:'
+ text: "Außerdem haben die folgenden <1>tollen Leute1> mitgewirkt:"
translator: "Übersetzer"
developer: "entwickelt von"
discord: "Genereller Support auf Discord"
@@ -781,7 +850,7 @@ html:
text: "Played between"
pluginGroup:
name: "Gruppe: "
- text: "are in ${plugin}'s ${group} Groups"
+ text: "are in {{plugin}}'s {{group}} Groups"
registeredBetween:
text: "Registered between"
skipped: "Skipped"
@@ -795,26 +864,26 @@ html:
are: "`are`"
label:
editQuery: "Edit Query"
- from: ">from"
+ from: "from"
makeAnother: "Make another query"
servers:
all: "using data of all servers"
- many: "using data of {number} servers"
+ many: "using data of{{number}}servers"
single: "using data of 1 server"
two: "using data of 2 servers"
showFullQuery: "Show Full Query"
- to: ">to"
+ to: "to"
view: "Show a view"
performQuery: "Perform Query!"
results:
- match: "matched ${resultCount} players"
+ match: "matched {{resultCount}} players"
none: "Query produced 0 results"
title: "Query Results"
title:
activity: "Activity of matched players"
- activityOnDate: 'Activity on '
+ activityOnDate: "Activity on {{activityDate}}"
sessionsWithinView: "Sessions within view"
- text: "Query<"
+ text: "Query"
register:
completion: "Complete Registration"
completion1: "You can now finish registering the user."
diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml
index eb5ba4e1ce..9219bb6787 100644
--- a/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml
+++ b/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml
@@ -367,7 +367,7 @@ html:
activityIndexExample2: "Very active is ~2x the threshold (y ≥ 3.75)."
activityIndexExample3: "The index approaches 5 indefinitely."
activityIndexVisual: "Here is a visualization of the curve where y = activity index, and x = playtime per week / threshold."
- activityIndexWeek: "Week {}"
+ activityIndexWeek: "Week {{number}}"
examples: "Examples"
graph:
labels: "You can hide/show a group by clicking on the label at the bottom."
@@ -564,6 +564,10 @@ html:
timeStep: "Time step"
secondDeadliestWeapon: "2nd PvP Weapon"
seenNicknames: "Seen Nicknames"
+ select:
+ noOptions: "No options available"
+ select: "Select.."
+ selectSomeAddresses: "Select some addresses"
server: "Server"
serverAnalysis: "Server Analysis"
serverAsNumberse: "Server as Numbers"
@@ -590,6 +594,60 @@ html:
showNofM: "Showing {{n}} of {{m}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
+ themeEditor:
+ addColor: "Add color"
+ addTheme: "Add theme"
+ alreadyExistsWarning: "Color with that name already exists - It will be overridden!"
+ basedOnTheme: "Based on theme"
+ canNotDeleteBuiltIn: "Note that you can not delete built-in themes, only the modifications you have made to them."
+ changes:
+ addColor: "Added {{name}} color {{color}}"
+ changeNightMode: "Changed night mode {{path}} to {{name}}"
+ changeNightModeArray: "Changed night mode {{path}} list"
+ changeUseCase: "Changed {{path}} to {{name}}"
+ changeUseCaseArray: "Changed {{path}} list"
+ deleteColor: "Deleted color {{name}}"
+ discardedChanges: "Discarded changes:"
+ removeNightMode: "Removed night mode override {{path}}"
+ renameColor: "Renamed {{previous}} to {{name}}, set color to {{color}}"
+ setColor: "Set {{name}} color to {{color}}"
+ colors: "Colors"
+ confirmDelete: "I confirm that I want to delete theme called '{{theme}}' and that this is an irreversible action."
+ defaultThemeNameFeedback: "Default theme can not be renamed. You can create a new theme based on Default instead."
+ deleteColors: "Delete colors"
+ deleteLocalTheme: "Delete theme (Only the locally stored one)"
+ deleteTheme: "Delete theme"
+ deleteThemes: "Delete themes"
+ downloadThemeBeforeDeleting: "Would you like to download the theme '{{theme}}' before deleting it?"
+ example: "Example"
+ failedToClone: "Failed to clone the original theme {{error}}"
+ finish: "Finish"
+ gradientWarning: "Gradients do not work with all elements."
+ hideHistory: "Hide history"
+ invalidName: "Name should be alphanumerical and must be unique. Max 100 characters."
+ issues:
+ missingNightCase: "Night mode {{name}} is missing color {{colorName}}"
+ missingUseCase: "Use case {{name}} is missing color {{colorName}}"
+ problems: "Problems"
+ lightModeInfo: "Theme editor uses light-mode to see fully saturated colors."
+ missing: "Missing color"
+ nameWarning: "A valid name that doesn't already exist is needed."
+ nightColors: "Night mode"
+ nightModeOverrides: "Night mode overrides"
+ noPermissionToDelete: "You don't have access rights for deleting non-local themes. You may need to delete them from the plugin folder."
+ openEditor: "Open editor"
+ redo: "Redo"
+ removeOverride: "Remove night mode override"
+ showHistory: "Show history"
+ themeColorOptions: "User theme color options"
+ themeName: "Theme name"
+ themeStoredOnlyLocally: "Theme is currently only in Browser local storage (Only you can see it)."
+ themeToDelete: "Theme to delete"
+ title: "Theme Editor"
+ undo: "Undo"
+ unsavedChanges: "There are unsaved changes - do you still want to leave the page?"
+ uploadTheme: "or Upload a previously downloaded theme:"
+ useCases: "Use cases"
themeSelect: "Theme Select"
thirdDeadliestWeapon: "3rd PvP Weapon"
thirtyDays: "30 days"
@@ -625,6 +683,7 @@ html:
users: "Manage Users"
version: "Version"
veryActive: "Very Active"
+ weapon: "Weapon"
weekComparison: "Week Comparison"
weekdays: "'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'"
world: "World Load"
@@ -657,7 +716,9 @@ html:
access_query: "Allows accessing /query and Query results pages"
access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions."
access_server: "Allows accessing all /server pages"
+ access_theme_editor: "Allows accessing /theme-editor page"
manage_groups: "Allows modifying group permissions & Access to /manage/groups page"
+ manage_themes: "Allows saving or deleting themes via theme-editor for everyone"
manage_users: "Allows modifying what users belong to what group"
page: "Controls what is visible on pages"
page_network: "See all of network page"
@@ -714,6 +775,14 @@ html:
page_server_overview_players_online_graph: "See Players Online graph"
page_server_performance: "See Performance tab"
page_server_performance_graphs: "See Performance graphs"
+ page_server_performance_graphs_chunks: "See Chunk count data in Performance graphs"
+ page_server_performance_graphs_cpu: "See CPU usage in Performance graphs"
+ page_server_performance_graphs_disk: "See Disk Space usage Performance graphs"
+ page_server_performance_graphs_entities: "See Entity count data in Performance graphs"
+ page_server_performance_graphs_ping: "See Ping data in Performance graphs"
+ page_server_performance_graphs_players_online: "See Players Online data in Performance graphs"
+ page_server_performance_graphs_ram: "See Memory usage in Performance graphs"
+ page_server_performance_graphs_tps: "See TPS data in Performance graphs"
page_server_performance_overview: "See Performance numbers"
page_server_player_versus: "See PvP & PvE -tab"
page_server_player_versus_kill_list: "See Player kill and death lists"
@@ -736,7 +805,7 @@ html:
bugreporters: "& Bug reporters!"
code: "code contributor"
donate: "Extra special thanks to those who have monetarily supported the development."
- text: 'In addition following awesome people have contributed:'
+ text: "In addition following <1>awesome people1> have contributed:"
translator: "translator"
developer: "is developed by"
discord: "General Support on Discord"
@@ -781,7 +850,7 @@ html:
text: "Played between"
pluginGroup:
name: "Group: "
- text: "are in ${plugin}'s ${group} Groups"
+ text: "are in {{plugin}}'s {{group}} Groups"
registeredBetween:
text: "Registered between"
skipped: "Skipped"
@@ -795,26 +864,26 @@ html:
are: "`are`"
label:
editQuery: "Edit Query"
- from: ">from"
+ from: "from"
makeAnother: "Make another query"
servers:
all: "using data of all servers"
- many: "using data of {number} servers"
+ many: "using data of{{number}}servers"
single: "using data of 1 server"
two: "using data of 2 servers"
showFullQuery: "Show Full Query"
- to: ">to"
+ to: "to"
view: "Show a view"
performQuery: "Perform Query!"
results:
- match: "matched ${resultCount} players"
+ match: "matched {{resultCount}} players"
none: "Query produced 0 results"
title: "Query Results"
title:
activity: "Activity of matched players"
- activityOnDate: 'Activity on '
+ activityOnDate: "Activity on {{activityDate}}"
sessionsWithinView: "Sessions within view"
- text: "Query<"
+ text: "Query"
register:
completion: "Complete Registration"
completion1: "You can now finish registering the user."
diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml
index 93f656ad24..00639e2689 100644
--- a/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml
+++ b/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml
@@ -367,7 +367,7 @@ html:
activityIndexExample2: "Very active is ~2x the threshold (y ≥ 3.75)."
activityIndexExample3: "The index approaches 5 indefinitely."
activityIndexVisual: "Here is a visualization of the curve where y = activity index, and x = playtime per week / threshold."
- activityIndexWeek: "Week {}"
+ activityIndexWeek: "Week {{number}}"
examples: "Examples"
graph:
labels: "You can hide/show a group by clicking on the label at the bottom."
@@ -564,6 +564,10 @@ html:
timeStep: "Time step"
secondDeadliestWeapon: "2ª arma PvP"
seenNicknames: "Nombres de usuarios vistos"
+ select:
+ noOptions: "No options available"
+ select: "Select.."
+ selectSomeAddresses: "Select some addresses"
server: "Servidor"
serverAnalysis: "Análisis de servidor"
serverAsNumberse: "Servidor en números"
@@ -590,6 +594,60 @@ html:
showNofM: "Showing {{n}} of {{m}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
+ themeEditor:
+ addColor: "Add color"
+ addTheme: "Add theme"
+ alreadyExistsWarning: "Color with that name already exists - It will be overridden!"
+ basedOnTheme: "Based on theme"
+ canNotDeleteBuiltIn: "Note that you can not delete built-in themes, only the modifications you have made to them."
+ changes:
+ addColor: "Added {{name}} color {{color}}"
+ changeNightMode: "Changed night mode {{path}} to {{name}}"
+ changeNightModeArray: "Changed night mode {{path}} list"
+ changeUseCase: "Changed {{path}} to {{name}}"
+ changeUseCaseArray: "Changed {{path}} list"
+ deleteColor: "Deleted color {{name}}"
+ discardedChanges: "Discarded changes:"
+ removeNightMode: "Removed night mode override {{path}}"
+ renameColor: "Renamed {{previous}} to {{name}}, set color to {{color}}"
+ setColor: "Set {{name}} color to {{color}}"
+ colors: "Colors"
+ confirmDelete: "I confirm that I want to delete theme called '{{theme}}' and that this is an irreversible action."
+ defaultThemeNameFeedback: "Default theme can not be renamed. You can create a new theme based on Default instead."
+ deleteColors: "Delete colors"
+ deleteLocalTheme: "Delete theme (Only the locally stored one)"
+ deleteTheme: "Delete theme"
+ deleteThemes: "Delete themes"
+ downloadThemeBeforeDeleting: "Would you like to download the theme '{{theme}}' before deleting it?"
+ example: "Example"
+ failedToClone: "Failed to clone the original theme {{error}}"
+ finish: "Finish"
+ gradientWarning: "Gradients do not work with all elements."
+ hideHistory: "Hide history"
+ invalidName: "Name should be alphanumerical and must be unique. Max 100 characters."
+ issues:
+ missingNightCase: "Night mode {{name}} is missing color {{colorName}}"
+ missingUseCase: "Use case {{name}} is missing color {{colorName}}"
+ problems: "Problems"
+ lightModeInfo: "Theme editor uses light-mode to see fully saturated colors."
+ missing: "Missing color"
+ nameWarning: "A valid name that doesn't already exist is needed."
+ nightColors: "Night mode"
+ nightModeOverrides: "Night mode overrides"
+ noPermissionToDelete: "You don't have access rights for deleting non-local themes. You may need to delete them from the plugin folder."
+ openEditor: "Open editor"
+ redo: "Redo"
+ removeOverride: "Remove night mode override"
+ showHistory: "Show history"
+ themeColorOptions: "User theme color options"
+ themeName: "Theme name"
+ themeStoredOnlyLocally: "Theme is currently only in Browser local storage (Only you can see it)."
+ themeToDelete: "Theme to delete"
+ title: "Theme Editor"
+ undo: "Undo"
+ unsavedChanges: "There are unsaved changes - do you still want to leave the page?"
+ uploadTheme: "or Upload a previously downloaded theme:"
+ useCases: "Use cases"
themeSelect: "Selección de tema"
thirdDeadliestWeapon: "3ª arma PvP"
thirtyDays: "30 días"
@@ -625,6 +683,7 @@ html:
users: "Manage Users"
version: "Version"
veryActive: "Muy activo"
+ weapon: "Weapon"
weekComparison: "Comparación semanal"
weekdays: "'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'"
world: "Carga de mundo"
@@ -657,7 +716,9 @@ html:
access_query: "Allows accessing /query and Query results pages"
access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions."
access_server: "Allows accessing all /server pages"
+ access_theme_editor: "Allows accessing /theme-editor page"
manage_groups: "Allows modifying group permissions & Access to /manage/groups page"
+ manage_themes: "Allows saving or deleting themes via theme-editor for everyone"
manage_users: "Allows modifying what users belong to what group"
page: "Controls what is visible on pages"
page_network: "See all of network page"
@@ -714,6 +775,14 @@ html:
page_server_overview_players_online_graph: "See Players Online graph"
page_server_performance: "See Performance tab"
page_server_performance_graphs: "See Performance graphs"
+ page_server_performance_graphs_chunks: "See Chunk count data in Performance graphs"
+ page_server_performance_graphs_cpu: "See CPU usage in Performance graphs"
+ page_server_performance_graphs_disk: "See Disk Space usage Performance graphs"
+ page_server_performance_graphs_entities: "See Entity count data in Performance graphs"
+ page_server_performance_graphs_ping: "See Ping data in Performance graphs"
+ page_server_performance_graphs_players_online: "See Players Online data in Performance graphs"
+ page_server_performance_graphs_ram: "See Memory usage in Performance graphs"
+ page_server_performance_graphs_tps: "See TPS data in Performance graphs"
page_server_performance_overview: "See Performance numbers"
page_server_player_versus: "See PvP & PvE -tab"
page_server_player_versus_kill_list: "See Player kill and death lists"
@@ -736,7 +805,7 @@ html:
bugreporters: "& Reporteros de Fallos!"
code: "código de contribuidor"
donate: "Gracias especialmente a aquellas personas que ayudaron en el desarrollo económico."
- text: 'Sobre todo las siguientes personas maravillosas que han contribuido:'
+ text: "Sobre todo las siguientes <1>personas maravillosas1> que han contribuido:"
translator: "traductor"
developer: "esta desarrollado por"
discord: "Soporte general en Discord"
@@ -781,7 +850,7 @@ html:
text: "Jugado entre"
pluginGroup:
name: "Grupo: "
- text: "are in ${plugin}'s ${group} Groups"
+ text: "are in {{plugin}}'s {{group}} Groups"
registeredBetween:
text: "Registrados entre"
skipped: "Skipped"
@@ -795,26 +864,26 @@ html:
are: "`are`"
label:
editQuery: "Edit Query"
- from: ">de"
+ from: "de"
makeAnother: "Realiza otra consulta"
servers:
all: "using data of all servers"
- many: "using data of {number} servers"
+ many: "using data of{{number}}servers"
single: "using data of 1 server"
two: "using data of 2 servers"
showFullQuery: "Show Full Query"
- to: ">a"
+ to: "a"
view: "Mostrar una vista"
performQuery: "Perform Query!"
results:
- match: "coincididos ${resultCount} jugadores"
+ match: "coincididos {{resultCount}} jugadores"
none: "La consulta ha entregado 0 resultados"
title: "Resultados de consulta"
title:
activity: "Actividad de los jugadores encontrados"
- activityOnDate: 'Actividad en '
+ activityOnDate: "Actividad en {{activityDate}}"
sessionsWithinView: "Sessions within view"
- text: "Consulta<"
+ text: "Consulta"
register:
completion: "Registro Completado"
completion1: "Ya puedes finalizar el registro del usuario."
diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml
index 88f41758e8..bd9e5d68c7 100644
--- a/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml
+++ b/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml
@@ -257,7 +257,7 @@ html:
noServers: "Palvelimia ei löytynyt tietokannasta"
noServersLong: 'Vaikuttaa että Plan peli-palvelimia ei ole asennettu tai yhdistetty samaan tietokantaan. Katso wikiin lisätietoja varten.'
noSpongeChunks: "Alueiden määrää ei voi laskea Sponge palvelimilla"
- noUptimeCalculation: "Server is offline, or has never restarted with Plan installed."
+ noUptimeCalculation: "Palvelin on poissa päältä, tai ei ole käynnistynyt uudelleen Plan asennettuna"
performanceNoGameServers: "TPS, Entiteetti tai Chunkki tietoja ei kerätä proxy palvelimilta, koska niillä ei ole peli-askel sykliä."
predictedNewPlayerRetention: "Tämä arvo on arvattu ennustus edellisten pelaajien perusteella"
error:
@@ -290,19 +290,19 @@ html:
active: "Aktiivinen"
activePlaytime: "Aktiivinen peliaika"
activityIndex: "Aktiivisuus Indeksi"
- addJoinAddressGroup: "Add address group"
- addressGroup: "Address group {{n}}"
+ addJoinAddressGroup: "Lisää osoitejoukko"
+ addressGroup: "Osoitejoukko {{n}}"
afk: "AFK"
afkTime: "Aika AFK:ina"
all: "Kaikki"
allTime: "Kaikkien aikojen"
- allowed: "Allowed"
- allowlist: "Allowlist"
- allowlistBounces: "Allowlist Bounces"
+ allowed: "Sallittu"
+ allowlist: "Sallimislista"
+ allowlistBounces: "Sallimislistalta Hylätyt"
alphabetical: "Aakkosjärjestys"
apply: "Käytä"
asNumbers: "Numeroina"
- attempts: "Attempts"
+ attempts: "Yritykset"
average: "Keskimäräinen"
averageActivePlaytime: "Keskimäräinen Aktiivinen peliaika"
averageAfkTime: "Keskimäräinen AFK aika"
@@ -323,7 +323,7 @@ html:
banned: "Pannassa"
bestPeak: "Paras Huippu"
bestPing: "Paras Vasteaika"
- blocked: "Blocked"
+ blocked: "Estetty"
calendar: " Kalenteri"
comparing7days: "Verrataan 7 päivää"
connectionInfo: "Yhteyksien tiedot"
@@ -333,7 +333,7 @@ html:
cpuUsage: "Suorittimen käyttö"
currentPlayerbase: "Nykyiset pelaajat"
currentUptime: "Käynnissäoloaika"
- currentlyInstalledPlugins: "Currently Installed Plugins"
+ currentlyInstalledPlugins: "Aktiiviset lisäosat"
dayByDay: "Päivittäinen katsaus"
dayOfweek: "Viikonpäivä"
deadliestWeapon: "Tappavin PvP Ase"
@@ -345,7 +345,7 @@ html:
duringLowTps: "Matalan TPS:n aikana:"
entities: "Entiteetit"
errors: "Plan Virhelokit"
- export: "Export"
+ export: "Vie"
exported: "Tietojen vientiaika"
favoriteServer: "Lempipalvelin"
firstSession: "Ensimmäinen sessio"
@@ -367,7 +367,7 @@ html:
activityIndexExample2: "Hyvin aktiivinen pelaa ~2x kynnysarvon verran (y ≥ 3.75)."
activityIndexExample3: "Indeksi lähestyy arvoa 5 äärettömyyteen asti"
activityIndexVisual: "Alapuolelta löytyy esimerkki käyrästä, missä y = aktiivisuus indeksi, and x = peliaika viikossa / kynnys."
- activityIndexWeek: "Viikko {}"
+ activityIndexWeek: "Viikko {{number}}"
examples: "Esimerkkejä"
graph:
labels: "Voit piilottaa/näyttää ryhmän klikkaamalla nimeä käyrän alapuolella"
@@ -428,7 +428,7 @@ html:
information: "TIETOJA"
insights: "Katsaukset"
insights30days: "Katsauksia 30 päivälle"
- installed: "Installed"
+ installed: "Asennettu"
irregular: "Epäsäännöllinen"
joinAddress: "Liittymisosoite"
joinAddresses: "Liittymisosoitteet"
@@ -437,10 +437,10 @@ html:
last24hours: "Viimeiset 24 tuntia"
last30days: "Viimeiset 30 päivää"
last7days: "Viimeiset 7 päivää"
- lastAllowed: "Last Allowed"
- lastBlocked: "Last Blocked"
+ lastAllowed: "Viimeksi sallittu"
+ lastBlocked: "Viimeksi estetty"
lastConnected: "Viimeisin yhteys"
- lastKnownAttempt: "Last Known Attempt"
+ lastKnownAttempt: "Viimeisin yritys"
lastPeak: "Viimeisin huippu"
lastSeen: "Nähty Viimeksi"
latestJoinAddresses: "Viimeisimmät Liittymisosoitteet"
@@ -483,7 +483,7 @@ html:
mobDeaths: "Otusten aiheuttamat Kuolemat"
mobKdr: "Otus-Tapposuhde"
mobKills: "Tapetut Otukset"
- modified: "Modified"
+ modified: "Muokattu"
mostActiveGamemode: "Aktiivisin pelitila"
mostPlayedWorld: "Eniten pelattu maailma"
name: "Nimi"
@@ -527,8 +527,8 @@ html:
playersOnlineNow: "Pelaajia paikalla (Nyt)"
playersOnlineOverview: "Yhteenveto Paikallaolosta"
playtime: "Peliaika"
- pluginHistory: "Plugin History"
- pluginVersionHistory: "Plugin Version History"
+ pluginHistory: "Lisäosahistoria"
+ pluginVersionHistory: "Lisäosien versiohistoria"
plugins: "Lisäosat"
pluginsOverview: "Lisäosien Yhteenveto"
punchcard: "Reikäkortti"
@@ -564,6 +564,10 @@ html:
timeStep: "Aika askel"
secondDeadliestWeapon: "2. PvP Ase"
seenNicknames: "Nähdyt Lempinimet"
+ select:
+ noOptions: "Ei valittavissa olevia vaihtoehtoja"
+ select: "Valitse.."
+ selectSomeAddresses: "Valitse joitain osoitteita"
server: "Palvelin"
serverAnalysis: "Palvelimen Analyysi"
serverAsNumberse: "Palvelin Numeroina"
@@ -590,6 +594,60 @@ html:
showNofM: "Näytetään {{n}}/{{m}} rivistä"
showPerPage: "Näytä per sivu"
visibleColumns: "Näkyvät sarakkeet"
+ themeEditor:
+ addColor: "Lisää väri"
+ addTheme: "Lisää teema"
+ alreadyExistsWarning: "Tämän niminen väri on jo olemassa - se korvataan!"
+ basedOnTheme: "Perustuen teemaan"
+ canNotDeleteBuiltIn: "Ota huomioon, että et voi poistaa sisäänrakennettuja teemoja, ainoastaan niihin tekemäsi muutokset"
+ changes:
+ addColor: "Lisää {{name}} värillä {{color}}"
+ changeNightMode: "Muuta yö-tilan {{path}} arvoksi {{name}}"
+ changeNightModeArray: "Muuta yö-tilan listaa {{path}}"
+ changeUseCase: "Muuta {{path}} arvoon {{name}}"
+ changeUseCaseArray: "Muuta listaa {{path}}"
+ deleteColor: "Poista väri {{name}}"
+ discardedChanges: "Hylätyt muutokset:"
+ removeNightMode: "Poista yö-tilan {{path}}"
+ renameColor: "Nimeä {{previous}} uudelleen {{name}}, aseta arvoon {{color}}"
+ setColor: "Aseta {{name}} väriksi {{color}}"
+ colors: "Värit"
+ confirmDelete: "Vahvistan että haluan poistaa teeman nimeltä '{{theme}}' ja että tämä on peruuttamatonta."
+ defaultThemeNameFeedback: "Default-teemaa ei voi nimetä uudelleen, voit sen sijaan tehdä uuden teeman Default-teeman pohjalta."
+ deleteColors: "Poista värejä"
+ deleteLocalTheme: "Poista teema (Ainoastaan paikallisesti talletettu kopio)"
+ deleteTheme: "Poista teema"
+ deleteThemes: "Poista teemoja"
+ downloadThemeBeforeDeleting: "Haluaisitko ladata teeman '{{theme}}' ennen sen poistamista?"
+ example: "Esimerkki"
+ failedToClone: "Alkuperäisen teeman kloonaus epäonnistui {{error}}"
+ finish: "Valmis"
+ gradientWarning: "Gradientit eivät toimi tekstielementtien kanssa."
+ hideHistory: "Piilota historia"
+ invalidName: "Nimen tulee sisältää vain kirjaimia ja numeroita. Max 100 merkkiä."
+ issues:
+ missingNightCase: "Yö-tilan {{name}} väri {{colorName}} puuttuu"
+ missingUseCase: "Käyttötapauksen {{name}} väri {{colorName}} puuttuu"
+ problems: "Ongelmaa"
+ lightModeInfo: "Teema editori käyttää päivä tilaa, jotta näyt täysväriset värit"
+ missing: "Väri puuttuu"
+ nameWarning: "Tarvitset kunnollisen nimen, joka ei ole jo käytössä"
+ nightColors: "Yö-tila"
+ nightModeOverrides: "Yö-tila"
+ noPermissionToDelete: "Sinulla ei ole oikeuksia poistaa ei-lokaaleja teemoja"
+ openEditor: "Avaa editori"
+ redo: "Toista"
+ removeOverride: "Poista yö-tilan erikoisväri"
+ showHistory: "Näytä historia"
+ themeColorOptions: "Käyttäjän värivaihtoehdot"
+ themeName: "Teeman nimi"
+ themeStoredOnlyLocally: "Teema on tallennettu Selaimen paikalliseen tallennustilaan (Vain sinä näet sen)"
+ themeToDelete: "Teema joka poistetaan"
+ title: "Teema muokkain"
+ undo: "Peruuta"
+ unsavedChanges: "Sinulla on tallentamattomia muutoksia, haluatko silti poistua?"
+ uploadTheme: "tai lähetä aikaisemmin lataamasi teema:"
+ useCases: "Käyttötapaukset"
themeSelect: "Teemavalikko"
thirdDeadliestWeapon: "3. PvP Ase"
thirtyDays: "30 päivää"
@@ -616,15 +674,16 @@ html:
tps: "TPS"
trend: "Suunta"
trends30days: "Suunnat 30 päivälle"
- uninstalled: "Uninstalled"
+ uninstalled: "Poistettu"
uniquePlayers: "Uniikkeja pelaajia"
uniquePlayers7days: "Uniikkeja pelaajia (7 päivää)"
unit:
percentage: "Prosentti"
playerCount: "Pelaajamäärä"
- users: "Manage Users"
- version: "Version"
+ users: "Hallitse käyttäjiä"
+ version: "Versio"
veryActive: "Todella Aktiivinen"
+ weapon: "Ase"
weekComparison: "Viikkojen vertaus"
weekdays: "'Maanantai', 'Tiistai', 'Keskiviikko', 'Torstai', 'Perjantai', 'Lauantai', 'Sunnuntai'"
world: "Maailmojen Resurssit"
@@ -656,8 +715,10 @@ html:
access_players: "Antaa pääsyn /players sivulle"
access_query: "Antaa pääsyn /query ja kysely tulos sivuille"
access_raw_player_data: "Antaa pääsyn /player/{uuid}/raw json tietoihin. Kunnioittaa 'access.player' oikeuksia."
- access_server: "Antaa pääsyn all /server sivuille"
+ access_server: "Antaa pääsyn kaikille /server sivuille"
+ access_theme_editor: "Antaa pääsyn /theme-editor sivulle"
manage_groups: "Antaa muuttaa ryhmien oikeuksia & Antaa pääsyn /manage/groups sivulle"
+ manage_themes: "Antaa muuttaa ja tallentaa teemoja teema-editorin kautta kaikille"
manage_users: "Antaa muuttaa mitkä käyttäjät kuuluvat mihinkin ryhmään"
page: "Ohjaa mitä milläkin sivulla näkyy"
page_network: "Näkee koko verkosto sivun"
@@ -679,7 +740,7 @@ html:
page_network_playerbase_graphs: "Näkee Pelaajakunnan katsaus kaaviot"
page_network_playerbase_overview: "Näkee Pelaajakunnan katsauksen numeroina"
page_network_players: "Näkee Pelaajalista osion"
- page_network_plugin_history: "See Plugin History across the network"
+ page_network_plugin_history: "Näkee koko verkoston Lisäosahistorian"
page_network_plugins: "Näkee Proxy palvelimen Lisäosat osion"
page_network_retention: "Näkee Pelaajien pysyvyys osion"
page_network_server_list: "Näkee listan palvelimista"
@@ -695,7 +756,7 @@ html:
page_player_sessions: "Näkee Pelaajan Istunnot osion"
page_player_versus: "Näkee PvP & PvE osion"
page_server: "Näkee koko palvelin sivun"
- page_server_allowlist_bounce: "See list of Game allowlist bounces"
+ page_server_allowlist_bounce: "Näkee pelipalvelimen Sallimislistan hylkäykset"
page_server_geolocations: "Näkee Geolokaatio osion"
page_server_geolocations_map: "Näkee Geolokaatio kartan"
page_server_geolocations_ping_per_country: "Näkee Viive per Maa -taulun"
@@ -708,12 +769,20 @@ html:
page_server_online_activity_graphs_day_by_day: "Näkee Päivä Kerrallaan -kaavion"
page_server_online_activity_graphs_hour_by_hour: "Näkee Tunti Kerrallaan -kaavion"
page_server_online_activity_graphs_punchcard: "Näkee Reikäkortti kaavion"
- page_server_online_activity_overview: "Näkee Online Aktiivisuuden numeroina"
+ page_server_online_activity_overview: "Näkee Paikallaolo Aktiivisuuden numeroina"
page_server_overview: "Näkee Palvelimen katsaus osion"
page_server_overview_numbers: "Näkee Palvelimen katsauksen numerot"
page_server_overview_players_online_graph: "Näkee Pelaajia paikalla -kaavion"
page_server_performance: "Näkee Suorituskyky osion"
page_server_performance_graphs: "Näkee Suorituskyky kaaviot"
+ page_server_performance_graphs_chunks: "Näkee Alue lukumäärän Suorituskyky kaavoissa"
+ page_server_performance_graphs_cpu: "Näkee Suorittimenkäytön Suorituskyky kaavoissa"
+ page_server_performance_graphs_disk: "Näkee Levykäytön Suorituskyky kaavoissa"
+ page_server_performance_graphs_entities: "Näkee Olioiden lukumäärän Suorituskyky kaavoissa"
+ page_server_performance_graphs_ping: "Näkee Vasteajan Suorituskyky kaavoissa"
+ page_server_performance_graphs_players_online: "Näkee Paikallaolevat pelaajat Suorituskyky kaavoissa"
+ page_server_performance_graphs_ram: "Näkee Muistinkäytön Suorituskyky kaavoissa"
+ page_server_performance_graphs_tps: "Näkee TPS tiedot Suorituskyky kaavoissa"
page_server_performance_overview: "Näkee Suorituskyky numerot"
page_server_player_versus: "Näkee PvP & PvE osion"
page_server_player_versus_kill_list: "Näkee pelaajien tappo- ja kuolemalistat"
@@ -722,7 +791,7 @@ html:
page_server_playerbase_graphs: "Näkee Pelaajakunnan katsaus kaaviot"
page_server_playerbase_overview: "Näkee Pelaajakunnan katsauksen numeroina"
page_server_players: "Näkee Pelaajalista osion"
- page_server_plugin_history: "See Plugin History"
+ page_server_plugin_history: "Näkee Lisäosahistorian"
page_server_plugins: "Näkee palvelinten Lisäosat osiot"
page_server_retention: "Näkee Pelaajien pysyvyys osion"
page_server_sessions: "Näkee Istunnot osion"
@@ -736,7 +805,7 @@ html:
bugreporters: "& Bugien ilmoittajat!"
code: "koodin tuottaja"
donate: "Suuret kiitokset projektia rahallisesti tukeneille henkilöille."
- text: 'Myös seuraavat mahtavat ihmiset ovat tukeneet kehitystä:'
+ text: "Myös seuraavat <1>mahtavat ihmiset1> ovat tukeneet kehitystä:"
translator: "kääntäjä"
developer: "on kehittänyt"
discord: "Discord-tuki"
@@ -768,20 +837,20 @@ html:
name: "Ovat pelanneet yhdellä palvelimista"
text: "ovat pelanneet ainakin yhdellä palvelimista"
hasPluginBooleanValue:
- name: "On Lisäosan boolean arvo"
- text: "on Lisäosan boolean arvo"
+ name: "Lisäosan boolean-arvo"
+ text: "jolla on Lisäosan boolean-arvo"
joinAddress:
text: "liittyi osoitteella"
- nonOperators: "Ei-operaattorit"
+ nonOperators: "Ei-operaattoreita"
notBanned: "Ei-pannassa"
operatorStatus:
name: "Operaattoristatuksen tila"
- operators: "Operaattorit"
+ operators: "Operaattoreita"
playedBetween:
text: "Pelasivat välillä"
pluginGroup:
name: "Luokka: "
- text: "ovat ${plugin}:n ${group} Luokissa"
+ text: "ovat {{plugin}}:n {{group}} Luokissa"
registeredBetween:
text: "Rekisteröityvät välillä"
skipped: "Ohitettu"
@@ -794,27 +863,27 @@ html:
generic:
are: "`ovat`"
label:
- editQuery: "Edit Query"
- from: ">tästä"
+ editQuery: "Muokkaa kyselyä"
+ from: "tästä"
makeAnother: "Tee toinen kysely"
servers:
all: "käyttäen kaikkien palvelimien tietoja"
- many: "käyttäen {number} palvelimen tietoja"
+ many: "käyttäen{{number}}palvelimen tietoja"
single: "käyttäen yhden palvelimen"
two: "käyttäen 2 palvelimen tietoja"
- showFullQuery: "Show Full Query"
- to: ">tänne"
+ showFullQuery: "Näytä kaikki tiedot"
+ to: "tänne"
view: "Näytä näkymä"
performQuery: "Tee kysely!"
results:
- match: "vastasi ${resultCount} pelaajaa"
+ match: "vastasi {{resultCount}} pelaajaa"
none: "Ei tuloksia"
title: "Kyselyn tulokset"
title:
activity: "Vastaavien pelaajien aktiivisuus"
- activityOnDate: 'Aktiivisuus '
+ activityOnDate: "Aktiivisuus {{activityDate}}"
sessionsWithinView: "Istunnot näkymän sisällä"
- text: "Kysely<"
+ text: "Kysely"
register:
completion: "Viimeistele rekisteröinti"
completion1: "Voit viimeistellä käyttäjän rekisteröinnin."
@@ -835,8 +904,8 @@ html:
success: "Käyttäjä rekisteröitiin onnistuneesti! Voit nyt kirjautua."
usernameTip: "Käyttäjänimi voi olla enintään 50 merkkiä."
text:
- click: "Click for more"
- clickAndDrag: "Click and Drag for more"
+ click: "Klikkaa nähdäksesi lisää"
+ clickAndDrag: "Klikkaa ja vedä nähdäksesi lisää"
clickToExpand: "Klikkaa laajentaaksesi"
comparing15days: "Verrataan 15 päivää"
comparing30daysAgo: "Verrataan 30 päivää sitten nykyhetkeen"
@@ -888,9 +957,9 @@ plugin:
emptyIP: "IP server.properties tiedostossa on tyhjä & Alternative_IP ei ole käytössä. Linkit ovat virheellisiä!"
geoDisabled: "Sijaintien keräys ei ole päällä. (Data.Geolocations: false)"
geoInternetRequired: "Plan Vaatii internetin ensimmäisellä käynnistyskerralla GeoLite2 tietokannan lataamiseen."
- proxyAddress: "Proxy server detected in the database - Proxy Webserver address is '${0}'."
- proxyDisabledWebserver: "Disabling Webserver on this server - You can override this behavior by setting '${0}' to false."
- settingChange: "Note: Set '${0}' to ${1}"
+ proxyAddress: "Proxy-palvelin löytyi tietokannassa - Proxyn verkkopalvelimen osoite on '${0}'."
+ proxyDisabledWebserver: "Sammutetaan tämän palvelimen verkkopalvelin - voit yliajaa tämän toiminnon asettamalla '${0}' arvoksi false."
+ settingChange: "Huomio: asetuksen '${0}' arvoksi asetettiin ${1}"
storeSessions: "Tallennetaan edellisellä sammutuskerralla talteenotettuja istuntoja."
webserverDisabled: "Verkkopalvelinta ei käynnistetty. (WebServer.DisableWebServer: true)"
webserver: "Verkkopalvelin pyörii PORTILLA ${0} ( ${1} )"
diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml
index 08f094bbda..1586efb562 100644
--- a/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml
+++ b/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml
@@ -367,7 +367,7 @@ html:
activityIndexExample2: "Très actif est ~2x le seuil (y ≥ 3,75)."
activityIndexExample3: "L'indice se rapproche indéfiniment de 5."
activityIndexVisual: "Voici une visualisation de la courbe où y = indice d'activité, et x = temps de jeu par semaine / seuil."
- activityIndexWeek: "Semaine {}"
+ activityIndexWeek: "Semaine {{number}}"
examples: "Exemples"
graph:
labels: "Vous pouvez masquer/afficher un groupe en cliquant sur l'étiquette en bas de page."
@@ -564,6 +564,10 @@ html:
timeStep: "Pas de temps"
secondDeadliestWeapon: "2ᵉ Arme de Combat"
seenNicknames: "Surnoms vus"
+ select:
+ noOptions: "No options available"
+ select: "Select.."
+ selectSomeAddresses: "Select some addresses"
server: "Serveur"
serverAnalysis: "Analyse du Serveur"
serverAsNumberse: "Serveur en Chiffres"
@@ -590,6 +594,60 @@ html:
showNofM: "Afficher {{n}} de {{m}} entrées"
showPerPage: "Afficher par page"
visibleColumns: "Colonnes visibles"
+ themeEditor:
+ addColor: "Add color"
+ addTheme: "Add theme"
+ alreadyExistsWarning: "Color with that name already exists - It will be overridden!"
+ basedOnTheme: "Based on theme"
+ canNotDeleteBuiltIn: "Note that you can not delete built-in themes, only the modifications you have made to them."
+ changes:
+ addColor: "Added {{name}} color {{color}}"
+ changeNightMode: "Changed night mode {{path}} to {{name}}"
+ changeNightModeArray: "Changed night mode {{path}} list"
+ changeUseCase: "Changed {{path}} to {{name}}"
+ changeUseCaseArray: "Changed {{path}} list"
+ deleteColor: "Deleted color {{name}}"
+ discardedChanges: "Discarded changes:"
+ removeNightMode: "Removed night mode override {{path}}"
+ renameColor: "Renamed {{previous}} to {{name}}, set color to {{color}}"
+ setColor: "Set {{name}} color to {{color}}"
+ colors: "Colors"
+ confirmDelete: "I confirm that I want to delete theme called '{{theme}}' and that this is an irreversible action."
+ defaultThemeNameFeedback: "Default theme can not be renamed. You can create a new theme based on Default instead."
+ deleteColors: "Delete colors"
+ deleteLocalTheme: "Delete theme (Only the locally stored one)"
+ deleteTheme: "Delete theme"
+ deleteThemes: "Delete themes"
+ downloadThemeBeforeDeleting: "Would you like to download the theme '{{theme}}' before deleting it?"
+ example: "Example"
+ failedToClone: "Failed to clone the original theme {{error}}"
+ finish: "Finish"
+ gradientWarning: "Gradients do not work with all elements."
+ hideHistory: "Hide history"
+ invalidName: "Name should be alphanumerical and must be unique. Max 100 characters."
+ issues:
+ missingNightCase: "Night mode {{name}} is missing color {{colorName}}"
+ missingUseCase: "Use case {{name}} is missing color {{colorName}}"
+ problems: "Problems"
+ lightModeInfo: "Theme editor uses light-mode to see fully saturated colors."
+ missing: "Missing color"
+ nameWarning: "A valid name that doesn't already exist is needed."
+ nightColors: "Night mode"
+ nightModeOverrides: "Night mode overrides"
+ noPermissionToDelete: "You don't have access rights for deleting non-local themes. You may need to delete them from the plugin folder."
+ openEditor: "Open editor"
+ redo: "Redo"
+ removeOverride: "Remove night mode override"
+ showHistory: "Show history"
+ themeColorOptions: "User theme color options"
+ themeName: "Theme name"
+ themeStoredOnlyLocally: "Theme is currently only in Browser local storage (Only you can see it)."
+ themeToDelete: "Theme to delete"
+ title: "Theme Editor"
+ undo: "Undo"
+ unsavedChanges: "There are unsaved changes - do you still want to leave the page?"
+ uploadTheme: "or Upload a previously downloaded theme:"
+ useCases: "Use cases"
themeSelect: "Sélection du Thème"
thirdDeadliestWeapon: "3ᵉ Arme de Combat"
thirtyDays: "30 jours"
@@ -625,6 +683,7 @@ html:
users: "Gérer les utilisateurs"
version: "Version"
veryActive: "Très Actif"
+ weapon: "Weapon"
weekComparison: "Comparaison Hebdomadaire"
weekdays: "'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'"
world: "Charge du Monde"
@@ -657,7 +716,9 @@ html:
access_query: "Permet d'accéder aux pages /query et Query results"
access_raw_player_data: "Permet d'accéder aux données json brutes de /player/{uuid}. Suit les permissions 'access.player'."
access_server: "Permet d'accéder à toutes les pages /server"
+ access_theme_editor: "Allows accessing /theme-editor page"
manage_groups: "Permet de modifier les permissions des groupes et d'accéder à la page /manage/groups"
+ manage_themes: "Allows saving or deleting themes via theme-editor for everyone"
manage_users: "Permet de modifier quels utilisateurs appartiennent à quel groupe"
page: "Contrôle ce qui est visible sur les pages"
page_network: "Voir toute la page du réseau"
@@ -714,6 +775,14 @@ html:
page_server_overview_players_online_graph: "Voir le graphique des joueurs en ligne"
page_server_performance: "Voir l'onglet Performance"
page_server_performance_graphs: "Voir les graphiques de performance"
+ page_server_performance_graphs_chunks: "See Chunk count data in Performance graphs"
+ page_server_performance_graphs_cpu: "See CPU usage in Performance graphs"
+ page_server_performance_graphs_disk: "See Disk Space usage Performance graphs"
+ page_server_performance_graphs_entities: "See Entity count data in Performance graphs"
+ page_server_performance_graphs_ping: "See Ping data in Performance graphs"
+ page_server_performance_graphs_players_online: "See Players Online data in Performance graphs"
+ page_server_performance_graphs_ram: "See Memory usage in Performance graphs"
+ page_server_performance_graphs_tps: "See TPS data in Performance graphs"
page_server_performance_overview: "Voir les chiffres de performance"
page_server_player_versus: "Voir PvP & PvE -tab"
page_server_player_versus_kill_list: "Voir Liste des joueurs tués ou morts"
@@ -736,7 +805,7 @@ html:
bugreporters: "& Rapporteurs de Bugs !"
code: "Contributeurs"
donate: "Un merci spécial à ceux qui ont financièrement soutenu le développement."
- text: 'En outre, ces gens formidables ont contribué :'
+ text: "En outre, ces <1>gens formidables1> ont contribué :"
translator: "Traducteurs"
developer: "est développé par"
discord: "Support général sur Discord"
@@ -781,7 +850,7 @@ html:
text: "Joués entre"
pluginGroup:
name: "Groupe : "
- text: "sont dans le groupe {group} de ${plugin}"
+ text: "sont dans le groupe {group} de {{plugin}}"
registeredBetween:
text: "Enregistrés entre"
skipped: "Sautée"
@@ -795,7 +864,7 @@ html:
are: "`sont`"
label:
editQuery: "Modifier la requête"
- from: ">de"
+ from: "de"
makeAnother: "Faire une autre Requête"
servers:
all: "en utilisant les données de tous les serveurs"
@@ -803,18 +872,18 @@ html:
single: "en utilisant les données d'un serveur"
two: "en utilisant les données de 2 serveurs"
showFullQuery: "Afficher la requête complète"
- to: ">à"
+ to: "à"
view: "Visualiser une vue"
performQuery: "Exécuter la Requête !"
results:
- match: "${resultCount} Joueurs appariés"
+ match: "{{resultCount}} Joueurs appariés"
none: "La Requête n'a produit aucun résultat"
title: "Résultats de la Requête"
title:
activity: "Activité des joueurs appariés"
- activityOnDate: 'Activité sur '
+ activityOnDate: "Activité sur {{activityDate}}"
sessionsWithinView: "Sessions à portée de vue"
- text: "Requête<"
+ text: "Requête"
register:
completion: "Enregistrement complet"
completion1: "Vous pouvez maintenant terminer l'enregistrement de l'utilisateur."
diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml
index 1eee8f8368..dcd5569f09 100644
--- a/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml
+++ b/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml
@@ -367,7 +367,7 @@ html:
activityIndexExample2: "Very active is ~2x the threshold (y ≥ 3.75)."
activityIndexExample3: "The index approaches 5 indefinitely."
activityIndexVisual: "Here is a visualization of the curve where y = activity index, and x = playtime per week / threshold."
- activityIndexWeek: "Week {}"
+ activityIndexWeek: "Week {{number}}"
examples: "Examples"
graph:
labels: "You can hide/show a group by clicking on the label at the bottom."
@@ -564,6 +564,10 @@ html:
timeStep: "Time step"
secondDeadliestWeapon: "2° Arma PvP Preferita"
seenNicknames: "Nick Usati"
+ select:
+ noOptions: "No options available"
+ select: "Select.."
+ selectSomeAddresses: "Select some addresses"
server: "Server"
serverAnalysis: "Analisi Server"
serverAsNumberse: "Statistiche Server"
@@ -590,6 +594,60 @@ html:
showNofM: "Showing {{n}} of {{m}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
+ themeEditor:
+ addColor: "Add color"
+ addTheme: "Add theme"
+ alreadyExistsWarning: "Color with that name already exists - It will be overridden!"
+ basedOnTheme: "Based on theme"
+ canNotDeleteBuiltIn: "Note that you can not delete built-in themes, only the modifications you have made to them."
+ changes:
+ addColor: "Added {{name}} color {{color}}"
+ changeNightMode: "Changed night mode {{path}} to {{name}}"
+ changeNightModeArray: "Changed night mode {{path}} list"
+ changeUseCase: "Changed {{path}} to {{name}}"
+ changeUseCaseArray: "Changed {{path}} list"
+ deleteColor: "Deleted color {{name}}"
+ discardedChanges: "Discarded changes:"
+ removeNightMode: "Removed night mode override {{path}}"
+ renameColor: "Renamed {{previous}} to {{name}}, set color to {{color}}"
+ setColor: "Set {{name}} color to {{color}}"
+ colors: "Colors"
+ confirmDelete: "I confirm that I want to delete theme called '{{theme}}' and that this is an irreversible action."
+ defaultThemeNameFeedback: "Default theme can not be renamed. You can create a new theme based on Default instead."
+ deleteColors: "Delete colors"
+ deleteLocalTheme: "Delete theme (Only the locally stored one)"
+ deleteTheme: "Delete theme"
+ deleteThemes: "Delete themes"
+ downloadThemeBeforeDeleting: "Would you like to download the theme '{{theme}}' before deleting it?"
+ example: "Example"
+ failedToClone: "Failed to clone the original theme {{error}}"
+ finish: "Finish"
+ gradientWarning: "Gradients do not work with all elements."
+ hideHistory: "Hide history"
+ invalidName: "Name should be alphanumerical and must be unique. Max 100 characters."
+ issues:
+ missingNightCase: "Night mode {{name}} is missing color {{colorName}}"
+ missingUseCase: "Use case {{name}} is missing color {{colorName}}"
+ problems: "Problems"
+ lightModeInfo: "Theme editor uses light-mode to see fully saturated colors."
+ missing: "Missing color"
+ nameWarning: "A valid name that doesn't already exist is needed."
+ nightColors: "Night mode"
+ nightModeOverrides: "Night mode overrides"
+ noPermissionToDelete: "You don't have access rights for deleting non-local themes. You may need to delete them from the plugin folder."
+ openEditor: "Open editor"
+ redo: "Redo"
+ removeOverride: "Remove night mode override"
+ showHistory: "Show history"
+ themeColorOptions: "User theme color options"
+ themeName: "Theme name"
+ themeStoredOnlyLocally: "Theme is currently only in Browser local storage (Only you can see it)."
+ themeToDelete: "Theme to delete"
+ title: "Theme Editor"
+ undo: "Undo"
+ unsavedChanges: "There are unsaved changes - do you still want to leave the page?"
+ uploadTheme: "or Upload a previously downloaded theme:"
+ useCases: "Use cases"
themeSelect: "Selezione Tema"
thirdDeadliestWeapon: "3° Arma PvP Preferita"
thirtyDays: "30 giorni"
@@ -625,6 +683,7 @@ html:
users: "Manage Users"
version: "Version"
veryActive: "Molto Attivo"
+ weapon: "Weapon"
weekComparison: "Confronto settimanale"
weekdays: "'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato', 'Domenica'"
world: "Caricamento Mondo"
@@ -657,7 +716,9 @@ html:
access_query: "Allows accessing /query and Query results pages"
access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions."
access_server: "Allows accessing all /server pages"
+ access_theme_editor: "Allows accessing /theme-editor page"
manage_groups: "Allows modifying group permissions & Access to /manage/groups page"
+ manage_themes: "Allows saving or deleting themes via theme-editor for everyone"
manage_users: "Allows modifying what users belong to what group"
page: "Controls what is visible on pages"
page_network: "See all of network page"
@@ -714,6 +775,14 @@ html:
page_server_overview_players_online_graph: "See Players Online graph"
page_server_performance: "See Performance tab"
page_server_performance_graphs: "See Performance graphs"
+ page_server_performance_graphs_chunks: "See Chunk count data in Performance graphs"
+ page_server_performance_graphs_cpu: "See CPU usage in Performance graphs"
+ page_server_performance_graphs_disk: "See Disk Space usage Performance graphs"
+ page_server_performance_graphs_entities: "See Entity count data in Performance graphs"
+ page_server_performance_graphs_ping: "See Ping data in Performance graphs"
+ page_server_performance_graphs_players_online: "See Players Online data in Performance graphs"
+ page_server_performance_graphs_ram: "See Memory usage in Performance graphs"
+ page_server_performance_graphs_tps: "See TPS data in Performance graphs"
page_server_performance_overview: "See Performance numbers"
page_server_player_versus: "See PvP & PvE -tab"
page_server_player_versus_kill_list: "See Player kill and death lists"
@@ -736,7 +805,7 @@ html:
bugreporters: "& Bug reporters!"
code: "contributori codice"
donate: "Un ringraziamento speciale a coloro che hanno sostenuto monetariamente lo sviluppo."
- text: 'Inoltre, hanno contribuito queste fantastiche persone :'
+ text: "Inoltre, hanno contribuito queste <1>fantastiche persone1>:"
translator: "traduttore"
developer: "è stato svillupato da"
discord: "Supporto generale su Discord"
@@ -781,7 +850,7 @@ html:
text: "Played between"
pluginGroup:
name: "Group: "
- text: "are in ${plugin}'s ${group} Groups"
+ text: "are in {{plugin}}'s {{group}} Groups"
registeredBetween:
text: "Registered between"
skipped: "Skipped"
@@ -795,26 +864,26 @@ html:
are: "`are`"
label:
editQuery: "Edit Query"
- from: ">from"
+ from: "from"
makeAnother: "Make another query"
servers:
all: "using data of all servers"
- many: "using data of {number} servers"
+ many: "using data of{{number}}servers"
single: "using data of 1 server"
two: "using data of 2 servers"
showFullQuery: "Show Full Query"
- to: ">to"
+ to: "to"
view: "Show a view"
performQuery: "Perform Query!"
results:
- match: "matched ${resultCount} players"
+ match: "matched {{resultCount}} players"
none: "Query produced 0 results"
title: "Query Results"
title:
activity: "Activity of matched players"
- activityOnDate: 'Activity on '
+ activityOnDate: "Activity on {{activityDate}}"
sessionsWithinView: "Sessions within view"
- text: "Query<"
+ text: "Query"
register:
completion: "Complete Registration"
completion1: "You can now finish registering the user."
diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml
index 35f5e66a95..68b5aadfff 100644
--- a/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml
+++ b/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml
@@ -367,7 +367,7 @@ html:
activityIndexExample2: "非常にアクティブとは、しきい値の約 2 倍です (y ≥ 3.75)"
activityIndexExample3: "指数は限りなく 5 に近づきます"
activityIndexVisual: "これは、y = アクティビティ指数、x = 週あたりのプレイ時間 / しきい値である曲線を視覚化したものです"
- activityIndexWeek: "週 {}"
+ activityIndexWeek: "週 {{number}}"
examples: "例"
graph:
labels: "下部のラベルをクリックすることで、グループの表示/非表示を切り替えられます"
@@ -564,6 +564,10 @@ html:
timeStep: "時間幅"
secondDeadliestWeapon: "2番目にPvPで使用されている武器"
seenNicknames: "ニックネーム一覧"
+ select:
+ noOptions: "No options available"
+ select: "Select.."
+ selectSomeAddresses: "Select some addresses"
server: "サーバー"
serverAnalysis: "サーバーの分析結果"
serverAsNumberse: "サーバーの状況"
@@ -590,6 +594,60 @@ html:
showNofM: "{{m}} 件中の {{n}} を表示"
showPerPage: "ページあたりの表示"
visibleColumns: "表示する項目"
+ themeEditor:
+ addColor: "Add color"
+ addTheme: "Add theme"
+ alreadyExistsWarning: "Color with that name already exists - It will be overridden!"
+ basedOnTheme: "Based on theme"
+ canNotDeleteBuiltIn: "Note that you can not delete built-in themes, only the modifications you have made to them."
+ changes:
+ addColor: "Added {{name}} color {{color}}"
+ changeNightMode: "Changed night mode {{path}} to {{name}}"
+ changeNightModeArray: "Changed night mode {{path}} list"
+ changeUseCase: "Changed {{path}} to {{name}}"
+ changeUseCaseArray: "Changed {{path}} list"
+ deleteColor: "Deleted color {{name}}"
+ discardedChanges: "Discarded changes:"
+ removeNightMode: "Removed night mode override {{path}}"
+ renameColor: "Renamed {{previous}} to {{name}}, set color to {{color}}"
+ setColor: "Set {{name}} color to {{color}}"
+ colors: "Colors"
+ confirmDelete: "I confirm that I want to delete theme called '{{theme}}' and that this is an irreversible action."
+ defaultThemeNameFeedback: "Default theme can not be renamed. You can create a new theme based on Default instead."
+ deleteColors: "Delete colors"
+ deleteLocalTheme: "Delete theme (Only the locally stored one)"
+ deleteTheme: "Delete theme"
+ deleteThemes: "Delete themes"
+ downloadThemeBeforeDeleting: "Would you like to download the theme '{{theme}}' before deleting it?"
+ example: "Example"
+ failedToClone: "Failed to clone the original theme {{error}}"
+ finish: "Finish"
+ gradientWarning: "Gradients do not work with all elements."
+ hideHistory: "Hide history"
+ invalidName: "Name should be alphanumerical and must be unique. Max 100 characters."
+ issues:
+ missingNightCase: "Night mode {{name}} is missing color {{colorName}}"
+ missingUseCase: "Use case {{name}} is missing color {{colorName}}"
+ problems: "Problems"
+ lightModeInfo: "Theme editor uses light-mode to see fully saturated colors."
+ missing: "Missing color"
+ nameWarning: "A valid name that doesn't already exist is needed."
+ nightColors: "Night mode"
+ nightModeOverrides: "Night mode overrides"
+ noPermissionToDelete: "You don't have access rights for deleting non-local themes. You may need to delete them from the plugin folder."
+ openEditor: "Open editor"
+ redo: "Redo"
+ removeOverride: "Remove night mode override"
+ showHistory: "Show history"
+ themeColorOptions: "User theme color options"
+ themeName: "Theme name"
+ themeStoredOnlyLocally: "Theme is currently only in Browser local storage (Only you can see it)."
+ themeToDelete: "Theme to delete"
+ title: "Theme Editor"
+ undo: "Undo"
+ unsavedChanges: "There are unsaved changes - do you still want to leave the page?"
+ uploadTheme: "or Upload a previously downloaded theme:"
+ useCases: "Use cases"
themeSelect: "テーマ選択"
thirdDeadliestWeapon: "3番目にPvPで使用されている武器"
thirtyDays: "1ヶ月"
@@ -625,6 +683,7 @@ html:
users: "Manage Users"
version: "Version"
veryActive: "とてもログインしている"
+ weapon: "Weapon"
weekComparison: "直近1週間での比較"
weekdays: "'月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日', '日曜日'"
world: "ワールドのロード数"
@@ -657,7 +716,9 @@ html:
access_query: "/query ページとクエリ結果へのアクセスを許可"
access_raw_player_data: "/player/{uuid}/raw ページへのアクセスを許可 のjsonデータへのアクセスを許可します。'access.player' 権限に従います"
access_server: "全ての /server ページへのアクセスを許可"
+ access_theme_editor: "Allows accessing /theme-editor page"
manage_groups: "グループ権限の変更と/manage/groups ページへのアクセスを許可"
+ manage_themes: "Allows saving or deleting themes via theme-editor for everyone"
manage_users: "ユーザーの所属グループを変更可能"
page: "ページに表示される内容を制御する"
page_network: "ネットワークのページをすべてを表示"
@@ -714,6 +775,14 @@ html:
page_server_overview_players_online_graph: "プレイヤーオンライングラフを表示"
page_server_performance: "パフォーマンスタブを表示"
page_server_performance_graphs: "パフォーマンスグラフを表示"
+ page_server_performance_graphs_chunks: "See Chunk count data in Performance graphs"
+ page_server_performance_graphs_cpu: "See CPU usage in Performance graphs"
+ page_server_performance_graphs_disk: "See Disk Space usage Performance graphs"
+ page_server_performance_graphs_entities: "See Entity count data in Performance graphs"
+ page_server_performance_graphs_ping: "See Ping data in Performance graphs"
+ page_server_performance_graphs_players_online: "See Players Online data in Performance graphs"
+ page_server_performance_graphs_ram: "See Memory usage in Performance graphs"
+ page_server_performance_graphs_tps: "See TPS data in Performance graphs"
page_server_performance_overview: "パフォーマンス番号を表示"
page_server_player_versus: "PvP & PvEを表示"
page_server_player_versus_kill_list: "プレイヤーのK/Dリストを表示"
@@ -736,7 +805,7 @@ html:
bugreporters: "そして、バグ報告者のみなさん!"
code: ":プログラミング貢献者 "
donate: "このプラグイン開発に募金して頂いた人々へ特別な感謝を"
- text: '加えて、以下の素晴らしい人々 が開発に貢献しています'
+ text: "加えて、以下の<1>素晴らしい人々1>が開発に貢献しています"
translator: ":翻訳者 "
developer: "開発者:"
discord: "Discordのサポートチャンネル"
@@ -781,7 +850,7 @@ html:
text: "プレイヤーの間"
pluginGroup:
name: "グループ: "
- text: "${plugin}の${group}に属しています"
+ text: "{{plugin}}の{{group}}に属しています"
registeredBetween:
text: "の間に登録されました"
skipped: "スキップ"
@@ -795,7 +864,7 @@ html:
are: "`それは`"
label:
editQuery: "Edit Query"
- from: ">から"
+ from: "から"
makeAnother: "別のクエリを作る"
servers:
all: "以下に含まれる全てのサーバー"
@@ -803,18 +872,18 @@ html:
single: "1つのサーバーのデータを使用しています"
two: "2つのサーバーのデータを使用しています"
showFullQuery: "Show Full Query"
- to: ">に"
+ to: "に"
view: "ビューを表示"
performQuery: "クエリを実行!"
results:
- match: "${resultCount}のプレイヤーがマッチしました"
+ match: "{{resultCount}}のプレイヤーがマッチしました"
none: "クエリで生成された結果は0件でした"
title: "クエリ結果"
title:
activity: "マッチしたプレイヤーのアクティビティ"
- activityOnDate: 'アクティビティ '
+ activityOnDate: "アクティビティ {{activityDate}}"
sessionsWithinView: "ビュー内のセッション"
- text: "クエリ<"
+ text: "クエリ"
register:
completion: "登録を完了するには"
completion1: "ユーザー登録ができるようになりました"
diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml
index 8e3fb5d6df..e4c8e13686 100644
--- a/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml
+++ b/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml
@@ -367,7 +367,7 @@ html:
activityIndexExample2: "Very active is ~2x the threshold (y ≥ 3.75)."
activityIndexExample3: "The index approaches 5 indefinitely."
activityIndexVisual: "Here is a visualization of the curve where y = activity index, and x = playtime per week / threshold."
- activityIndexWeek: "Week {}"
+ activityIndexWeek: "Week {{number}}"
examples: "Examples"
graph:
labels: "You can hide/show a group by clicking on the label at the bottom."
@@ -564,6 +564,10 @@ html:
timeStep: "Time step"
secondDeadliestWeapon: "2nd PvP 무기"
seenNicknames: "본 별명"
+ select:
+ noOptions: "No options available"
+ select: "Select.."
+ selectSomeAddresses: "Select some addresses"
server: "서버"
serverAnalysis: "서버 분석"
serverAsNumberse: "서버 번호"
@@ -590,6 +594,60 @@ html:
showNofM: "Showing {{n}} of {{m}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
+ themeEditor:
+ addColor: "Add color"
+ addTheme: "Add theme"
+ alreadyExistsWarning: "Color with that name already exists - It will be overridden!"
+ basedOnTheme: "Based on theme"
+ canNotDeleteBuiltIn: "Note that you can not delete built-in themes, only the modifications you have made to them."
+ changes:
+ addColor: "Added {{name}} color {{color}}"
+ changeNightMode: "Changed night mode {{path}} to {{name}}"
+ changeNightModeArray: "Changed night mode {{path}} list"
+ changeUseCase: "Changed {{path}} to {{name}}"
+ changeUseCaseArray: "Changed {{path}} list"
+ deleteColor: "Deleted color {{name}}"
+ discardedChanges: "Discarded changes:"
+ removeNightMode: "Removed night mode override {{path}}"
+ renameColor: "Renamed {{previous}} to {{name}}, set color to {{color}}"
+ setColor: "Set {{name}} color to {{color}}"
+ colors: "Colors"
+ confirmDelete: "I confirm that I want to delete theme called '{{theme}}' and that this is an irreversible action."
+ defaultThemeNameFeedback: "Default theme can not be renamed. You can create a new theme based on Default instead."
+ deleteColors: "Delete colors"
+ deleteLocalTheme: "Delete theme (Only the locally stored one)"
+ deleteTheme: "Delete theme"
+ deleteThemes: "Delete themes"
+ downloadThemeBeforeDeleting: "Would you like to download the theme '{{theme}}' before deleting it?"
+ example: "Example"
+ failedToClone: "Failed to clone the original theme {{error}}"
+ finish: "Finish"
+ gradientWarning: "Gradients do not work with all elements."
+ hideHistory: "Hide history"
+ invalidName: "Name should be alphanumerical and must be unique. Max 100 characters."
+ issues:
+ missingNightCase: "Night mode {{name}} is missing color {{colorName}}"
+ missingUseCase: "Use case {{name}} is missing color {{colorName}}"
+ problems: "Problems"
+ lightModeInfo: "Theme editor uses light-mode to see fully saturated colors."
+ missing: "Missing color"
+ nameWarning: "A valid name that doesn't already exist is needed."
+ nightColors: "Night mode"
+ nightModeOverrides: "Night mode overrides"
+ noPermissionToDelete: "You don't have access rights for deleting non-local themes. You may need to delete them from the plugin folder."
+ openEditor: "Open editor"
+ redo: "Redo"
+ removeOverride: "Remove night mode override"
+ showHistory: "Show history"
+ themeColorOptions: "User theme color options"
+ themeName: "Theme name"
+ themeStoredOnlyLocally: "Theme is currently only in Browser local storage (Only you can see it)."
+ themeToDelete: "Theme to delete"
+ title: "Theme Editor"
+ undo: "Undo"
+ unsavedChanges: "There are unsaved changes - do you still want to leave the page?"
+ uploadTheme: "or Upload a previously downloaded theme:"
+ useCases: "Use cases"
themeSelect: "테마 선택"
thirdDeadliestWeapon: "3rd PvP 무기"
thirtyDays: "30일"
@@ -625,6 +683,7 @@ html:
users: "Manage Users"
version: "Version"
veryActive: "매우 활성화된"
+ weapon: "Weapon"
weekComparison: "주 비교"
weekdays: "'월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'"
world: "월드 로드"
@@ -657,7 +716,9 @@ html:
access_query: "Allows accessing /query and Query results pages"
access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions."
access_server: "Allows accessing all /server pages"
+ access_theme_editor: "Allows accessing /theme-editor page"
manage_groups: "Allows modifying group permissions & Access to /manage/groups page"
+ manage_themes: "Allows saving or deleting themes via theme-editor for everyone"
manage_users: "Allows modifying what users belong to what group"
page: "Controls what is visible on pages"
page_network: "See all of network page"
@@ -714,6 +775,14 @@ html:
page_server_overview_players_online_graph: "See Players Online graph"
page_server_performance: "See Performance tab"
page_server_performance_graphs: "See Performance graphs"
+ page_server_performance_graphs_chunks: "See Chunk count data in Performance graphs"
+ page_server_performance_graphs_cpu: "See CPU usage in Performance graphs"
+ page_server_performance_graphs_disk: "See Disk Space usage Performance graphs"
+ page_server_performance_graphs_entities: "See Entity count data in Performance graphs"
+ page_server_performance_graphs_ping: "See Ping data in Performance graphs"
+ page_server_performance_graphs_players_online: "See Players Online data in Performance graphs"
+ page_server_performance_graphs_ram: "See Memory usage in Performance graphs"
+ page_server_performance_graphs_tps: "See TPS data in Performance graphs"
page_server_performance_overview: "See Performance numbers"
page_server_player_versus: "See PvP & PvE -tab"
page_server_player_versus_kill_list: "See Player kill and death lists"
@@ -736,7 +805,7 @@ html:
bugreporters: "& Bug reporters!"
code: "코드 기여자"
donate: "금전적으로 개발을 지원 해주신 분들께 특별히 감사드립니다."
- text: '또한 다음 멋진 사람들 이 기여했습니다.'
+ text: "또한 다음 <1>멋진 사람들1>이 기여했습니다."
translator: "번역"
developer: "에 의해 개발되었습니다"
discord: "디스코드로 기술지원"
@@ -781,7 +850,7 @@ html:
text: "Played between"
pluginGroup:
name: "Group: "
- text: "are in ${plugin}'s ${group} Groups"
+ text: "are in {{plugin}}'s {{group}} Groups"
registeredBetween:
text: "Registered between"
skipped: "Skipped"
@@ -795,26 +864,26 @@ html:
are: "`are`"
label:
editQuery: "Edit Query"
- from: ">from"
+ from: "from"
makeAnother: "Make another query"
servers:
all: "using data of all servers"
- many: "using data of {number} servers"
+ many: "using data of{{number}}servers"
single: "using data of 1 server"
two: "using data of 2 servers"
showFullQuery: "Show Full Query"
- to: ">to"
+ to: "to"
view: "Show a view"
performQuery: "Perform Query!"
results:
- match: "matched ${resultCount} players"
+ match: "matched {{resultCount}} players"
none: "Query produced 0 results"
title: "Query Results"
title:
activity: "Activity of matched players"
- activityOnDate: 'Activity on '
+ activityOnDate: "Activity on {{activityDate}}"
sessionsWithinView: "Sessions within view"
- text: "Query<"
+ text: "Query"
register:
completion: "Complete Registration"
completion1: "You can now finish registering the user."
diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml
index 08b41e3e29..2e247e4ec1 100644
--- a/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml
+++ b/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml
@@ -367,7 +367,7 @@ html:
activityIndexExample2: "Very active is ~2x the threshold (y ≥ 3.75)."
activityIndexExample3: "The index approaches 5 indefinitely."
activityIndexVisual: "Here is a visualization of the curve where y = activity index, and x = playtime per week / threshold."
- activityIndexWeek: "Week {}"
+ activityIndexWeek: "Week {{number}}"
examples: "Examples"
graph:
labels: "You can hide/show a group by clicking on the label at the bottom."
@@ -564,6 +564,10 @@ html:
timeStep: "Time step"
secondDeadliestWeapon: "2e PvP-wapen"
seenNicknames: "Bijnamen gezien"
+ select:
+ noOptions: "No options available"
+ select: "Select.."
+ selectSomeAddresses: "Select some addresses"
server: "Server"
serverAnalysis: "Serveranalyse"
serverAsNumberse: "Server als getallen"
@@ -590,6 +594,60 @@ html:
showNofM: "Showing {{n}} of {{m}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
+ themeEditor:
+ addColor: "Add color"
+ addTheme: "Add theme"
+ alreadyExistsWarning: "Color with that name already exists - It will be overridden!"
+ basedOnTheme: "Based on theme"
+ canNotDeleteBuiltIn: "Note that you can not delete built-in themes, only the modifications you have made to them."
+ changes:
+ addColor: "Added {{name}} color {{color}}"
+ changeNightMode: "Changed night mode {{path}} to {{name}}"
+ changeNightModeArray: "Changed night mode {{path}} list"
+ changeUseCase: "Changed {{path}} to {{name}}"
+ changeUseCaseArray: "Changed {{path}} list"
+ deleteColor: "Deleted color {{name}}"
+ discardedChanges: "Discarded changes:"
+ removeNightMode: "Removed night mode override {{path}}"
+ renameColor: "Renamed {{previous}} to {{name}}, set color to {{color}}"
+ setColor: "Set {{name}} color to {{color}}"
+ colors: "Colors"
+ confirmDelete: "I confirm that I want to delete theme called '{{theme}}' and that this is an irreversible action."
+ defaultThemeNameFeedback: "Default theme can not be renamed. You can create a new theme based on Default instead."
+ deleteColors: "Delete colors"
+ deleteLocalTheme: "Delete theme (Only the locally stored one)"
+ deleteTheme: "Delete theme"
+ deleteThemes: "Delete themes"
+ downloadThemeBeforeDeleting: "Would you like to download the theme '{{theme}}' before deleting it?"
+ example: "Example"
+ failedToClone: "Failed to clone the original theme {{error}}"
+ finish: "Finish"
+ gradientWarning: "Gradients do not work with all elements."
+ hideHistory: "Hide history"
+ invalidName: "Name should be alphanumerical and must be unique. Max 100 characters."
+ issues:
+ missingNightCase: "Night mode {{name}} is missing color {{colorName}}"
+ missingUseCase: "Use case {{name}} is missing color {{colorName}}"
+ problems: "Problems"
+ lightModeInfo: "Theme editor uses light-mode to see fully saturated colors."
+ missing: "Missing color"
+ nameWarning: "A valid name that doesn't already exist is needed."
+ nightColors: "Night mode"
+ nightModeOverrides: "Night mode overrides"
+ noPermissionToDelete: "You don't have access rights for deleting non-local themes. You may need to delete them from the plugin folder."
+ openEditor: "Open editor"
+ redo: "Redo"
+ removeOverride: "Remove night mode override"
+ showHistory: "Show history"
+ themeColorOptions: "User theme color options"
+ themeName: "Theme name"
+ themeStoredOnlyLocally: "Theme is currently only in Browser local storage (Only you can see it)."
+ themeToDelete: "Theme to delete"
+ title: "Theme Editor"
+ undo: "Undo"
+ unsavedChanges: "There are unsaved changes - do you still want to leave the page?"
+ uploadTheme: "or Upload a previously downloaded theme:"
+ useCases: "Use cases"
themeSelect: "Thema selecteren"
thirdDeadliestWeapon: "3e PvP-wapen"
thirtyDays: "30 dagen"
@@ -625,6 +683,7 @@ html:
users: "Manage Users"
version: "Version"
veryActive: "Heel Actief"
+ weapon: "Weapon"
weekComparison: "Weekvergelijking"
weekdays: "'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag', 'Zondag'"
world: "Wereldbelasting"
@@ -657,7 +716,9 @@ html:
access_query: "Allows accessing /query and Query results pages"
access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions."
access_server: "Allows accessing all /server pages"
+ access_theme_editor: "Allows accessing /theme-editor page"
manage_groups: "Allows modifying group permissions & Access to /manage/groups page"
+ manage_themes: "Allows saving or deleting themes via theme-editor for everyone"
manage_users: "Allows modifying what users belong to what group"
page: "Controls what is visible on pages"
page_network: "See all of network page"
@@ -714,6 +775,14 @@ html:
page_server_overview_players_online_graph: "See Players Online graph"
page_server_performance: "See Performance tab"
page_server_performance_graphs: "See Performance graphs"
+ page_server_performance_graphs_chunks: "See Chunk count data in Performance graphs"
+ page_server_performance_graphs_cpu: "See CPU usage in Performance graphs"
+ page_server_performance_graphs_disk: "See Disk Space usage Performance graphs"
+ page_server_performance_graphs_entities: "See Entity count data in Performance graphs"
+ page_server_performance_graphs_ping: "See Ping data in Performance graphs"
+ page_server_performance_graphs_players_online: "See Players Online data in Performance graphs"
+ page_server_performance_graphs_ram: "See Memory usage in Performance graphs"
+ page_server_performance_graphs_tps: "See TPS data in Performance graphs"
page_server_performance_overview: "See Performance numbers"
page_server_player_versus: "See PvP & PvE -tab"
page_server_player_versus_kill_list: "See Player kill and death lists"
@@ -736,7 +805,7 @@ html:
bugreporters: "& Bug melders!"
code: "code bijdrager"
donate: "Extra speciale dank aan degenen die de ontwikkeling financieel hebben ondersteund."
- text: 'Daarnaast hebben de volgende geweldige mensen bijgedragen:'
+ text: "Daarnaast hebben de volgende <1>geweldige mensen1> bijgedragen:"
translator: "vertaler"
developer: "is ontwikkeld door"
discord: "Algemene ondersteuning op Discord"
@@ -781,7 +850,7 @@ html:
text: "Spelers tussen"
pluginGroup:
name: "Groep: "
- text: "zijn in ${plugin}'s ${group} groepen"
+ text: "zijn in {{plugin}}'s {{group}} groepen"
registeredBetween:
text: "Geregistreerd tussen"
skipped: "Skipped"
@@ -795,26 +864,26 @@ html:
are: "`zijn`"
label:
editQuery: "Edit Query"
- from: ">van"
+ from: "van"
makeAnother: "Maak nog een query"
servers:
all: "using data of all servers"
- many: "using data of {number} servers"
+ many: "using data of{{number}}servers"
single: "using data of 1 server"
two: "using data of 2 servers"
showFullQuery: "Show Full Query"
- to: ">naar"
+ to: "naar"
view: "Toon een weergave"
performQuery: "Query Uitvoeren!"
results:
- match: "matchte ${resultCount} spelers"
+ match: "matchte {{resultCount}} spelers"
none: "Query heeft 0 resultaten opgeleverd"
title: "Query Resultaat"
title:
activity: "Activiteit van gematchte spelers"
- activityOnDate: 'Activiteit op '
+ activityOnDate: "Activiteit op {{activityDate}}"
sessionsWithinView: "Sessies in weergave"
- text: "Query<"
+ text: "Query"
register:
completion: "Voltooi registratie"
completion1: "U kunt nu de registratie van de gebruiker voltooien."
diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml
index 401d50d049..a134155713 100644
--- a/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml
+++ b/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml
@@ -367,7 +367,7 @@ html:
activityIndexExample2: "Very active is ~2x the threshold (y ≥ 3.75)."
activityIndexExample3: "The index approaches 5 indefinitely."
activityIndexVisual: "Here is a visualization of the curve where y = activity index, and x = playtime per week / threshold."
- activityIndexWeek: "Week {}"
+ activityIndexWeek: "Week {{number}}"
examples: "Examples"
graph:
labels: "You can hide/show a group by clicking on the label at the bottom."
@@ -564,6 +564,10 @@ html:
timeStep: "Time step"
secondDeadliestWeapon: "2nd PvP Weapon"
seenNicknames: "Nicks Vistos"
+ select:
+ noOptions: "No options available"
+ select: "Select.."
+ selectSomeAddresses: "Select some addresses"
server: "Servidor"
serverAnalysis: "Análise do Servidor"
serverAsNumberse: "Server as Numbers"
@@ -590,6 +594,60 @@ html:
showNofM: "Showing {{n}} of {{m}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
+ themeEditor:
+ addColor: "Add color"
+ addTheme: "Add theme"
+ alreadyExistsWarning: "Color with that name already exists - It will be overridden!"
+ basedOnTheme: "Based on theme"
+ canNotDeleteBuiltIn: "Note that you can not delete built-in themes, only the modifications you have made to them."
+ changes:
+ addColor: "Added {{name}} color {{color}}"
+ changeNightMode: "Changed night mode {{path}} to {{name}}"
+ changeNightModeArray: "Changed night mode {{path}} list"
+ changeUseCase: "Changed {{path}} to {{name}}"
+ changeUseCaseArray: "Changed {{path}} list"
+ deleteColor: "Deleted color {{name}}"
+ discardedChanges: "Discarded changes:"
+ removeNightMode: "Removed night mode override {{path}}"
+ renameColor: "Renamed {{previous}} to {{name}}, set color to {{color}}"
+ setColor: "Set {{name}} color to {{color}}"
+ colors: "Colors"
+ confirmDelete: "I confirm that I want to delete theme called '{{theme}}' and that this is an irreversible action."
+ defaultThemeNameFeedback: "Default theme can not be renamed. You can create a new theme based on Default instead."
+ deleteColors: "Delete colors"
+ deleteLocalTheme: "Delete theme (Only the locally stored one)"
+ deleteTheme: "Delete theme"
+ deleteThemes: "Delete themes"
+ downloadThemeBeforeDeleting: "Would you like to download the theme '{{theme}}' before deleting it?"
+ example: "Example"
+ failedToClone: "Failed to clone the original theme {{error}}"
+ finish: "Finish"
+ gradientWarning: "Gradients do not work with all elements."
+ hideHistory: "Hide history"
+ invalidName: "Name should be alphanumerical and must be unique. Max 100 characters."
+ issues:
+ missingNightCase: "Night mode {{name}} is missing color {{colorName}}"
+ missingUseCase: "Use case {{name}} is missing color {{colorName}}"
+ problems: "Problems"
+ lightModeInfo: "Theme editor uses light-mode to see fully saturated colors."
+ missing: "Missing color"
+ nameWarning: "A valid name that doesn't already exist is needed."
+ nightColors: "Night mode"
+ nightModeOverrides: "Night mode overrides"
+ noPermissionToDelete: "You don't have access rights for deleting non-local themes. You may need to delete them from the plugin folder."
+ openEditor: "Open editor"
+ redo: "Redo"
+ removeOverride: "Remove night mode override"
+ showHistory: "Show history"
+ themeColorOptions: "User theme color options"
+ themeName: "Theme name"
+ themeStoredOnlyLocally: "Theme is currently only in Browser local storage (Only you can see it)."
+ themeToDelete: "Theme to delete"
+ title: "Theme Editor"
+ undo: "Undo"
+ unsavedChanges: "There are unsaved changes - do you still want to leave the page?"
+ uploadTheme: "or Upload a previously downloaded theme:"
+ useCases: "Use cases"
themeSelect: "Theme Select"
thirdDeadliestWeapon: "3rd PvP Weapon"
thirtyDays: "30 days"
@@ -625,6 +683,7 @@ html:
users: "Manage Users"
version: "Version"
veryActive: "Muito Ativo"
+ weapon: "Weapon"
weekComparison: "Week Comparison"
weekdays: "'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'"
world: "World Load"
@@ -657,7 +716,9 @@ html:
access_query: "Allows accessing /query and Query results pages"
access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions."
access_server: "Allows accessing all /server pages"
+ access_theme_editor: "Allows accessing /theme-editor page"
manage_groups: "Allows modifying group permissions & Access to /manage/groups page"
+ manage_themes: "Allows saving or deleting themes via theme-editor for everyone"
manage_users: "Allows modifying what users belong to what group"
page: "Controls what is visible on pages"
page_network: "See all of network page"
@@ -714,6 +775,14 @@ html:
page_server_overview_players_online_graph: "See Players Online graph"
page_server_performance: "See Performance tab"
page_server_performance_graphs: "See Performance graphs"
+ page_server_performance_graphs_chunks: "See Chunk count data in Performance graphs"
+ page_server_performance_graphs_cpu: "See CPU usage in Performance graphs"
+ page_server_performance_graphs_disk: "See Disk Space usage Performance graphs"
+ page_server_performance_graphs_entities: "See Entity count data in Performance graphs"
+ page_server_performance_graphs_ping: "See Ping data in Performance graphs"
+ page_server_performance_graphs_players_online: "See Players Online data in Performance graphs"
+ page_server_performance_graphs_ram: "See Memory usage in Performance graphs"
+ page_server_performance_graphs_tps: "See TPS data in Performance graphs"
page_server_performance_overview: "See Performance numbers"
page_server_player_versus: "See PvP & PvE -tab"
page_server_player_versus_kill_list: "See Player kill and death lists"
@@ -736,7 +805,7 @@ html:
bugreporters: "& Bug reporters!"
code: "code contributor"
donate: "Extra special thanks to those who have monetarily supported the development."
- text: 'In addition following awesome people have contributed:'
+ text: "In addition following <1>awesome people1> have contributed:"
translator: "translator"
developer: "is developed by"
discord: "General Support on Discord"
@@ -781,7 +850,7 @@ html:
text: "Played between"
pluginGroup:
name: "Group: "
- text: "are in ${plugin}'s ${group} Groups"
+ text: "are in {{plugin}}'s {{group}} Groups"
registeredBetween:
text: "Registered between"
skipped: "Skipped"
@@ -795,26 +864,26 @@ html:
are: "`are`"
label:
editQuery: "Edit Query"
- from: ">from"
+ from: "from"
makeAnother: "Make another query"
servers:
all: "using data of all servers"
- many: "using data of {number} servers"
+ many: "using data of{{number}}servers"
single: "using data of 1 server"
two: "using data of 2 servers"
showFullQuery: "Show Full Query"
- to: ">to"
+ to: "to"
view: "Show a view"
performQuery: "Perform Query!"
results:
- match: "matched ${resultCount} players"
+ match: "matched {{resultCount}} players"
none: "Query produced 0 results"
title: "Query Results"
title:
activity: "Activity of matched players"
- activityOnDate: 'Activity on '
+ activityOnDate: "Activity on {{activityDate}}"
sessionsWithinView: "Sessions within view"
- text: "Query<"
+ text: "Query"
register:
completion: "Complete Registration"
completion1: "You can now finish registering the user."
diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml
index 6e9e651556..ba6f3ba79e 100644
--- a/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml
+++ b/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml
@@ -367,7 +367,7 @@ html:
activityIndexExample2: "Very active is ~2x the threshold (y ≥ 3.75)."
activityIndexExample3: "The index approaches 5 indefinitely."
activityIndexVisual: "Here is a visualization of the curve where y = activity index, and x = playtime per week / threshold."
- activityIndexWeek: "Week {}"
+ activityIndexWeek: "Week {{number}}"
examples: "Examples"
graph:
labels: "You can hide/show a group by clicking on the label at the bottom."
@@ -564,6 +564,10 @@ html:
timeStep: "Time step"
secondDeadliestWeapon: "2-е PvP оружие"
seenNicknames: "Увиденные никнеймы"
+ select:
+ noOptions: "No options available"
+ select: "Select.."
+ selectSomeAddresses: "Select some addresses"
server: "Сервер"
serverAnalysis: "Анализ сервера"
serverAsNumberse: "Сервер в числах"
@@ -590,6 +594,60 @@ html:
showNofM: "Showing {{n}} of {{m}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
+ themeEditor:
+ addColor: "Add color"
+ addTheme: "Add theme"
+ alreadyExistsWarning: "Color with that name already exists - It will be overridden!"
+ basedOnTheme: "Based on theme"
+ canNotDeleteBuiltIn: "Note that you can not delete built-in themes, only the modifications you have made to them."
+ changes:
+ addColor: "Added {{name}} color {{color}}"
+ changeNightMode: "Changed night mode {{path}} to {{name}}"
+ changeNightModeArray: "Changed night mode {{path}} list"
+ changeUseCase: "Changed {{path}} to {{name}}"
+ changeUseCaseArray: "Changed {{path}} list"
+ deleteColor: "Deleted color {{name}}"
+ discardedChanges: "Discarded changes:"
+ removeNightMode: "Removed night mode override {{path}}"
+ renameColor: "Renamed {{previous}} to {{name}}, set color to {{color}}"
+ setColor: "Set {{name}} color to {{color}}"
+ colors: "Colors"
+ confirmDelete: "I confirm that I want to delete theme called '{{theme}}' and that this is an irreversible action."
+ defaultThemeNameFeedback: "Default theme can not be renamed. You can create a new theme based on Default instead."
+ deleteColors: "Delete colors"
+ deleteLocalTheme: "Delete theme (Only the locally stored one)"
+ deleteTheme: "Delete theme"
+ deleteThemes: "Delete themes"
+ downloadThemeBeforeDeleting: "Would you like to download the theme '{{theme}}' before deleting it?"
+ example: "Example"
+ failedToClone: "Failed to clone the original theme {{error}}"
+ finish: "Finish"
+ gradientWarning: "Gradients do not work with all elements."
+ hideHistory: "Hide history"
+ invalidName: "Name should be alphanumerical and must be unique. Max 100 characters."
+ issues:
+ missingNightCase: "Night mode {{name}} is missing color {{colorName}}"
+ missingUseCase: "Use case {{name}} is missing color {{colorName}}"
+ problems: "Problems"
+ lightModeInfo: "Theme editor uses light-mode to see fully saturated colors."
+ missing: "Missing color"
+ nameWarning: "A valid name that doesn't already exist is needed."
+ nightColors: "Night mode"
+ nightModeOverrides: "Night mode overrides"
+ noPermissionToDelete: "You don't have access rights for deleting non-local themes. You may need to delete them from the plugin folder."
+ openEditor: "Open editor"
+ redo: "Redo"
+ removeOverride: "Remove night mode override"
+ showHistory: "Show history"
+ themeColorOptions: "User theme color options"
+ themeName: "Theme name"
+ themeStoredOnlyLocally: "Theme is currently only in Browser local storage (Only you can see it)."
+ themeToDelete: "Theme to delete"
+ title: "Theme Editor"
+ undo: "Undo"
+ unsavedChanges: "There are unsaved changes - do you still want to leave the page?"
+ uploadTheme: "or Upload a previously downloaded theme:"
+ useCases: "Use cases"
themeSelect: "Выбор темы"
thirdDeadliestWeapon: "3-е PvP оружие"
thirtyDays: "30 дней"
@@ -625,6 +683,7 @@ html:
users: "Manage Users"
version: "Version"
veryActive: "Очень активный"
+ weapon: "Weapon"
weekComparison: "Сравнение за неделю"
weekdays: "'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'"
world: "Загрузка мира"
@@ -657,7 +716,9 @@ html:
access_query: "Allows accessing /query and Query results pages"
access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions."
access_server: "Allows accessing all /server pages"
+ access_theme_editor: "Allows accessing /theme-editor page"
manage_groups: "Allows modifying group permissions & Access to /manage/groups page"
+ manage_themes: "Allows saving or deleting themes via theme-editor for everyone"
manage_users: "Allows modifying what users belong to what group"
page: "Controls what is visible on pages"
page_network: "See all of network page"
@@ -714,6 +775,14 @@ html:
page_server_overview_players_online_graph: "See Players Online graph"
page_server_performance: "See Performance tab"
page_server_performance_graphs: "See Performance graphs"
+ page_server_performance_graphs_chunks: "See Chunk count data in Performance graphs"
+ page_server_performance_graphs_cpu: "See CPU usage in Performance graphs"
+ page_server_performance_graphs_disk: "See Disk Space usage Performance graphs"
+ page_server_performance_graphs_entities: "See Entity count data in Performance graphs"
+ page_server_performance_graphs_ping: "See Ping data in Performance graphs"
+ page_server_performance_graphs_players_online: "See Players Online data in Performance graphs"
+ page_server_performance_graphs_ram: "See Memory usage in Performance graphs"
+ page_server_performance_graphs_tps: "See TPS data in Performance graphs"
page_server_performance_overview: "See Performance numbers"
page_server_player_versus: "See PvP & PvE -tab"
page_server_player_versus_kill_list: "See Player kill and death lists"
@@ -736,7 +805,7 @@ html:
bugreporters: "& Баг репортеры!"
code: "автор кода"
donate: "Особая благодарность тем, кто оказал финансовую поддержку."
- text: 'Кроме того, данные замечательные люди внесли свой вклад:'
+ text: "Кроме того, данные <1>замечательные люди1> внесли свой вклад:"
translator: "переводчик"
developer: "разработан"
discord: "Общая поддержка в Discord"
@@ -781,7 +850,7 @@ html:
text: "Играл между"
pluginGroup:
name: "Группа: "
- text: "в ${plugin} ${group} Группах"
+ text: "в {{plugin}} {{group}} Группах"
registeredBetween:
text: "Зарегистрировался между"
skipped: "Skipped"
@@ -795,26 +864,26 @@ html:
are: "``"
label:
editQuery: "Edit Query"
- from: ">с"
+ from: "с"
makeAnother: "Сделать другой запрос"
servers:
all: "using data of all servers"
- many: "using data of {number} servers"
+ many: "using data of{{number}}servers"
single: "using data of 1 server"
two: "using data of 2 servers"
showFullQuery: "Show Full Query"
- to: ">в"
+ to: "в"
view: "Показывать результат"
performQuery: "Выполнить запрос!"
results:
- match: "найдено ${resultCount} игроков"
+ match: "найдено {{resultCount}} игроков"
none: "Результал дал 0 результатов"
title: "Результаты запроса"
title:
activity: "Активность выбраных игроков"
- activityOnDate: 'Активность на '
+ activityOnDate: "Активность на {{activityDate}}"
sessionsWithinView: "Сеансы в пределах видимости"
- text: "Запрос<"
+ text: "Запрос"
register:
completion: "Регистрация завершена"
completion1: "Вы должны закончить регистрацию пользователя."
diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml
index c363139a67..cb61251010 100644
--- a/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml
+++ b/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml
@@ -367,7 +367,7 @@ html:
activityIndexExample2: "Very active is ~2x the threshold (y ≥ 3.75)."
activityIndexExample3: "The index approaches 5 indefinitely."
activityIndexVisual: "Here is a visualization of the curve where y = activity index, and x = playtime per week / threshold."
- activityIndexWeek: "Week {}"
+ activityIndexWeek: "Week {{number}}"
examples: "Examples"
graph:
labels: "You can hide/show a group by clicking on the label at the bottom."
@@ -564,6 +564,10 @@ html:
timeStep: "Time step"
secondDeadliestWeapon: "2. PvP Silahı"
seenNicknames: "Görülen takma adlar"
+ select:
+ noOptions: "No options available"
+ select: "Select.."
+ selectSomeAddresses: "Select some addresses"
server: "Sunucu"
serverAnalysis: "Sunucu analizi"
serverAsNumberse: "Server as Numbers"
@@ -590,6 +594,60 @@ html:
showNofM: "Showing {{n}} of {{m}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
+ themeEditor:
+ addColor: "Add color"
+ addTheme: "Add theme"
+ alreadyExistsWarning: "Color with that name already exists - It will be overridden!"
+ basedOnTheme: "Based on theme"
+ canNotDeleteBuiltIn: "Note that you can not delete built-in themes, only the modifications you have made to them."
+ changes:
+ addColor: "Added {{name}} color {{color}}"
+ changeNightMode: "Changed night mode {{path}} to {{name}}"
+ changeNightModeArray: "Changed night mode {{path}} list"
+ changeUseCase: "Changed {{path}} to {{name}}"
+ changeUseCaseArray: "Changed {{path}} list"
+ deleteColor: "Deleted color {{name}}"
+ discardedChanges: "Discarded changes:"
+ removeNightMode: "Removed night mode override {{path}}"
+ renameColor: "Renamed {{previous}} to {{name}}, set color to {{color}}"
+ setColor: "Set {{name}} color to {{color}}"
+ colors: "Colors"
+ confirmDelete: "I confirm that I want to delete theme called '{{theme}}' and that this is an irreversible action."
+ defaultThemeNameFeedback: "Default theme can not be renamed. You can create a new theme based on Default instead."
+ deleteColors: "Delete colors"
+ deleteLocalTheme: "Delete theme (Only the locally stored one)"
+ deleteTheme: "Delete theme"
+ deleteThemes: "Delete themes"
+ downloadThemeBeforeDeleting: "Would you like to download the theme '{{theme}}' before deleting it?"
+ example: "Example"
+ failedToClone: "Failed to clone the original theme {{error}}"
+ finish: "Finish"
+ gradientWarning: "Gradients do not work with all elements."
+ hideHistory: "Hide history"
+ invalidName: "Name should be alphanumerical and must be unique. Max 100 characters."
+ issues:
+ missingNightCase: "Night mode {{name}} is missing color {{colorName}}"
+ missingUseCase: "Use case {{name}} is missing color {{colorName}}"
+ problems: "Problems"
+ lightModeInfo: "Theme editor uses light-mode to see fully saturated colors."
+ missing: "Missing color"
+ nameWarning: "A valid name that doesn't already exist is needed."
+ nightColors: "Night mode"
+ nightModeOverrides: "Night mode overrides"
+ noPermissionToDelete: "You don't have access rights for deleting non-local themes. You may need to delete them from the plugin folder."
+ openEditor: "Open editor"
+ redo: "Redo"
+ removeOverride: "Remove night mode override"
+ showHistory: "Show history"
+ themeColorOptions: "User theme color options"
+ themeName: "Theme name"
+ themeStoredOnlyLocally: "Theme is currently only in Browser local storage (Only you can see it)."
+ themeToDelete: "Theme to delete"
+ title: "Theme Editor"
+ undo: "Undo"
+ unsavedChanges: "There are unsaved changes - do you still want to leave the page?"
+ uploadTheme: "or Upload a previously downloaded theme:"
+ useCases: "Use cases"
themeSelect: "Tema Seçimi"
thirdDeadliestWeapon: "3. PvP Silahı"
thirtyDays: "30 gün"
@@ -625,6 +683,7 @@ html:
users: "Manage Users"
version: "Version"
veryActive: "Çok Aktif"
+ weapon: "Weapon"
weekComparison: "Hafta Karşılaştırması"
weekdays: "'Pazartesi', 'Salı', 'Çarşamba', 'Perşembe', 'Cuma', 'Cumartesi', 'Pazar'"
world: "Dünya Yükle"
@@ -657,7 +716,9 @@ html:
access_query: "Allows accessing /query and Query results pages"
access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions."
access_server: "Allows accessing all /server pages"
+ access_theme_editor: "Allows accessing /theme-editor page"
manage_groups: "Allows modifying group permissions & Access to /manage/groups page"
+ manage_themes: "Allows saving or deleting themes via theme-editor for everyone"
manage_users: "Allows modifying what users belong to what group"
page: "Controls what is visible on pages"
page_network: "See all of network page"
@@ -714,6 +775,14 @@ html:
page_server_overview_players_online_graph: "See Players Online graph"
page_server_performance: "See Performance tab"
page_server_performance_graphs: "See Performance graphs"
+ page_server_performance_graphs_chunks: "See Chunk count data in Performance graphs"
+ page_server_performance_graphs_cpu: "See CPU usage in Performance graphs"
+ page_server_performance_graphs_disk: "See Disk Space usage Performance graphs"
+ page_server_performance_graphs_entities: "See Entity count data in Performance graphs"
+ page_server_performance_graphs_ping: "See Ping data in Performance graphs"
+ page_server_performance_graphs_players_online: "See Players Online data in Performance graphs"
+ page_server_performance_graphs_ram: "See Memory usage in Performance graphs"
+ page_server_performance_graphs_tps: "See TPS data in Performance graphs"
page_server_performance_overview: "See Performance numbers"
page_server_player_versus: "See PvP & PvE -tab"
page_server_player_versus_kill_list: "See Player kill and death lists"
@@ -736,7 +805,7 @@ html:
bugreporters: "& Bug reporters!"
code: "kod katkıda bulunan"
donate: "Gelişimi parasal olarak destekleyenlere ekstra özel teşekkürler."
- text: 'Ayrıca harika insanları takip ederek katkıda bulundu:'
+ text: "Ayrıca <1> harika insanları 1> takip ederek katkıda bulundu:"
translator: "çevirmen"
developer: "tarafından geliştirilmiştir"
discord: "Discord'da Genel Destek"
@@ -781,7 +850,7 @@ html:
text: "Arasında oynanan"
pluginGroup:
name: "Grup: "
- text: "${plugin} adlı kişinin ${group} Grubunda"
+ text: "{{plugin}} adlı kişinin {{group}} Grubunda"
registeredBetween:
text: "Arasında kayıtlı"
skipped: "Skipped"
@@ -795,26 +864,26 @@ html:
are: "`vardır`"
label:
editQuery: "Edit Query"
- from: ">dan"
+ from: "dan"
makeAnother: "Başka bir sorgu yap"
servers:
all: "using data of all servers"
- many: "using data of {number} servers"
+ many: "using data of{{number}}servers"
single: "using data of 1 server"
two: "using data of 2 servers"
showFullQuery: "Show Full Query"
- to: ">a"
+ to: "a"
view: "Bir görünüm göster"
performQuery: "Sorgu Gerçekleştirin!"
results:
- match: "${resultCount} oyuncuyla eşleşti"
+ match: "{{resultCount}} oyuncuyla eşleşti"
none: "Sorgu 0 sonuç üretti"
title: "Sorgu Sonuçları"
title:
activity: "Eşleşen oyuncuların etkinliği"
- activityOnDate: ' tarihindeki etkinlik'
+ activityOnDate: "{{activityDate}} tarihindeki etkinlik"
sessionsWithinView: "Görünüm içindeki oturumlar"
- text: "Sorgu<"
+ text: "Sorgu"
register:
completion: "Kaydı Tamamla"
completion1: "Artık kullanıcıyı kaydetmeyi bitirebilirsiniz."
diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_UK.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_UK.yml
index 0477308274..b510aed8ae 100644
--- a/Plan/common/src/main/resources/assets/plan/locale/locale_UK.yml
+++ b/Plan/common/src/main/resources/assets/plan/locale/locale_UK.yml
@@ -367,7 +367,7 @@ html:
activityIndexExample2: "Дуже активні - це ~2x поріг (y ≥ 3.75)."
activityIndexExample3: "Індекс наближається до 5 нескінченно."
activityIndexVisual: "Ось візуалізація кривої, де y - індекс активності, а x - час гри на тиждень / поріг."
- activityIndexWeek: "Тиждень {}"
+ activityIndexWeek: "Тиждень {{number}}"
examples: "Приклади"
graph:
labels: "Ви можете приховати/показати групу, натиснувши на ярлик внизу."
@@ -564,6 +564,10 @@ html:
timeStep: "Часовий крок"
secondDeadliestWeapon: "2-га PvP зброя"
seenNicknames: "Побачені нікнейми"
+ select:
+ noOptions: "No options available"
+ select: "Select.."
+ selectSomeAddresses: "Select some addresses"
server: "Сервер"
serverAnalysis: "Аналіз сервера"
serverAsNumberse: "Сервер у числах"
@@ -590,6 +594,60 @@ html:
showNofM: "Showing {{n}} of {{m}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
+ themeEditor:
+ addColor: "Add color"
+ addTheme: "Add theme"
+ alreadyExistsWarning: "Color with that name already exists - It will be overridden!"
+ basedOnTheme: "Based on theme"
+ canNotDeleteBuiltIn: "Note that you can not delete built-in themes, only the modifications you have made to them."
+ changes:
+ addColor: "Added {{name}} color {{color}}"
+ changeNightMode: "Changed night mode {{path}} to {{name}}"
+ changeNightModeArray: "Changed night mode {{path}} list"
+ changeUseCase: "Changed {{path}} to {{name}}"
+ changeUseCaseArray: "Changed {{path}} list"
+ deleteColor: "Deleted color {{name}}"
+ discardedChanges: "Discarded changes:"
+ removeNightMode: "Removed night mode override {{path}}"
+ renameColor: "Renamed {{previous}} to {{name}}, set color to {{color}}"
+ setColor: "Set {{name}} color to {{color}}"
+ colors: "Colors"
+ confirmDelete: "I confirm that I want to delete theme called '{{theme}}' and that this is an irreversible action."
+ defaultThemeNameFeedback: "Default theme can not be renamed. You can create a new theme based on Default instead."
+ deleteColors: "Delete colors"
+ deleteLocalTheme: "Delete theme (Only the locally stored one)"
+ deleteTheme: "Delete theme"
+ deleteThemes: "Delete themes"
+ downloadThemeBeforeDeleting: "Would you like to download the theme '{{theme}}' before deleting it?"
+ example: "Example"
+ failedToClone: "Failed to clone the original theme {{error}}"
+ finish: "Finish"
+ gradientWarning: "Gradients do not work with all elements."
+ hideHistory: "Hide history"
+ invalidName: "Name should be alphanumerical and must be unique. Max 100 characters."
+ issues:
+ missingNightCase: "Night mode {{name}} is missing color {{colorName}}"
+ missingUseCase: "Use case {{name}} is missing color {{colorName}}"
+ problems: "Problems"
+ lightModeInfo: "Theme editor uses light-mode to see fully saturated colors."
+ missing: "Missing color"
+ nameWarning: "A valid name that doesn't already exist is needed."
+ nightColors: "Night mode"
+ nightModeOverrides: "Night mode overrides"
+ noPermissionToDelete: "You don't have access rights for deleting non-local themes. You may need to delete them from the plugin folder."
+ openEditor: "Open editor"
+ redo: "Redo"
+ removeOverride: "Remove night mode override"
+ showHistory: "Show history"
+ themeColorOptions: "User theme color options"
+ themeName: "Theme name"
+ themeStoredOnlyLocally: "Theme is currently only in Browser local storage (Only you can see it)."
+ themeToDelete: "Theme to delete"
+ title: "Theme Editor"
+ undo: "Undo"
+ unsavedChanges: "There are unsaved changes - do you still want to leave the page?"
+ uploadTheme: "or Upload a previously downloaded theme:"
+ useCases: "Use cases"
themeSelect: "Вибір теми"
thirdDeadliestWeapon: "3-тя PvP зброя"
thirtyDays: "30 днів"
@@ -625,6 +683,7 @@ html:
users: "Manage Users"
version: "Version"
veryActive: "Дуже активний"
+ weapon: "Weapon"
weekComparison: "Порівняння за тиждень"
weekdays: "'Понеділок', 'Вівторок', 'Середа', 'Четвер', 'П`ятниця', 'Субота', 'Неділя'"
world: "Завантаження світу"
@@ -657,7 +716,9 @@ html:
access_query: "Allows accessing /query and Query results pages"
access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions."
access_server: "Allows accessing all /server pages"
+ access_theme_editor: "Allows accessing /theme-editor page"
manage_groups: "Allows modifying group permissions & Access to /manage/groups page"
+ manage_themes: "Allows saving or deleting themes via theme-editor for everyone"
manage_users: "Allows modifying what users belong to what group"
page: "Controls what is visible on pages"
page_network: "See all of network page"
@@ -714,6 +775,14 @@ html:
page_server_overview_players_online_graph: "See Players Online graph"
page_server_performance: "See Performance tab"
page_server_performance_graphs: "See Performance graphs"
+ page_server_performance_graphs_chunks: "See Chunk count data in Performance graphs"
+ page_server_performance_graphs_cpu: "See CPU usage in Performance graphs"
+ page_server_performance_graphs_disk: "See Disk Space usage Performance graphs"
+ page_server_performance_graphs_entities: "See Entity count data in Performance graphs"
+ page_server_performance_graphs_ping: "See Ping data in Performance graphs"
+ page_server_performance_graphs_players_online: "See Players Online data in Performance graphs"
+ page_server_performance_graphs_ram: "See Memory usage in Performance graphs"
+ page_server_performance_graphs_tps: "See TPS data in Performance graphs"
page_server_performance_overview: "See Performance numbers"
page_server_player_versus: "See PvP & PvE -tab"
page_server_player_versus_kill_list: "See Player kill and death lists"
@@ -736,7 +805,7 @@ html:
bugreporters: "& Баг репортери!"
code: "автор коду"
donate: "Особлива подяка тим, хто надав фінансову підтримку."
- text: 'Крім того, дані чудові люди зробили свій внесок:'
+ text: "Крім того, дані <1>чудові люди1> зробили свій внесок:"
translator: "перекладач"
developer: "розроблено"
discord: "Загальна підтримка в Discord"
@@ -781,7 +850,7 @@ html:
text: "Грав між"
pluginGroup:
name: "Група: "
- text: "в ${plugin} ${group} Групах"
+ text: "в {{plugin}} {{group}} Групах"
registeredBetween:
text: "Зареєструвався між"
skipped: "Пропущено"
@@ -795,26 +864,26 @@ html:
are: "``"
label:
editQuery: "Edit Query"
- from: ">з"
+ from: "з"
makeAnother: "Зробити інший запит"
servers:
all: "використовуючи дані всіх серверів"
- many: "з використанням даних {number} серверів"
+ many: "з використанням даних{{number}}серверів"
single: "з використанням даних 1 сервера"
two: "з використанням даних 2 серверів"
showFullQuery: "Show Full Query"
- to: ">в"
+ to: "в"
view: "Показувати результат"
performQuery: "Виконати запит!"
results:
- match: "знайдено ${resultCount} гравців"
+ match: "знайдено {{resultCount}} гравців"
none: "Запит дав 0 результатів"
title: "Результати запиту"
title:
activity: "Активність обраних гравців"
- activityOnDate: 'Активність на '
+ activityOnDate: "Активність на {{activityDate}}"
sessionsWithinView: "Сеанси в межах видимості"
- text: "Запит<"
+ text: "Запит"
register:
completion: "Реєстрація завершена"
completion1: "Ви маєте закінчити реєстрацію користувача."
diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml
index 0c8005d75b..18cf4223bd 100644
--- a/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml
+++ b/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml
@@ -367,7 +367,7 @@ html:
activityIndexExample2: "非常活躍約為門檻的 2 倍(y ≥ 3.75)。"
activityIndexExample3: "指數會無限趨近於 5。"
activityIndexVisual: "這是 y=活躍指數,x=每週遊玩時間/門檻的曲線圖。"
- activityIndexWeek: "第 {} 週"
+ activityIndexWeek: "第 {{number}} 週"
examples: "範例"
graph:
labels: "可點擊下方標籤隱藏/顯示群組。"
@@ -564,6 +564,10 @@ html:
timeStep: "時間步長"
secondDeadliestWeapon: "第二致命的 PvP 武器"
seenNicknames: "使用過的暱稱"
+ select:
+ noOptions: "No options available"
+ select: "Select.."
+ selectSomeAddresses: "Select some addresses"
server: "伺服器"
serverAnalysis: "伺服器分析"
serverAsNumberse: "伺服器統計"
@@ -590,6 +594,60 @@ html:
showNofM: "顯示 {{n}} 筆,共 {{m}} 筆"
showPerPage: "每頁顯示"
visibleColumns: "可見欄位"
+ themeEditor:
+ addColor: "Add color"
+ addTheme: "Add theme"
+ alreadyExistsWarning: "Color with that name already exists - It will be overridden!"
+ basedOnTheme: "Based on theme"
+ canNotDeleteBuiltIn: "Note that you can not delete built-in themes, only the modifications you have made to them."
+ changes:
+ addColor: "Added {{name}} color {{color}}"
+ changeNightMode: "Changed night mode {{path}} to {{name}}"
+ changeNightModeArray: "Changed night mode {{path}} list"
+ changeUseCase: "Changed {{path}} to {{name}}"
+ changeUseCaseArray: "Changed {{path}} list"
+ deleteColor: "Deleted color {{name}}"
+ discardedChanges: "Discarded changes:"
+ removeNightMode: "Removed night mode override {{path}}"
+ renameColor: "Renamed {{previous}} to {{name}}, set color to {{color}}"
+ setColor: "Set {{name}} color to {{color}}"
+ colors: "Colors"
+ confirmDelete: "I confirm that I want to delete theme called '{{theme}}' and that this is an irreversible action."
+ defaultThemeNameFeedback: "Default theme can not be renamed. You can create a new theme based on Default instead."
+ deleteColors: "Delete colors"
+ deleteLocalTheme: "Delete theme (Only the locally stored one)"
+ deleteTheme: "Delete theme"
+ deleteThemes: "Delete themes"
+ downloadThemeBeforeDeleting: "Would you like to download the theme '{{theme}}' before deleting it?"
+ example: "Example"
+ failedToClone: "Failed to clone the original theme {{error}}"
+ finish: "Finish"
+ gradientWarning: "Gradients do not work with all elements."
+ hideHistory: "Hide history"
+ invalidName: "Name should be alphanumerical and must be unique. Max 100 characters."
+ issues:
+ missingNightCase: "Night mode {{name}} is missing color {{colorName}}"
+ missingUseCase: "Use case {{name}} is missing color {{colorName}}"
+ problems: "Problems"
+ lightModeInfo: "Theme editor uses light-mode to see fully saturated colors."
+ missing: "Missing color"
+ nameWarning: "A valid name that doesn't already exist is needed."
+ nightColors: "Night mode"
+ nightModeOverrides: "Night mode overrides"
+ noPermissionToDelete: "You don't have access rights for deleting non-local themes. You may need to delete them from the plugin folder."
+ openEditor: "Open editor"
+ redo: "Redo"
+ removeOverride: "Remove night mode override"
+ showHistory: "Show history"
+ themeColorOptions: "User theme color options"
+ themeName: "Theme name"
+ themeStoredOnlyLocally: "Theme is currently only in Browser local storage (Only you can see it)."
+ themeToDelete: "Theme to delete"
+ title: "Theme Editor"
+ undo: "Undo"
+ unsavedChanges: "There are unsaved changes - do you still want to leave the page?"
+ uploadTheme: "or Upload a previously downloaded theme:"
+ useCases: "Use cases"
themeSelect: "主題色選擇"
thirdDeadliestWeapon: "第三致命的 PvP 武器"
thirtyDays: "30 天"
@@ -625,6 +683,7 @@ html:
users: "管理使用者"
version: "版本"
veryActive: "非常活躍"
+ weapon: "Weapon"
weekComparison: "每週對比"
weekdays: "'星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'"
world: "世界載入"
@@ -657,7 +716,9 @@ html:
access_query: "允許存取 /query 及查詢結果頁面"
access_raw_player_data: "允許存取 /player/{uuid}/raw json 資料,遵循 'access.player' 權限"
access_server: "允許存取所有 /server 頁面"
+ access_theme_editor: "Allows accessing /theme-editor page"
manage_groups: "允許修改群組權限及存取 /manage/groups 頁面"
+ manage_themes: "Allows saving or deleting themes via theme-editor for everyone"
manage_users: "允許管理使用者所屬群組"
page: "控制頁面可見內容"
page_network: "檢視群組網路頁面全部內容"
@@ -714,6 +775,14 @@ html:
page_server_overview_players_online_graph: "檢視伺服器線上玩家圖表"
page_server_performance: "檢視伺服器效能分頁"
page_server_performance_graphs: "檢視伺服器效能圖表"
+ page_server_performance_graphs_chunks: "See Chunk count data in Performance graphs"
+ page_server_performance_graphs_cpu: "See CPU usage in Performance graphs"
+ page_server_performance_graphs_disk: "See Disk Space usage Performance graphs"
+ page_server_performance_graphs_entities: "See Entity count data in Performance graphs"
+ page_server_performance_graphs_ping: "See Ping data in Performance graphs"
+ page_server_performance_graphs_players_online: "See Players Online data in Performance graphs"
+ page_server_performance_graphs_ram: "See Memory usage in Performance graphs"
+ page_server_performance_graphs_tps: "See TPS data in Performance graphs"
page_server_performance_overview: "檢視伺服器效能統計"
page_server_player_versus: "檢視伺服器 PvP & PvE 分頁"
page_server_player_versus_kill_list: "檢視玩家擊殺與死亡列表"
@@ -736,7 +805,7 @@ html:
bugreporters: "和其他問題報告者!"
code: "代碼貢獻者"
donate: "特別感謝那些在經濟上支援開發的人們。"
- text: '以下 優秀人物 也做出了貢獻:'
+ text: "以下 <1>優秀人物1> 也做出了貢獻:"
translator: "翻譯者"
developer: "的開發者是"
discord: "一般問題支援:Discord"
@@ -781,7 +850,7 @@ html:
text: "在此期間遊玩過"
pluginGroup:
name: "小組:"
- text: "在 ${plugin} 插件的 ${group} 分組中"
+ text: "在 {{plugin}} 插件的 {{group}} 分組中"
registeredBetween:
text: "在此期間註冊"
skipped: "Skipped"
@@ -795,26 +864,26 @@ html:
are: "`是`"
label:
editQuery: "Edit Query"
- from: ">從 "
+ from: "從 "
makeAnother: "進行另一個查詢"
servers:
all: "using data of all servers"
- many: "using data of {number} servers"
+ many: "using data of{{number}}servers"
single: "using data of 1 server"
two: "using data of 2 servers"
showFullQuery: "Show Full Query"
- to: ">到 "
+ to: "到 "
view: "日期範圍"
performQuery: "執行查詢!"
results:
- match: "搜尋到 ${resultCount} 個玩家"
+ match: "搜尋到 {{resultCount}} 個玩家"
none: "查詢到 0 個結果"
title: "查詢結果"
title:
activity: "查詢玩家的活躍度"
- activityOnDate: '活躍在 '
+ activityOnDate: "活躍在 {{activityDate}}"
sessionsWithinView: "查看範圍內的會話"
- text: "查詢<"
+ text: "查詢"
register:
completion: "註冊完成"
completion1: "您現在可以完成使用者註冊流程。"
diff --git a/Plan/common/src/main/resources/assets/plan/themes/default.json b/Plan/common/src/main/resources/assets/plan/themes/default.json
new file mode 100644
index 0000000000..7aeef730bd
--- /dev/null
+++ b/Plan/common/src/main/resources/assets/plan/themes/default.json
@@ -0,0 +1,489 @@
+{
+ "name": "default",
+ "colors": {
+ "black": "#555555",
+ "white": "#ffffff",
+ "text-dark": "#fff",
+ "text-light": "#333",
+ "white-15-percent": "rgba(255,255,255,0.15)",
+ "black-10-percent": "rgba(0, 0, 0, 0.1)",
+ "plan": "#368F17",
+ "red": "#F44336",
+ "pink": "#E91E63",
+ "purple": "#9C27B0",
+ "deep-purple": "#673AB7",
+ "indigo": "#3F51B5",
+ "blue": "#2196F3",
+ "light-blue": "#03A9F4",
+ "cyan": "#00BCD4",
+ "teal": "#009688",
+ "green": "#4CAF50",
+ "light-green": "#8BC34A",
+ "lime": "#CDDC39",
+ "yellow": "#ffe821",
+ "amber": "#FFC107",
+ "orange": "#FF9800",
+ "deep-orange": "#FF5722",
+ "brown": "#795548",
+ "blue-grey": "#607D8B",
+ "yellow-15-percent": "rgba(255, 220, 40, 0.15)",
+ "cpu-yellow": "#e0d264",
+ "ram-green": "#7dcc24",
+ "chunk-brown": "#b58310",
+ "entity-purple": "#ac69ef",
+ "bright-blue": "#1E90FF",
+ "ping-amber": "#ffd54f",
+ "map-green": "#EEFFEE",
+ "high-green": "#267F00",
+ "medium-yellow": "#e5cc12",
+ "low-red": "#b74343",
+ "alert-success": "#d2f4e8",
+ "alert-warning": "#fdf3d8",
+ "alert-danger": "#fadbd8",
+ "success": "#1CC88A",
+ "warning": "#F6C23E",
+ "danger": "#e74A3B",
+ "alert-success-text": "#0f6848",
+ "alert-warning-text": "#806520",
+ "alert-danger-text": "#78261f",
+ "secondary": "#6c757d",
+ "cool-grey": "#6e707e",
+ "grey": "#9E9E9E",
+ "ink": "#222222",
+ "dark-slate": "#212529",
+ "medium-slate": "#3a3b45",
+ "table-slate": "#44454e",
+ "border-slate": "#4b4d5a",
+ "light-slate": "#5a5c69",
+ "light-grey": "#dddddd",
+ "table-grey": "#f3f3f3",
+ "white-grey": "#f8f9fc",
+ "pale-grey": "#eaecf4",
+ "cement-grey": "#e3e6f0",
+ "border-grey": "#dddfeb",
+ "pie-0": "#0099C6",
+ "pie-1": "#66AA00",
+ "pie-2": "#316395",
+ "pie-3": "#994499",
+ "pie-4": "#22AA99",
+ "pie-5": "#AAAA11",
+ "pie-6": "#6633CC",
+ "pie-7": "#E67300",
+ "pie-8": "#329262",
+ "pie-9": "#5574A6",
+ "drilldown-0": "#438c99",
+ "drilldown-1": "#639A67",
+ "drilldown-2": "#D8EBB5",
+ "drilldown-3": "#D9BF77"
+ },
+ "nightColors": {
+ "night-black": "#282a36",
+ "night-dark-blue": "#44475a",
+ "night-blue": "#6272a4",
+ "night-grey-blue": "#646e8c",
+ "night-dark-grey-blue": "#606270",
+ "night-text": "#eee8d5",
+ "night-text-50-percent": "rgba(238, 232, 213, 0.5)"
+ },
+ "useCases": {
+ "themeColorOptions": [
+ "theme",
+ "red",
+ "pink",
+ "purple",
+ "deep-purple",
+ "indigo",
+ "blue",
+ "light-blue",
+ "cyan",
+ "teal",
+ "green",
+ "light-green",
+ "lime",
+ "yellow",
+ "amber",
+ "orange",
+ "deep-orange",
+ "brown",
+ "grey",
+ "blue-grey"
+ ],
+ "referenceColors": {
+ "theme": "var(--color-plan)",
+ "themeText": "var(--color-theme)",
+ "text": "var(--color-text-light)"
+ },
+ "layout": {
+ "background": "var(--color-white-grey)",
+ "title": "var(--color-light-slate)",
+ "divider": "var(--color-black-10-percent)",
+ "helpIcon": "var(--color-light-blue)",
+ "loader": {
+ "border": "var(--color-theme)",
+ "background": "var(--color-theme)"
+ }
+ },
+ "sidebar": {
+ "background": "var(--color-theme)",
+ "text": "var(--color-text-dark)",
+ "divider": "var(--color-white-15-percent)",
+ "collapsibleSection": {
+ "background": "var(--color-white)",
+ "text": "var(--color-text)",
+ "hover": "var(--color-pale-grey)",
+ "border": "var(--color-pale-grey)"
+ },
+ "navigationItem": {
+ "background": "var(--color-theme)",
+ "icon": "var(--color-text-dark)"
+ }
+ },
+ "cards": {
+ "background": "var(--color-white)",
+ "border": "var(--color-cement-grey)",
+ "header": {
+ "background": "var(--color-white-grey)",
+ "border": "var(--color-cement-grey)"
+ }
+ },
+ "tabs": {
+ "background": "var(--color-white)",
+ "selected": "var(--color-white)",
+ "border": "var(--color-border-grey)"
+ },
+ "infoBox": {
+ "info": "var(--color-alert-success)",
+ "infoText": "var(--color-alert-success-text)",
+ "notice": "var(--color-alert-warning)",
+ "noticeText": "var(--color-alert-warning-text)",
+ "error": "var(--color-alert-danger)",
+ "errorText": "var(--color-alert-danger-text)"
+ },
+ "calendar": {
+ "today": "var(--color-yellow-15-percent)",
+ "border": "var(--color-cement-grey)",
+ "popover": {
+ "body": "var(--color-white)"
+ }
+ },
+ "tables": {
+ "text": "var(--color-text)",
+ "coloredHeaderText": "var(--color-text-dark)",
+ "oddRow": "var(--color-table-grey)",
+ "evenRow": "var(--color-white)",
+ "border": "var(--color-border-grey)"
+ },
+ "forms": {
+ "buttons": {
+ "actionButton": "var(--color-theme)",
+ "dangerousButton": "var(--color-danger)",
+ "secondaryActionButton": "var(--color-grey)",
+ "outlineButtonBorder": "var(--color-secondary)"
+ },
+ "input": {
+ "background": "var(--color-white)",
+ "border": "var(--color-border-grey)",
+ "text": "var(--color-cool-grey)"
+ },
+ "dropdown": {
+ "hover": "var(--color-white-grey)"
+ },
+ "multiSelect": {
+ "itemBackground": "var(--color-pale-grey)"
+ },
+ "checkbox": {
+ "checked": "var(--color-theme)"
+ }
+ },
+ "graphs": {
+ "style": {
+ "gridLine": "var(--color-cement-grey)",
+ "minorGridLine": "var(--color-black)",
+ "border": "var(--color-light-grey)",
+ "tooltipBackground": "var(--color-white-grey)",
+ "selectorButton": {
+ "background": "var(--color-white-grey)",
+ "hover": "var(--color-cement-grey)",
+ "selected": "var(--color-cement-grey)"
+ },
+ "selectorTextInput": {
+ "background": "var(--color-white-grey)",
+ "border": "var(--color-light-grey)"
+ },
+ "selectorRange": {
+ "handle": {
+ "background": "var(--color-white-grey)",
+ "border": "var(--color-grey)"
+ },
+ "outline": "var(--color-light-grey)",
+ "selectedArea": "var(--color-bright-blue)",
+ "seriesLine": "var(--color-bright-blue)"
+ },
+ "scrollbar": {
+ "decoration": "var(--color-text)",
+ "barBackground": "var(--color-light-grey)",
+ "buttonBackground": "var(--color-cement-grey)",
+ "trackBackground": "var(--color-white-grey)"
+ }
+ },
+ "punchCard": "var(--color-ink)",
+ "playersOnline": "var(--color-bright-blue)",
+ "tps": {
+ "high": "var(--color-high-green)",
+ "medium": "var(--color-medium-yellow)",
+ "low": "var(--color-low-red)"
+ },
+ "cpu": "var(--color-cpu-yellow)",
+ "ram": "var(--color-ram-green)",
+ "chunks": "var(--color-chunk-brown)",
+ "entities": "var(--color-entity-purple)",
+ "disk": {
+ "high": "var(--color-high-green)",
+ "medium": "var(--color-medium-yellow)",
+ "low": "var(--color-low-red)"
+ },
+ "ping": {
+ "max": "var(--color-amber)",
+ "avg": "var(--color-warning)",
+ "min": "var(--color-ping-amber)"
+ },
+ "worldMap": {
+ "high": "var(--color-high-green)",
+ "low": "var(--color-map-green)",
+ "bars": "var(--color-green)"
+ },
+ "pie": {
+ "colors": [
+ "pie-0",
+ "pie-1",
+ "pie-2",
+ "pie-3",
+ "pie-4",
+ "pie-5",
+ "pie-6",
+ "pie-7",
+ "pie-8",
+ "pie-9"
+ ],
+ "drilldown": [
+ "drilldown-0",
+ "drilldown-1",
+ "drilldown-2",
+ "drilldown-3"
+ ]
+ }
+ },
+ "data": {
+ "servers": "var(--color-light-green)",
+ "trend": {
+ "better": "var(--color-success)",
+ "same": "var(--color-warning)",
+ "worse": "var(--color-danger)"
+ },
+ "play": {
+ "playtime": "var(--color-green)",
+ "playtimeActive": "var(--color-green)",
+ "playtimeAfk": "var(--color-grey)",
+ "sessions": "var(--color-teal)",
+ "sessionLength": "var(--color-teal)",
+ "gamemode": "var(--color-teal)",
+ "firstSeen": "var(--color-light-green)",
+ "lastSeen": "var(--color-teal)"
+ },
+ "players": {
+ "count": "var(--color-black)",
+ "online": "var(--color-blue)",
+ "unique": "var(--color-light-blue)",
+ "new": "var(--color-light-green)",
+ "activityIndex": "var(--color-amber)",
+ "veryActive": "var(--color-green)",
+ "active": "var(--color-light-green)",
+ "regular": "var(--color-lime)",
+ "irregular": "var(--color-amber)",
+ "inactive": "var(--color-blue-grey)"
+ },
+ "playerPeakLast": "var(--color-light-blue)",
+ "playerPeakAllTime": "var(--color-light-green)",
+ "performance": {
+ "uptime": "var(--color-light-green)",
+ "downtime": "var(--color-red)",
+ "tps": "var(--color-red)",
+ "tpsLowSpikes": "var(--color-red)",
+ "tpsAverage": "var(--color-orange)",
+ "cpu": "var(--color-amber)",
+ "ram": "var(--color-light-green)",
+ "entities": "var(--color-purple)",
+ "chunks": "var(--color-blue-grey)",
+ "disk": "var(--color-green)",
+ "ping": "var(--color-amber)"
+ },
+ "calculated": {
+ "insights": "var(--color-red)",
+ "joinAddresses": "var(--color-amber)",
+ "retention": "var(--color-indigo)",
+ "retentionNewPlayers": "var(--color-light-green)",
+ "geolocation": "var(--color-green)",
+ "allowList": "var(--color-orange)",
+ "pluginVersions": "var(--color-indigo)"
+ },
+ "playerVersus": {
+ "playerKills": "var(--color-red)",
+ "mobKills": "var(--color-green)",
+ "deaths": "var(--color-black)",
+ "top-3": {
+ "first": "var(--color-amber)",
+ "second": "var(--color-grey)",
+ "third": "var(--color-brown)"
+ }
+ },
+ "playerStatus": {
+ "online": "var(--color-green)",
+ "offline": "var(--color-red)",
+ "banned": "var(--color-red)",
+ "operator": "var(--color-blue)",
+ "kicks": "var(--color-brown)",
+ "nicknames": "var(--color-purple)"
+ }
+ },
+ "plugin": {
+ "red": "var(--color-red)",
+ "pink": "var(--color-pink)",
+ "purple": "var(--color-purple)",
+ "deepPurple": "var(--color-deep-purple)",
+ "indigo": "var(--color-indigo)",
+ "blue": "var(--color-blue)",
+ "lightBlue": "var(--color-light-blue)",
+ "cyan": "var(--color-cyan)",
+ "teal": "var(--color-teal)",
+ "green": "var(--color-green)",
+ "lightGreen": "var(--color-light-green)",
+ "lime": "var(--color-lime)",
+ "yellow": "var(--color-yellow)",
+ "amber": "var(--color-amber)",
+ "orange": "var(--color-orange)",
+ "deepOrange": "var(--color-deep-orange)",
+ "brown": "var(--color-brown)",
+ "grey": "var(--color-grey)",
+ "blueGrey": "var(--color-blue-grey)",
+ "black": "var(--color-black)"
+ }
+ },
+ "nightModeUseCases": {
+ "themeColorOptions": [
+ "theme"
+ ],
+ "referenceColors": {
+ "theme": "var(--color-night-dark-blue)",
+ "themeText": "var(--color-plan)",
+ "text": "var(--color-night-text)"
+ },
+ "layout": {
+ "background": "var(--color-night-black)",
+ "divider": "var(--color-night-blue)",
+ "title": "var(--color-text)",
+ "loader": {
+ "border": "var(--color-plan)",
+ "background": "var(--color-plan)"
+ }
+ },
+ "sidebar": {
+ "text": "var(--color-text)",
+ "divider": "var(--color-night-blue)",
+ "navigationItem": {
+ "icon": "var(--color-text)"
+ },
+ "collapsibleSection": {
+ "background": "var(--color-theme)",
+ "hover": "var(--color-night-dark-grey-blue)",
+ "border": "var(--color-night-blue)"
+ }
+ },
+ "cards": {
+ "background": "var(--color-night-dark-blue)",
+ "border": "var(--color-night-blue)",
+ "header": {
+ "background": "var(--color-night-dark-blue)",
+ "border": "var(--color-night-blue)"
+ }
+ },
+ "tabs": {
+ "background": "var(--color-night-dark-blue)",
+ "selected": "var(--color-night-dark-blue)",
+ "border": "var(--color-night-blue)"
+ },
+ "tables": {
+ "coloredHeaderText": "var(--color-text)",
+ "oddRow": "var(--color-table-slate)",
+ "evenRow": "var(--color-medium-slate)",
+ "border": "var(--color-border-slate)"
+ },
+ "forms": {
+ "buttons": {
+ "actionButton": "var(--color-plan)",
+ "outlineButtonBorder": "var(--color-text)"
+ },
+ "input": {
+ "background": "var(--color-night-dark-blue)",
+ "border": "var(--color-night-blue)",
+ "text": "var(--color-text)"
+ },
+ "multiSelect": {
+ "itemBackground": "var(--color-night-dark-grey-blue)"
+ },
+ "checkbox": {
+ "checked": "var(--color-plan)"
+ },
+ "dropdown": {
+ "hover": "var(--color-night-dark-grey-blue)"
+ }
+ },
+ "calendar": {
+ "today": "var(--color-night-grey-blue)",
+ "border": "var(--color-night-blue)",
+ "popover": {
+ "body": "var(--color-night-dark-blue)"
+ }
+ },
+ "graphs": {
+ "style": {
+ "gridLine": "var(--color-night-dark-grey-blue)",
+ "minorGridLine": "var(--color-black)",
+ "border": "var(--color-night-dark-grey-blue)",
+ "tooltipBackground": "var(--color-night-dark-blue)",
+ "selectorButton": {
+ "background": "var(--color-light-slate)",
+ "hover": "var(--color-night-blue)",
+ "selected": "var(--color-night-blue)"
+ },
+ "selectorTextInput": {
+ "background": "var(--color-theme)",
+ "border": "var(--color-night-dark-grey-blue)"
+ },
+ "selectorRange": {
+ "handle": {
+ "background": "var(--color-light-slate)",
+ "border": "var(--color-grey)"
+ },
+ "outline": "var(--color-grey)",
+ "selectedArea": "var(--color-white)",
+ "seriesLine": "var(--color-bright-blue)"
+ },
+ "scrollbar": {
+ "decoration": "var(--color-text)",
+ "barBackground": "var(--color-night-grey-blue)",
+ "buttonBackground": "var(--color-grey)",
+ "trackBackground": "var(--color-medium-slate)"
+ }
+ },
+ "punchCard": "var(--color-text)"
+ },
+ "data": {
+ "players": {
+ "count": "var(--color-text)"
+ },
+ "playerVersus": {
+ "deaths": "var(--color-text)"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plan/common/src/main/resources/assets/plan/themes/high-contrast.json b/Plan/common/src/main/resources/assets/plan/themes/high-contrast.json
new file mode 100644
index 0000000000..45dd1ec72c
--- /dev/null
+++ b/Plan/common/src/main/resources/assets/plan/themes/high-contrast.json
@@ -0,0 +1,485 @@
+{
+ "name": "high-contrast",
+ "colors": {
+ "black": "#000",
+ "white": "#ffffff",
+ "text-dark": "#fff",
+ "text-light": "#000",
+ "white-50-percent": "rgba(255,255,255,0.50)",
+ "black-10-percent": "rgba(0, 0, 0, 0.1)",
+ "plan": "#208100",
+ "red": "#ff1000",
+ "pink": "#ff0059",
+ "purple": "#a800c4",
+ "deep-purple": "#4a00ca",
+ "indigo": "#0827c6",
+ "blue": "#0088f3",
+ "light-blue": "#00d0ff",
+ "cyan": "#00e1ff",
+ "teal": "#00bcaa",
+ "green": "#00c406",
+ "light-green": "#73d600",
+ "lime": "#cae400",
+ "yellow": "#ffe821",
+ "amber": "#FFC107",
+ "orange": "#FF9800",
+ "deep-orange": "#FF5722",
+ "brown": "#7a4235",
+ "blue-grey": "#355c70",
+ "yellow-30-percent": "rgba(255, 220, 40, 0.30)",
+ "cpu-yellow": "#f3d700",
+ "ram-green": "#2cde00",
+ "chunk-brown": "#a36d10",
+ "entity-purple": "#8f3ced",
+ "bright-blue": "#0081ff",
+ "ping-amber": "#ffce2f",
+ "map-green": "#EEFFEE",
+ "high-green": "#267F00",
+ "medium-yellow": "#e5cc12",
+ "low-red": "#b74343",
+ "alert-success": "#d2f4e8",
+ "alert-warning": "#fdf3d8",
+ "alert-danger": "#fadbd8",
+ "success": "#1CC88A",
+ "warning": "#F6C23E",
+ "danger": "#e74A3B",
+ "secondary": "#6c757d",
+ "cool-grey": "#6e707e",
+ "grey": "#9E9E9E",
+ "ink": "#222222",
+ "dark-slate": "#212529",
+ "medium-slate": "#3a3b45",
+ "table-slate": "#44454e",
+ "border-slate": "#4b4d5a",
+ "light-slate": "#5a5c69",
+ "light-grey": "#dddddd",
+ "table-grey": "#f3f3f3",
+ "white-grey": "#f8f9fc",
+ "pale-grey": "#eaecf4",
+ "cement-grey": "#e3e6f0",
+ "border-grey": "#dddfeb",
+ "pie-0": "#0099C6",
+ "pie-1": "#66AA00",
+ "pie-2": "#316395",
+ "pie-3": "#994499",
+ "pie-4": "#22AA99",
+ "pie-5": "#AAAA11",
+ "pie-6": "#6633CC",
+ "pie-7": "#E67300",
+ "pie-8": "#329262",
+ "pie-9": "#5574A6",
+ "drilldown-0": "#00a4cc",
+ "drilldown-1": "#458549",
+ "drilldown-2": "#d3ff84",
+ "drilldown-3": "#ffcb3c"
+ },
+ "nightColors": {
+ "night-black": "#000000",
+ "night-dark-blue": "#121212",
+ "night-blue": "#6272a4",
+ "night-grey-blue": "#646e8c",
+ "night-dark-grey-blue": "#232429",
+ "night-text": "#fff",
+ "night-text-50-percent": "rgba(238, 232, 213, 0.5)"
+ },
+ "useCases": {
+ "themeColorOptions": [
+ "theme",
+ "red",
+ "pink",
+ "purple",
+ "deep-purple",
+ "indigo",
+ "blue",
+ "light-blue",
+ "cyan",
+ "teal",
+ "green",
+ "light-green",
+ "lime",
+ "yellow",
+ "amber",
+ "orange",
+ "deep-orange",
+ "brown",
+ "grey",
+ "blue-grey"
+ ],
+ "referenceColors": {
+ "theme": "var(--color-plan)",
+ "themeText": "var(--color-theme)",
+ "text": "var(--color-text-light)"
+ },
+ "layout": {
+ "background": "var(--color-white)",
+ "title": "var(--color-text)",
+ "divider": "var(--color-black)",
+ "helpIcon": "var(--color-bright-blue)",
+ "loader": {
+ "border": "var(--color-theme)",
+ "background": "var(--color-theme)"
+ }
+ },
+ "sidebar": {
+ "background": "var(--color-theme)",
+ "text": "var(--color-text-dark)",
+ "divider": "var(--color-white-50-percent)",
+ "collapsibleSection": {
+ "background": "var(--color-white)",
+ "text": "var(--color-text)",
+ "hover": "var(--color-light-grey)",
+ "border": "var(--color-pale-grey)"
+ },
+ "navigationItem": {
+ "background": "var(--color-theme)",
+ "icon": "var(--color-text-dark)"
+ }
+ },
+ "cards": {
+ "background": "var(--color-white)",
+ "border": "var(--color-cement-grey)",
+ "header": {
+ "background": "var(--color-white)",
+ "border": "var(--color-black)"
+ }
+ },
+ "tabs": {
+ "background": "var(--color-white)",
+ "selected": "var(--color-white)",
+ "border": "var(--color-border-grey)"
+ },
+ "infoBox": {
+ "info": "var(--color-alert-success)",
+ "infoText": "var(--color-black)",
+ "notice": "var(--color-alert-warning)",
+ "noticeText": "var(--color-black)",
+ "error": "var(--color-alert-danger)",
+ "errorText": "var(--color-black)"
+ },
+ "calendar": {
+ "today": "var(--color-yellow-30-percent)",
+ "border": "var(--color-black)",
+ "popover": {
+ "body": "var(--color-white)"
+ }
+ },
+ "tables": {
+ "text": "var(--color-text)",
+ "coloredHeaderText": "var(--color-text-dark)",
+ "oddRow": "var(--color-table-grey)",
+ "evenRow": "var(--color-white)",
+ "border": "var(--color-black)"
+ },
+ "forms": {
+ "buttons": {
+ "actionButton": "var(--color-theme)",
+ "dangerousButton": "var(--color-danger)",
+ "secondaryActionButton": "var(--color-medium-yellow)",
+ "outlineButtonBorder": "var(--color-black)"
+ },
+ "input": {
+ "background": "var(--color-white)",
+ "border": "var(--color-black)",
+ "text": "var(--color-black)"
+ },
+ "dropdown": {
+ "hover": "var(--color-light-grey)"
+ },
+ "multiSelect": {
+ "itemBackground": "var(--color-table-grey)"
+ },
+ "checkbox": {
+ "checked": "var(--color-theme)"
+ }
+ },
+ "graphs": {
+ "style": {
+ "gridLine": "var(--color-black)",
+ "minorGridLine": "var(--color-black)",
+ "border": "var(--color-light-grey)",
+ "tooltipBackground": "var(--color-white)",
+ "selectorButton": {
+ "background": "var(--color-light-grey)",
+ "hover": "var(--color-theme)",
+ "selected": "var(--color-theme)"
+ },
+ "selectorTextInput": {
+ "background": "var(--color-white-grey)",
+ "border": "var(--color-black)"
+ },
+ "selectorRange": {
+ "handle": {
+ "background": "var(--color-white-grey)",
+ "border": "var(--color-black)"
+ },
+ "outline": "var(--color-light-grey)",
+ "selectedArea": "var(--color-bright-blue)",
+ "seriesLine": "var(--color-bright-blue)"
+ },
+ "scrollbar": {
+ "decoration": "var(--color-white)",
+ "barBackground": "var(--color-theme)",
+ "buttonBackground": "var(--color-cement-grey)",
+ "trackBackground": "var(--color-light-grey)"
+ }
+ },
+ "punchCard": "var(--color-ink)",
+ "playersOnline": "var(--color-bright-blue)",
+ "tps": {
+ "high": "var(--color-high-green)",
+ "medium": "var(--color-medium-yellow)",
+ "low": "var(--color-low-red)"
+ },
+ "cpu": "var(--color-cpu-yellow)",
+ "ram": "var(--color-ram-green)",
+ "chunks": "var(--color-chunk-brown)",
+ "entities": "var(--color-entity-purple)",
+ "disk": {
+ "high": "var(--color-high-green)",
+ "medium": "var(--color-medium-yellow)",
+ "low": "var(--color-low-red)"
+ },
+ "ping": {
+ "max": "var(--color-deep-orange)",
+ "avg": "var(--color-warning)",
+ "min": "var(--color-success)"
+ },
+ "worldMap": {
+ "high": "var(--color-high-green)",
+ "low": "var(--color-white)",
+ "bars": "var(--color-blue)"
+ },
+ "pie": {
+ "colors": [
+ "pie-0",
+ "pie-1",
+ "pie-2",
+ "pie-3",
+ "pie-4",
+ "pie-5",
+ "pie-6",
+ "pie-7",
+ "pie-8",
+ "pie-9"
+ ],
+ "drilldown": [
+ "drilldown-0",
+ "drilldown-1",
+ "drilldown-2",
+ "drilldown-3"
+ ]
+ }
+ },
+ "data": {
+ "servers": "var(--color-light-green)",
+ "trend": {
+ "better": "var(--color-success)",
+ "same": "var(--color-warning)",
+ "worse": "var(--color-danger)"
+ },
+ "play": {
+ "playtime": "var(--color-green)",
+ "playtimeActive": "var(--color-green)",
+ "playtimeAfk": "var(--color-grey)",
+ "sessions": "var(--color-teal)",
+ "sessionLength": "var(--color-teal)",
+ "gamemode": "var(--color-teal)",
+ "firstSeen": "var(--color-light-green)",
+ "lastSeen": "var(--color-teal)"
+ },
+ "players": {
+ "count": "var(--color-black)",
+ "online": "var(--color-blue)",
+ "unique": "var(--color-light-blue)",
+ "new": "var(--color-light-green)",
+ "activityIndex": "var(--color-amber)",
+ "veryActive": "var(--color-green)",
+ "active": "var(--color-light-green)",
+ "regular": "var(--color-lime)",
+ "irregular": "var(--color-amber)",
+ "inactive": "var(--color-blue-grey)"
+ },
+ "playerPeakLast": "var(--color-light-blue)",
+ "playerPeakAllTime": "var(--color-light-green)",
+ "performance": {
+ "uptime": "var(--color-light-green)",
+ "downtime": "var(--color-red)",
+ "tps": "var(--color-red)",
+ "tpsLowSpikes": "var(--color-red)",
+ "tpsAverage": "var(--color-orange)",
+ "cpu": "var(--color-amber)",
+ "ram": "var(--color-light-green)",
+ "entities": "var(--color-purple)",
+ "chunks": "var(--color-blue-grey)",
+ "disk": "var(--color-green)",
+ "ping": "var(--color-amber)"
+ },
+ "calculated": {
+ "insights": "var(--color-red)",
+ "joinAddresses": "var(--color-amber)",
+ "retention": "var(--color-indigo)",
+ "retentionNewPlayers": "var(--color-light-green)",
+ "geolocation": "var(--color-green)",
+ "allowList": "var(--color-orange)",
+ "pluginVersions": "var(--color-indigo)"
+ },
+ "playerVersus": {
+ "playerKills": "var(--color-red)",
+ "mobKills": "var(--color-green)",
+ "deaths": "var(--color-black)",
+ "top-3": {
+ "first": "var(--color-amber)",
+ "second": "var(--color-grey)",
+ "third": "var(--color-brown)"
+ }
+ },
+ "playerStatus": {
+ "online": "var(--color-green)",
+ "offline": "var(--color-red)",
+ "banned": "var(--color-red)",
+ "operator": "var(--color-blue)",
+ "kicks": "var(--color-brown)",
+ "nicknames": "var(--color-purple)"
+ }
+ },
+ "plugin": {
+ "red": "var(--color-red)",
+ "pink": "var(--color-pink)",
+ "purple": "var(--color-purple)",
+ "deepPurple": "var(--color-deep-purple)",
+ "indigo": "var(--color-indigo)",
+ "blue": "var(--color-blue)",
+ "lightBlue": "var(--color-light-blue)",
+ "cyan": "var(--color-cyan)",
+ "teal": "var(--color-teal)",
+ "green": "var(--color-green)",
+ "lightGreen": "var(--color-light-green)",
+ "lime": "var(--color-lime)",
+ "yellow": "var(--color-yellow)",
+ "amber": "var(--color-amber)",
+ "orange": "var(--color-orange)",
+ "deepOrange": "var(--color-deep-orange)",
+ "brown": "var(--color-brown)",
+ "grey": "var(--color-grey)",
+ "blueGrey": "var(--color-blue-grey)",
+ "black": "var(--color-black)"
+ }
+ },
+ "nightModeUseCases": {
+ "themeColorOptions": [
+ "theme"
+ ],
+ "referenceColors": {
+ "theme": "var(--color-night-dark-blue)",
+ "themeText": "var(--color-plan)",
+ "text": "var(--color-night-text)"
+ },
+ "layout": {
+ "background": "var(--color-night-black)",
+ "divider": "var(--color-white)",
+ "title": "var(--color-text)",
+ "loader": {
+ "border": "var(--color-plan)",
+ "background": "var(--color-plan)"
+ }
+ },
+ "sidebar": {
+ "text": "var(--color-text)",
+ "navigationItem": {
+ "icon": "var(--color-text)"
+ },
+ "collapsibleSection": {
+ "background": "var(--color-theme)",
+ "hover": "var(--color-night-dark-grey-blue)",
+ "border": "var(--color-night-blue)"
+ }
+ },
+ "cards": {
+ "background": "var(--color-night-dark-blue)",
+ "border": "var(--color-night-blue)",
+ "header": {
+ "background": "var(--color-night-dark-blue)",
+ "border": "var(--color-night-blue)"
+ }
+ },
+ "tabs": {
+ "background": "var(--color-night-dark-blue)",
+ "selected": "var(--color-night-dark-blue)",
+ "border": "var(--color-night-blue)"
+ },
+ "tables": {
+ "coloredHeaderText": "var(--color-text)",
+ "oddRow": "var(--color-table-slate)",
+ "evenRow": "var(--color-dark-slate)",
+ "border": "var(--color-black)"
+ },
+ "forms": {
+ "buttons": {
+ "actionButton": "var(--color-plan)",
+ "outlineButtonBorder": "var(--color-text)"
+ },
+ "input": {
+ "background": "var(--color-night-dark-blue)",
+ "border": "var(--color-white)",
+ "text": "var(--color-text)"
+ },
+ "multiSelect": {
+ "itemBackground": "var(--color-night-dark-grey-blue)"
+ },
+ "checkbox": {
+ "checked": "var(--color-plan)"
+ },
+ "dropdown": {
+ "hover": "var(--color-night-dark-grey-blue)"
+ }
+ },
+ "calendar": {
+ "today": "var(--color-medium-slate)",
+ "border": "var(--color-night-blue)",
+ "popover": {
+ "body": "var(--color-night-dark-blue)"
+ }
+ },
+ "graphs": {
+ "style": {
+ "gridLine": "var(--color-white)",
+ "minorGridLine": "var(--color-white)",
+ "border": "var(--color-night-dark-grey-blue)",
+ "tooltipBackground": "var(--color-night-dark-blue)",
+ "selectorButton": {
+ "background": "var(--color-light-slate)",
+ "hover": "var(--color-indigo)",
+ "selected": "var(--color-indigo)"
+ },
+ "selectorTextInput": {
+ "background": "var(--color-theme)",
+ "border": "var(--color-white)"
+ },
+ "selectorRange": {
+ "handle": {
+ "background": "var(--color-night-black)",
+ "border": "var(--color-white)"
+ },
+ "outline": "var(--color-grey)",
+ "selectedArea": "var(--color-white)",
+ "seriesLine": "var(--color-bright-blue)"
+ },
+ "scrollbar": {
+ "decoration": "var(--color-text)",
+ "barBackground": "var(--color-indigo)",
+ "buttonBackground": "var(--color-grey)",
+ "trackBackground": "var(--color-light-slate)"
+ }
+ },
+ "punchCard": "var(--color-text)"
+ },
+ "data": {
+ "players": {
+ "count": "var(--color-text)"
+ },
+ "playerVersus": {
+ "deaths": "var(--color-text)"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plan/common/src/main/resources/assets/plan/themes/monochromatic.json b/Plan/common/src/main/resources/assets/plan/themes/monochromatic.json
new file mode 100644
index 0000000000..346792a1fb
--- /dev/null
+++ b/Plan/common/src/main/resources/assets/plan/themes/monochromatic.json
@@ -0,0 +1,491 @@
+{
+ "name": "monochromatic",
+ "colors": {
+ "black": "#555555",
+ "white": "#ffffff",
+ "text-dark": "#fff",
+ "text-light": "#333",
+ "white-15-percent": "rgba(255,255,255,0.15)",
+ "black-10-percent": "rgba(0, 0, 0, 0.1)",
+ "plan": "#368F17",
+ "red": "#F44336",
+ "pink": "#E91E63",
+ "purple": "#9C27B0",
+ "deep-purple": "#673AB7",
+ "indigo": "#3F51B5",
+ "blue": "#2196F3",
+ "light-blue": "#03A9F4",
+ "cyan": "#00BCD4",
+ "teal": "#009688",
+ "green": "#4CAF50",
+ "light-green": "#8BC34A",
+ "lime": "#CDDC39",
+ "yellow": "#ffe821",
+ "amber": "#FFC107",
+ "orange": "#FF9800",
+ "deep-orange": "#FF5722",
+ "brown": "#795548",
+ "blue-grey": "#607D8B",
+ "yellow-15-percent": "rgba(255, 220, 40, 0.15)",
+ "cpu-yellow": "#e0d264",
+ "ram-green": "#7dcc24",
+ "chunk-brown": "#b58310",
+ "entity-purple": "#ac69ef",
+ "bright-blue": "#1E90FF",
+ "ping-amber": "#ffd54f",
+ "map-green": "#EEFFEE",
+ "high-green": "#267F00",
+ "medium-yellow": "#e5cc12",
+ "low-red": "#b74343",
+ "alert-success": "#d2f4e8",
+ "alert-warning": "#fdf3d8",
+ "alert-danger": "#fadbd8",
+ "success": "#1CC88A",
+ "warning": "#F6C23E",
+ "danger": "#e74A3B",
+ "alert-success-text": "#0f6848",
+ "alert-warning-text": "#806520",
+ "alert-danger-text": "#78261f",
+ "secondary": "#6c757d",
+ "cool-grey": "#6e707e",
+ "grey": "#9E9E9E",
+ "ink": "#222222",
+ "dark-slate": "#212529",
+ "medium-slate": "#3a3b45",
+ "table-slate": "#44454e",
+ "border-slate": "#4b4d5a",
+ "light-slate": "#5a5c69",
+ "light-grey": "#dddddd",
+ "table-grey": "#f3f3f3",
+ "white-grey": "#f8f9fc",
+ "pale-grey": "#eaecf4",
+ "cement-grey": "#e3e6f0",
+ "border-grey": "#dddfeb",
+ "pie-0": "#0099C6",
+ "pie-1": "#66AA00",
+ "pie-2": "#316395",
+ "pie-3": "#994499",
+ "pie-4": "#22AA99",
+ "pie-5": "#AAAA11",
+ "pie-6": "#6633CC",
+ "pie-7": "#E67300",
+ "pie-8": "#329262",
+ "pie-9": "#5574A6",
+ "drilldown-0": "#438c99",
+ "drilldown-1": "#639A67",
+ "drilldown-2": "#D8EBB5",
+ "drilldown-3": "#D9BF77"
+ },
+ "nightColors": {
+ "night-black": "#282a36",
+ "night-dark-blue": "#44475a",
+ "night-blue": "#6272a4",
+ "night-grey-blue": "#646e8c",
+ "night-dark-grey-blue": "#606270",
+ "night-text": "#eee8d5",
+ "night-text-50-percent": "rgba(238, 232, 213, 0.5)"
+ },
+ "useCases": {
+ "themeColorOptions": [
+ "theme",
+ "red",
+ "pink",
+ "purple",
+ "deep-purple",
+ "indigo",
+ "blue",
+ "light-blue",
+ "cyan",
+ "teal",
+ "green",
+ "light-green",
+ "lime",
+ "yellow",
+ "amber",
+ "orange",
+ "deep-orange",
+ "brown",
+ "grey",
+ "blue-grey"
+ ],
+ "referenceColors": {
+ "theme": "var(--color-plan)",
+ "themeText": "var(--color-theme)",
+ "text": "var(--color-text-light)"
+ },
+ "layout": {
+ "background": "var(--color-white-grey)",
+ "title": "var(--color-light-slate)",
+ "divider": "var(--color-black-10-percent)",
+ "helpIcon": "var(--color-themeText)",
+ "loader": {
+ "border": "var(--color-theme)",
+ "background": "var(--color-theme)"
+ }
+ },
+ "sidebar": {
+ "background": "var(--color-theme)",
+ "text": "var(--color-text-dark)",
+ "divider": "var(--color-white-15-percent)",
+ "collapsibleSection": {
+ "background": "var(--color-white)",
+ "text": "var(--color-text)",
+ "hover": "var(--color-pale-grey)",
+ "border": "var(--color-pale-grey)"
+ },
+ "navigationItem": {
+ "background": "var(--color-theme)",
+ "icon": "var(--color-text-dark)"
+ }
+ },
+ "cards": {
+ "background": "var(--color-white)",
+ "border": "var(--color-cement-grey)",
+ "header": {
+ "background": "var(--color-white-grey)",
+ "border": "var(--color-cement-grey)"
+ }
+ },
+ "tabs": {
+ "background": "var(--color-white)",
+ "selected": "var(--color-white)",
+ "border": "var(--color-border-grey)"
+ },
+ "infoBox": {
+ "info": "var(--color-alert-success)",
+ "infoText": "var(--color-alert-success-text)",
+ "notice": "var(--color-alert-warning)",
+ "noticeText": "var(--color-alert-warning-text)",
+ "error": "var(--color-alert-danger)",
+ "errorText": "var(--color-alert-danger-text)"
+ },
+ "calendar": {
+ "today": "var(--color-yellow-15-percent)",
+ "border": "var(--color-cement-grey)",
+ "popover": {
+ "body": "var(--color-white)"
+ }
+ },
+ "tables": {
+ "text": "var(--color-text)",
+ "coloredHeaderText": "var(--color-text-dark)",
+ "oddRow": "var(--color-table-grey)",
+ "evenRow": "var(--color-white)",
+ "border": "var(--color-border-grey)"
+ },
+ "forms": {
+ "buttons": {
+ "actionButton": "var(--color-theme)",
+ "dangerousButton": "var(--color-theme)",
+ "secondaryActionButton": "var(--color-theme)",
+ "outlineButtonBorder": "var(--color-theme)"
+ },
+ "input": {
+ "background": "var(--color-white)",
+ "border": "var(--color-border-grey)",
+ "text": "var(--color-cool-grey)"
+ },
+ "dropdown": {
+ "hover": "var(--color-white-grey)"
+ },
+ "multiSelect": {
+ "itemBackground": "var(--color-pale-grey)"
+ },
+ "checkbox": {
+ "checked": "var(--color-theme)"
+ }
+ },
+ "graphs": {
+ "style": {
+ "gridLine": "var(--color-cement-grey)",
+ "minorGridLine": "var(--color-black)",
+ "border": "var(--color-light-grey)",
+ "tooltipBackground": "var(--color-white-grey)",
+ "selectorButton": {
+ "background": "var(--color-white-grey)",
+ "hover": "var(--color-cement-grey)",
+ "selected": "var(--color-cement-grey)"
+ },
+ "selectorTextInput": {
+ "background": "var(--color-white-grey)",
+ "border": "var(--color-light-grey)"
+ },
+ "selectorRange": {
+ "handle": {
+ "background": "var(--color-white-grey)",
+ "border": "var(--color-grey)"
+ },
+ "outline": "var(--color-light-grey)",
+ "selectedArea": "var(--color-theme)",
+ "seriesLine": "var(--color-theme)"
+ },
+ "scrollbar": {
+ "decoration": "var(--color-text)",
+ "barBackground": "var(--color-light-grey)",
+ "buttonBackground": "var(--color-cement-grey)",
+ "trackBackground": "var(--color-white-grey)"
+ }
+ },
+ "punchCard": "var(--color-ink)",
+ "playersOnline": "var(--color-theme)",
+ "tps": {
+ "high": "var(--color-high-green)",
+ "medium": "var(--color-medium-yellow)",
+ "low": "var(--color-low-red)"
+ },
+ "cpu": "var(--color-cpu-yellow)",
+ "ram": "var(--color-ram-green)",
+ "chunks": "var(--color-chunk-brown)",
+ "entities": "var(--color-entity-purple)",
+ "disk": {
+ "high": "var(--color-high-green)",
+ "medium": "var(--color-medium-yellow)",
+ "low": "var(--color-low-red)"
+ },
+ "ping": {
+ "max": "var(--color-amber)",
+ "avg": "var(--color-warning)",
+ "min": "var(--color-ping-amber)"
+ },
+ "worldMap": {
+ "high": "var(--color-high-green)",
+ "low": "var(--color-map-green)",
+ "bars": "var(--color-theme)"
+ },
+ "pie": {
+ "colors": [
+ "pie-0",
+ "pie-1",
+ "pie-2",
+ "pie-3",
+ "pie-4",
+ "pie-5",
+ "pie-6",
+ "pie-7",
+ "pie-8",
+ "pie-9"
+ ],
+ "drilldown": [
+ "drilldown-0",
+ "drilldown-1",
+ "drilldown-2",
+ "drilldown-3"
+ ]
+ }
+ },
+ "data": {
+ "servers": "var(--color-themeText)",
+ "trend": {
+ "better": "var(--color-themeText)",
+ "same": "var(--color-themeText)",
+ "worse": "var(--color-themeText)"
+ },
+ "play": {
+ "playtime": "var(--color-themeText)",
+ "playtimeActive": "var(--color-themeText)",
+ "playtimeAfk": "var(--color-themeText)",
+ "sessions": "var(--color-themeText)",
+ "sessionLength": "var(--color-themeText)",
+ "gamemode": "var(--color-themeText)",
+ "firstSeen": "var(--color-themeText)",
+ "lastSeen": "var(--color-themeText)"
+ },
+ "players": {
+ "count": "var(--color-themeText)",
+ "online": "var(--color-themeText)",
+ "unique": "var(--color-themeText)",
+ "new": "var(--color-themeText)",
+ "activityIndex": "var(--color-themeText)",
+ "veryActive": "var(--color-green)",
+ "active": "var(--color-light-green)",
+ "regular": "var(--color-lime)",
+ "irregular": "var(--color-amber)",
+ "inactive": "var(--color-blue-grey)"
+ },
+ "playerPeakLast": "var(--color-themeText)",
+ "playerPeakAllTime": "var(--color-themeText)",
+ "performance": {
+ "uptime": "var(--color-themeText)",
+ "downtime": "var(--color-themeText)",
+ "tps": "var(--color-themeText)",
+ "tpsLowSpikes": "var(--color-themeText)",
+ "tpsAverage": "var(--color-themeText)",
+ "cpu": "var(--color-themeText)",
+ "ram": "var(--color-themeText)",
+ "entities": "var(--color-themeText)",
+ "chunks": "var(--color-themeText)",
+ "disk": "var(--color-themeText)",
+ "ping": "var(--color-themeText)"
+ },
+ "calculated": {
+ "insights": "var(--color-themeText)",
+ "joinAddresses": "var(--color-themeText)",
+ "retention": "var(--color-themeText)",
+ "retentionNewPlayers": "var(--color-themeText)",
+ "geolocation": "var(--color-themeText)",
+ "allowList": "var(--color-themeText)",
+ "pluginVersions": "var(--color-themeText)"
+ },
+ "playerVersus": {
+ "playerKills": "var(--color-themeText)",
+ "mobKills": "var(--color-themeText)",
+ "deaths": "var(--color-themeText)",
+ "top-3": {
+ "first": "var(--color-themeText)",
+ "second": "var(--color-themeText)",
+ "third": "var(--color-themeText)"
+ }
+ },
+ "playerStatus": {
+ "online": "var(--color-themeText)",
+ "offline": "var(--color-grey)",
+ "banned": "var(--color-themeText)",
+ "operator": "var(--color-themeText)",
+ "kicks": "var(--color-themeText)",
+ "nicknames": "var(--color-themeText)"
+ }
+ },
+ "plugin": {
+ "red": "var(--color-themeText)",
+ "pink": "var(--color-themeText)",
+ "purple": "var(--color-themeText)",
+ "deepPurple": "var(--color-themeText)",
+ "indigo": "var(--color-themeText)",
+ "blue": "var(--color-themeText)",
+ "lightBlue": "var(--color-themeText)",
+ "cyan": "var(--color-themeText)",
+ "teal": "var(--color-themeText)",
+ "green": "var(--color-themeText)",
+ "lightGreen": "var(--color-themeText)",
+ "lime": "var(--color-themeText)",
+ "yellow": "var(--color-themeText)",
+ "amber": "var(--color-themeText)",
+ "orange": "var(--color-themeText)",
+ "deepOrange": "var(--color-themeText)",
+ "brown": "var(--color-themeText)",
+ "grey": "var(--color-themeText)",
+ "blueGrey": "var(--color-themeText)",
+ "black": "var(--color-themeText)"
+ }
+ },
+ "nightModeUseCases": {
+ "themeColorOptions": [
+ "theme",
+ "night-blue"
+ ],
+ "referenceColors": {
+ "theme": "var(--color-night-dark-blue)",
+ "themeText": "var(--color-night-blue)",
+ "text": "var(--color-night-text)"
+ },
+ "layout": {
+ "background": "var(--color-night-black)",
+ "divider": "var(--color-night-blue)",
+ "title": "var(--color-text)",
+ "loader": {
+ "border": "var(--color-themeText)",
+ "background": "var(--color-themeText)"
+ },
+ "helpIcon": "var(--color-themeText)"
+ },
+ "sidebar": {
+ "text": "var(--color-text)",
+ "divider": "var(--color-night-blue)",
+ "navigationItem": {
+ "icon": "var(--color-text)"
+ },
+ "collapsibleSection": {
+ "background": "var(--color-theme)",
+ "hover": "var(--color-night-dark-grey-blue)",
+ "border": "var(--color-night-blue)"
+ }
+ },
+ "cards": {
+ "background": "var(--color-night-dark-blue)",
+ "border": "var(--color-night-blue)",
+ "header": {
+ "background": "var(--color-night-dark-blue)",
+ "border": "var(--color-night-blue)"
+ }
+ },
+ "tabs": {
+ "background": "var(--color-night-dark-blue)",
+ "selected": "var(--color-night-dark-blue)",
+ "border": "var(--color-night-blue)"
+ },
+ "tables": {
+ "coloredHeaderText": "var(--color-text)",
+ "oddRow": "var(--color-table-slate)",
+ "evenRow": "var(--color-medium-slate)",
+ "border": "var(--color-border-slate)"
+ },
+ "forms": {
+ "buttons": {
+ "actionButton": "var(--color-themeText)",
+ "outlineButtonBorder": "var(--color-text)",
+ "dangerousButton": "var(--color-themeText)",
+ "secondaryActionButton": "var(--color-themeText)"
+ },
+ "input": {
+ "background": "var(--color-night-dark-blue)",
+ "border": "var(--color-night-blue)",
+ "text": "var(--color-text)"
+ },
+ "multiSelect": {
+ "itemBackground": "var(--color-night-dark-grey-blue)"
+ },
+ "checkbox": {
+ "checked": "var(--color-plan)"
+ },
+ "dropdown": {
+ "hover": "var(--color-night-dark-grey-blue)"
+ }
+ },
+ "calendar": {
+ "today": "var(--color-night-grey-blue)",
+ "border": "var(--color-night-blue)",
+ "popover": {
+ "body": "var(--color-night-dark-blue)"
+ }
+ },
+ "graphs": {
+ "style": {
+ "gridLine": "var(--color-night-dark-grey-blue)",
+ "minorGridLine": "var(--color-black)",
+ "border": "var(--color-night-dark-grey-blue)",
+ "tooltipBackground": "var(--color-night-dark-blue)",
+ "selectorButton": {
+ "background": "var(--color-light-slate)",
+ "hover": "var(--color-night-blue)",
+ "selected": "var(--color-night-blue)"
+ },
+ "selectorTextInput": {
+ "background": "var(--color-theme)",
+ "border": "var(--color-night-dark-grey-blue)"
+ },
+ "selectorRange": {
+ "handle": {
+ "background": "var(--color-light-slate)",
+ "border": "var(--color-grey)"
+ },
+ "outline": "var(--color-grey)",
+ "selectedArea": "var(--color-white)",
+ "seriesLine": "var(--color-night-blue)"
+ },
+ "scrollbar": {
+ "decoration": "var(--color-text)",
+ "barBackground": "var(--color-night-grey-blue)",
+ "buttonBackground": "var(--color-grey)",
+ "trackBackground": "var(--color-medium-slate)"
+ }
+ },
+ "punchCard": "var(--color-text)",
+ "worldMap": {
+ "bars": "var(--color-themeText)",
+ "high": "var(--color-night-blue)",
+ "low": "var(--color-grey)"
+ },
+ "playersOnline": "var(--color-themeText)"
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plan/common/src/main/resources/assets/plan/web/error.html b/Plan/common/src/main/resources/assets/plan/web/error.html
index 4f9a7b58c6..13b050d775 100644
--- a/Plan/common/src/main/resources/assets/plan/web/error.html
+++ b/Plan/common/src/main/resources/assets/plan/web/error.html
@@ -48,9 +48,6 @@
-
-
-
Logout
@@ -85,68 +82,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -160,7 +95,6 @@
-
diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/export/ExportTestUtilities.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/export/ExportTestUtilities.java
index 48ad74536c..b66b06ea44 100644
--- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/export/ExportTestUtilities.java
+++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/export/ExportTestUtilities.java
@@ -26,6 +26,7 @@
import com.djrapitops.plan.storage.database.transactions.events.StoreWorldNameTransaction;
import com.djrapitops.plan.utilities.java.Lists;
import org.apache.commons.lang3.StringUtils;
+import org.awaitility.Awaitility;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.NoSuchElementException;
@@ -34,7 +35,6 @@
import org.openqa.selenium.logging.LogEntry;
import org.openqa.selenium.logging.LogType;
import org.openqa.selenium.support.ui.WebDriverWait;
-import org.awaitility.Awaitility;
import utilities.RandomData;
import utilities.TestConstants;
@@ -105,7 +105,7 @@ public static List getLogsAfterRequestToAddress(ChromeDriver driver, S
driver.get(address);
new WebDriverWait(driver, Duration.of(10, ChronoUnit.SECONDS)).until(
- webDriver -> ((JavascriptExecutor) webDriver).executeScript("return document.readyState").equals("complete"));
+ webDriver -> "complete".equals(((JavascriptExecutor) webDriver).executeScript("return document.readyState")));
assertFalse(driver.findElement(By.tagName("body")).getText().contains("Bad Gateway"), "502 Bad Gateway, nginx could not reach Plan");
@@ -141,6 +141,9 @@ public static List getEndpointsToTest(ServerUUID serverUUID) {
.addAll(ServerPageExporter.getRedirections(serverUUID))
.addAll(PlayerPageExporter.getRedirections(TestConstants.PLAYER_ONE_UUID))
.add("/players")
+ .add("/theme-editor")
+ .add("/theme-editor/new")
+ .add("/theme-editor/delete")
.build();
}
diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/export/ReactExporterTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/export/ReactExporterTest.java
index e05f0f3e20..19a090c9d7 100644
--- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/export/ReactExporterTest.java
+++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/export/ReactExporterTest.java
@@ -68,6 +68,26 @@ void allReactFilesAreExported(PlanConfig config, Exporter exporter) throws Expor
.toList());
}
+ @Test
+ void allThemesAreExported(PlanConfig config, Exporter exporter) throws ExportException, IOException {
+ config.set(ExportSettings.SERVER_PAGE, true);
+ Path exportPath = config.getPageExportPath();
+ exporter.exportReact();
+
+ Path themePath = Path.of(new File("").getAbsolutePath())
+ .resolve("src/main/resources/assets/plan/themes");
+
+ List filesToExport = Files.list(themePath)
+ .filter(path -> path.toFile().getAbsolutePath().endsWith(".json"))
+ .map(path -> path.relativize(themePath))
+ .toList();
+ assertFalse(filesToExport.isEmpty());
+ List filesExported = Files.list(exportPath).map(path -> path.relativize(exportPath)).toList();
+ assertAll(filesToExport.stream()
+ .map(path -> (Executable) () -> assertTrue(filesExported.contains(path)))
+ .toList());
+ }
+
@Test
void noReactFilesAreExported(PlanConfig config, Exporter exporter) throws ExportException, IOException {
config.set(ExportSettings.PLAYER_PAGES, false);
diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/web/AssetVersionsTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/web/AssetVersionsTest.java
new file mode 100644
index 0000000000..e3f3bafa0e
--- /dev/null
+++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/web/AssetVersionsTest.java
@@ -0,0 +1,58 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.web;
+
+import com.djrapitops.plan.storage.file.PlanFiles;
+import extension.FullSystemExtension;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import utilities.TestResources;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author AuroraLS3
+ */
+@ExtendWith(FullSystemExtension.class)
+class AssetVersionsTest {
+
+ // This test may fail if new themes are added, but determineAssetModifications task is not run
+ @Test
+ void themeNamesAreCorrect(PlanFiles files) throws IOException {
+ List themeNames = new AssetVersions(files).getThemeNames();
+ List expected = getFileNamesInFolder(TestResources.getAsset("themes"))
+ .filter(file -> file.endsWith("json"))
+ .map(file -> file.substring(0, file.indexOf('.')))
+ .sorted()
+ .toList();
+ assertEquals(expected, themeNames);
+ }
+
+ private Stream getFileNamesInFolder(File folder) {
+ return Arrays.stream(Objects.requireNonNull(folder.listFiles()))
+ .filter(Objects::nonNull)
+ .filter(File::isFile)
+ .map(File::getName);
+ }
+}
\ No newline at end of file
diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlTest.java
index bc6ac5c8d1..40a253b2be 100644
--- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlTest.java
+++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlTest.java
@@ -159,13 +159,20 @@ static Stream testCases() {
Arguments.of("/manage", WebPermission.MANAGE_GROUPS, 200, 403),
Arguments.of("/v1/groupPermissions?group=admin", WebPermission.MANAGE_GROUPS, 200, 403),
Arguments.of("/v1/webGroups", WebPermission.MANAGE_GROUPS, 200, 403),
- Arguments.of("/v1/deleteGroup?group=admin&moveTo=no_access", WebPermission.MANAGE_GROUPS, 400, 403),
- Arguments.of("/v1/saveGroupPermissions?group=admin", WebPermission.MANAGE_GROUPS, 400, 403),
+ Arguments.of("/v1/deleteGroup?group=admin&moveTo=no_access", WebPermission.MANAGE_GROUPS, 405, 403),
+ Arguments.of("/v1/saveGroupPermissions?group=admin", WebPermission.MANAGE_GROUPS, 405, 403),
Arguments.of("/v1/preferences", WebPermission.ACCESS, 200, 200),
- Arguments.of("/v1/storePreferences", WebPermission.ACCESS, 400, 400),
+ Arguments.of("/v1/storePreferences", WebPermission.ACCESS, 405, 405),
Arguments.of("/v1/pluginHistory?server=" + TestConstants.SERVER_UUID_STRING, WebPermission.PAGE_NETWORK_PLUGIN_HISTORY, 200, 403),
Arguments.of("/v1/pluginHistory?server=" + TestConstants.SERVER_UUID_STRING, WebPermission.PAGE_SERVER_PLUGIN_HISTORY, 200, 403),
- Arguments.of("/v1/gameAllowlistBounces?server=" + TestConstants.SERVER_UUID_STRING, WebPermission.PAGE_SERVER_ALLOWLIST_BOUNCE, 200, 403)
+ Arguments.of("/v1/gameAllowlistBounces?server=" + TestConstants.SERVER_UUID_STRING, WebPermission.PAGE_SERVER_ALLOWLIST_BOUNCE, 200, 403),
+ Arguments.of("/v1/theme?theme=default", WebPermission.ACCESS, 200, 200),
+ Arguments.of("/v1/saveTheme?theme=default", WebPermission.MANAGE_THEMES, 405, 403),
+ Arguments.of("/v1/deleteTheme?theme=default", WebPermission.MANAGE_THEMES, 405, 403),
+ Arguments.of("/theme-editor", WebPermission.ACCESS_THEME_EDITOR, 200, 403),
+ Arguments.of("/theme-editor/new", WebPermission.ACCESS_THEME_EDITOR, 200, 403),
+ Arguments.of("/theme-editor/delete", WebPermission.ACCESS_THEME_EDITOR, 200, 403),
+ Arguments.of("/theme-editor/default", WebPermission.ACCESS_THEME_EDITOR, 200, 403)
);
}
diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlVisibilityTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlVisibilityTest.java
index 89f386ab6e..93e7c64779 100644
--- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlVisibilityTest.java
+++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlVisibilityTest.java
@@ -37,6 +37,7 @@
import com.djrapitops.plan.utilities.PassEncryptUtil;
import extension.FullSystemExtension;
import extension.SeleniumExtension;
+import org.awaitility.Awaitility;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
@@ -49,7 +50,6 @@
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.logging.LogEntry;
import org.openqa.selenium.logging.LogType;
-import org.awaitility.Awaitility;
import utilities.RandomData;
import utilities.TestConstants;
import utilities.TestResources;
@@ -200,7 +200,10 @@ static Stream pageLevelVisibleCases() {
Arguments.arguments(WebPermission.MANAGE_GROUPS, "slice_h_0", "manage"),
Arguments.arguments(WebPermission.ACCESS_QUERY, "query-button", "query"),
Arguments.arguments(WebPermission.ACCESS_PLAYERS, "players-table", "players"),
- Arguments.arguments(WebPermission.ACCESS_ERRORS, "content", "errors")
+ Arguments.arguments(WebPermission.ACCESS_ERRORS, "content", "errors"),
+ Arguments.arguments(WebPermission.ACCESS_THEME_EDITOR, "theme-editor", "theme-editor/default"),
+ Arguments.arguments(WebPermission.ACCESS_THEME_EDITOR, "add-theme", "theme-editor/new"),
+ Arguments.arguments(WebPermission.ACCESS_THEME_EDITOR, "delete-theme", "theme-editor/delete")
// Arguments.arguments(WebPermission.ACCESS_DOCS, "swagger-ui", "docs")
);
}
diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/HttpAccessControlTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/HttpAccessControlTest.java
index c0d989a760..efc2915a71 100644
--- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/HttpAccessControlTest.java
+++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/HttpAccessControlTest.java
@@ -91,6 +91,8 @@ static void afterAll(PlanSystem system) {
"/v1/saveGroupPermissions",
"/v1/deleteGroup",
"/v1/storePreferences",
+ "/v1/saveTheme",
+ "/v1/deleteTheme",
"/v1/pluginHistory?server=" + TestConstants.SERVER_UUID_STRING,
"/manage",
"/auth/register",
@@ -104,14 +106,12 @@ void endpointNotEnabled(String endpoint) throws Exception {
assertEquals(404, code);
}
-
private int access(String resource) throws IOException, KeyManagementException, NoSuchAlgorithmException {
HttpURLConnection connection = null;
try {
connection = CONNECTOR.getConnection("GET", ADDRESS + resource);
return connection.getResponseCode();
-
} finally {
if (connection != null) connection.disconnect();
}
diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/auth/ActiveCookieStoreTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/auth/ActiveCookieStoreTest.java
index 99ab15dd09..b1cfcd679a 100644
--- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/auth/ActiveCookieStoreTest.java
+++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/auth/ActiveCookieStoreTest.java
@@ -62,26 +62,26 @@ void clearCookies() {
@Test
void cookiesAreStored() {
- String cookie = underTest.generateNewCookie(user);
- User matchingUser = underTest.checkCookie(cookie).orElseThrow(AssertionError::new);
+ String cookie = underTest.generateNewCookie(user, TestConstants.IPV4_ADDRESS);
+ User matchingUser = underTest.findCookie(cookie).map(CookieMetadata::getUser).orElseThrow(AssertionError::new);
assertEquals(user, matchingUser);
}
@Test
void cookiesAreRemoved() {
- String cookie = underTest.generateNewCookie(user);
+ String cookie = underTest.generateNewCookie(user, TestConstants.IPV4_ADDRESS);
underTest.removeCookie(cookie);
- assertFalse(underTest.checkCookie(cookie).isPresent());
+ assertFalse(underTest.findCookie(cookie).isPresent());
}
@Test
void usersCookiesAreRemoved() {
- String cookie = underTest.generateNewCookie(user);
+ String cookie = underTest.generateNewCookie(user, TestConstants.IPV4_ADDRESS);
ActiveCookieStore.removeUserCookie(user.getUsername());
- assertFalse(underTest.checkCookie(cookie).isPresent());
+ assertFalse(underTest.findCookie(cookie).isPresent());
}
}
\ No newline at end of file
diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/resolver/json/theme/SaveThemeJSONResolverTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/resolver/json/theme/SaveThemeJSONResolverTest.java
new file mode 100644
index 0000000000..ff10259f33
--- /dev/null
+++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/resolver/json/theme/SaveThemeJSONResolverTest.java
@@ -0,0 +1,281 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.webserver.resolver.json.theme;
+
+import com.djrapitops.plan.delivery.domain.datatransfer.ThemeDto;
+import com.djrapitops.plan.delivery.web.resolver.Response;
+import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
+import com.djrapitops.plan.delivery.web.resolver.exception.MethodNotAllowedException;
+import com.djrapitops.plan.delivery.web.resolver.request.Request;
+import com.djrapitops.plan.delivery.web.resolver.request.URIQuery;
+import com.djrapitops.plan.delivery.webserver.ResponseFactory;
+import com.djrapitops.plan.storage.file.PlanFiles;
+import com.google.gson.Gson;
+import extension.FullSystemExtension;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+import utilities.RandomData;
+import utilities.TestResources;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author AuroraLS3
+ */
+@ExtendWith({MockitoExtension.class, FullSystemExtension.class})
+class SaveThemeJSONResolverTest {
+
+ @Mock
+ ResponseFactory responseFactory;
+ @Spy
+ Gson gson = new Gson();
+ @InjectMocks
+ SaveThemeJSONResolver saveThemeJSONResolver;
+
+ @Mock
+ Request request;
+
+ static Stream invalidThemeModificationsCases() {
+ return Stream.of(
+ Arguments.arguments("null name", (Consumer) themeDto -> themeDto.setName(null)),
+ Arguments.arguments("bad char name", (Consumer) themeDto -> themeDto.setName("bad-char-?")),
+ Arguments.arguments("empty name", (Consumer