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, "", ""); + 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 people 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>优秀人物 也做出了贡献:" 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é, 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 Leute 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 people 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 maravillosas 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 ihmiset 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 formidables 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 persone:" 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>素晴らしい人々が開発に貢献しています" 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>멋진 사람들이 기여했습니다." 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 mensen 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 people 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>замечательные люди внесли свой вклад:" 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ı 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>чудові люди зробили свій внесок:" 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>優秀人物 也做出了貢獻:" 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 @@ {!hideUpdater && <> @@ -69,7 +68,7 @@ const Header = ({page, tab, hideUpdater}) => {
- + {authRequired && user ? <> {user.username} user img diff --git a/Plan/react/dashboard/src/components/navigation/Loader.jsx b/Plan/react/dashboard/src/components/navigation/Loader.jsx index d550bb2ffa..55611e8a46 100644 --- a/Plan/react/dashboard/src/components/navigation/Loader.jsx +++ b/Plan/react/dashboard/src/components/navigation/Loader.jsx @@ -5,7 +5,7 @@ export const CardLoader = () => { return ( -
+
...
diff --git a/Plan/react/dashboard/src/components/navigation/PageNavigationItem.jsx b/Plan/react/dashboard/src/components/navigation/PageNavigationItem.jsx index e8c6ac29a5..d3f82364c5 100644 --- a/Plan/react/dashboard/src/components/navigation/PageNavigationItem.jsx +++ b/Plan/react/dashboard/src/components/navigation/PageNavigationItem.jsx @@ -124,7 +124,7 @@ const PageNavigationItem = ({page}) => {
  • -
    + + + + + + + + + + ); +}); + +const ColorEditForm = ({onFocus}) => { + const {t} = useTranslation(); + const { + alreadyExists, + name, + color, + onNameChange, + onColorChange, + open, + editNewColor, + finishEdit, + discardEdit, + deleting, + setDeleting + } = useColorEditContext(); + + const ref = useRef(null); + + useEffect(() => { + if (open) { + onFocus(); + } + if (open && ref.current) { + ref.current.focus(); + } + }, [open]); + + if (deleting) { + return ( + + setDeleting(false)}> + {t('html.label.themeEditor.finish')} + + + ) + } + + if (!open) { + return ( + + + {t('html.label.themeEditor.addColor')} + + setDeleting(true)}> + {t('html.label.themeEditor.deleteColors')} + + + ) + } + + const contrastColor = !isColorInvalid() && getContrastColor(color) || "var(--color-forms-input-text)"; + + function isNameInvalid() { + return name.length > 100; + } + + function isColorInvalid() { + if (!color.length) { + return true + } + + const hexRegex = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/; + const rgbRegex = /^rgba?\(\s*(\d{1,3}%?\s*,\s*){2}\d{1,3}%?(\s*,\s*(0|1|0?\.\d+))?\s*\)$/; + const hslRegex = /^hsla?\(\s*(\d{1,3})(deg)?\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%(?:\s*,\s*(0|1|0?\.\d+))?\s*\)$/i; + const hsvRegex = /^hsva?\(\s*(\d{1,3})(deg)?\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%(?:\s*,\s*(0|1|0?\.\d+))?\s*\)$/i; + const linearGradientRegex = /^linear-gradient\(.*\)$/i; + const radialGradientRegex = /^radial-gradient\(.*\)$/i; + const colorMix = /^color-mix\(.*\)$/i; + + return !(hexRegex.test(color) || rgbRegex.test(color) || hslRegex.test(color) || hsvRegex.test(color) || linearGradientRegex.test(color) || radialGradientRegex.test(color)) || colorMix.test(color); + } + + const isGradient = color.includes('gradient'); + + return ( + + + +
    + +
    + onNameChange(event.target.value)} + /> + +
    + {alreadyExists && {t('html.label.themeEditor.alreadyExistsWarning')}} + {isGradient && {t('html.label.themeEditor.gradientWarning')}} + + + + {t('html.label.managePage.changes.save')} + + + {t('html.label.managePage.changes.discard')} + + +
    + ) +}; + +export default ColorEditForm \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/ColorMultiSelect.jsx b/Plan/react/dashboard/src/components/theme/ColorMultiSelect.jsx new file mode 100644 index 0000000000..426036ca35 --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/ColorMultiSelect.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import MultiSelect from "../input/MultiSelect.jsx"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faSquare} from "@fortawesome/free-solid-svg-icons"; + +const ColorMultiSelect = ({className, colors, selectedColors, setSelectedColors, sort, style}) => { + const colorArray = Object.keys(colors); + const selectedIndexes = selectedColors ? selectedColors.map(color => colorArray.indexOf(color)) : []; + + const changeSelectedIndexes = selected => { + const newIndexes = sort ? selected.toSorted() : selected; + setSelectedColors?.(newIndexes.length ? newIndexes.map(index => colorArray[index]) : [colorArray[0]]); + }; + + const options = colorArray.map(color => ( + {color} + )); + + return ( + + ) +}; + +export default ColorMultiSelect \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/ColorSection.jsx b/Plan/react/dashboard/src/components/theme/ColorSection.jsx new file mode 100644 index 0000000000..9295530fcb --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/ColorSection.jsx @@ -0,0 +1,15 @@ +import {ColorBox} from "./ColorBox.jsx"; +import React from "react"; + +const ColorSection = ({title, colors}) => ( +
    +
    {title}
    +
    + {Object.entries(colors).map(([name, color]) => ( + + ))} +
    +
    +); + +export default ColorSection; \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/ColorSelectorButton.jsx b/Plan/react/dashboard/src/components/theme/ColorSelectorButton.jsx new file mode 100644 index 0000000000..7844025bfb --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/ColorSelectorButton.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import {nameToContrastCssVariable, nameToCssVariable} from "../../util/colors.js"; +import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; +import {faCheck, faPalette} from "@fortawesome/free-solid-svg-icons"; + +const ColorSelectorButton = ({color, setColor, disabled, active}) => { + const validCssColor = color => { + return color === 'theme' ? 'reference-colors-theme' : color; + } + return ( + + ) +} + +export default ColorSelectorButton \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/DownloadButton.jsx b/Plan/react/dashboard/src/components/theme/DownloadButton.jsx new file mode 100644 index 0000000000..916f41dd9a --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/DownloadButton.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faDownload} from "@fortawesome/free-solid-svg-icons"; +import ActionButton from "../input/button/ActionButton.jsx"; +import {useTranslation} from "react-i18next"; +import {useThemeEditContext} from "../../hooks/context/themeEditContextHook.jsx"; + +const DownloadButton = ({className, disabled}) => { + const {t} = useTranslation(); + + const { + name, currentColors, currentNightColors, currentUseCases, currentNightModeUseCases + } = useThemeEditContext(); + + const download = () => { + const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify({ + name, + colors: currentColors, + nightColors: currentNightColors, + useCases: currentUseCases, + nightModeUseCases: currentNightModeUseCases + })); + const dlAnchorElem = document.createElement('a'); + dlAnchorElem.setAttribute("href", dataStr); + dlAnchorElem.setAttribute("download", name + ".json"); + document.body.appendChild(dlAnchorElem); + dlAnchorElem.click(); + dlAnchorElem.remove(); + } + + return ( + + {t('html.modal.version.download')} + + ) +}; + +export default DownloadButton \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/EditorMenuToast.jsx b/Plan/react/dashboard/src/components/theme/EditorMenuToast.jsx new file mode 100644 index 0000000000..47c68227cc --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/EditorMenuToast.jsx @@ -0,0 +1,83 @@ +import React, {useEffect, useState} from 'react'; +import {Toast} from "react-bootstrap"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faHistory, faRedoAlt, faUndoAlt} from "@fortawesome/free-solid-svg-icons"; +import {useThemeEditContext} from "../../hooks/context/themeEditContextHook.jsx"; +import ActionButton from "../input/button/ActionButton.jsx"; +import {useTranslation} from "react-i18next"; +import {unstable_usePrompt} from "react-router-dom"; +import ThemeEditHistory from "./ThemeEditHistory.jsx"; +import OutlineButton from "../input/button/OutlineButton.jsx"; +import ThemeEditIssues from "./ThemeEditIssues.jsx"; + +const EditorMenuToast = () => { + const {t} = useTranslation(); + const {editCount, redoCount, undo, redo, savePossible} = useThemeEditContext(); + const [historyOpen, setHistoryOpen] = useState(false); + const toggleHistory = () => { + setHistoryOpen(!historyOpen); + } + + const [scrolled, setScrolled] = useState(false); + useEffect(() => { + const onScroll = () => { + setScrolled(window.scrollY > 80); // adjust threshold as needed + }; + window.addEventListener("scroll", onScroll); + return () => window.removeEventListener("scroll", onScroll); + }, []); + + useEffect(() => { + const handleKeyDown = (e) => { + // For Mac: metaKey (Cmd), for Windows/Linux: ctrlKey + const isUndo = (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z' && !e.shiftKey; + const isRedo = (e.ctrlKey || e.metaKey) && ( + (e.key.toLowerCase() === 'y') || + (e.key.toLowerCase() === 'z' && e.shiftKey) + ); + + if (isUndo && editCount > 0) { + e.preventDefault(); + undo(); + } else if (isRedo && redoCount > 0) { + e.preventDefault(); + redo(); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [editCount, redoCount, undo, redo]); + + useEffect(() => { + if (editCount > 0) { + const handleBeforeUnload = (event) => { + event.preventDefault(); + event.returnValue = ''; + }; + window.addEventListener('beforeunload', handleBeforeUnload); + return () => window.removeEventListener('beforeunload', handleBeforeUnload); + } + }, [editCount]); + + unstable_usePrompt({when: editCount > 0, message: t('html.label.themeEditor.unsavedChanges')}); + + if (editCount === 0 && redoCount === 0) return <> + + return ( + + + {t('html.label.themeEditor.undo')} + {t('html.label.themeEditor.redo')} + {t(historyOpen ? 'html.label.themeEditor.hideHistory' : 'html.label.themeEditor.showHistory')} + + {!savePossible && editCount > 0 && } + {historyOpen && } + + + ) +}; + +export default EditorMenuToast \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/ExampleSection.jsx b/Plan/react/dashboard/src/components/theme/ExampleSection.jsx new file mode 100644 index 0000000000..e458b9962b --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/ExampleSection.jsx @@ -0,0 +1,78 @@ +import {SidebarUseCase} from "./usecase/SidebarUseCase.jsx"; +import InfoBoxUseCase from "./usecase/InfoBoxUseCase.jsx"; +import {ChartLoader} from "../navigation/Loader.jsx"; +import TrendUseCase from "./usecase/TrendUseCase.jsx"; +import CardUseCase from "./usecase/CardUseCase.jsx"; +import CalendarUseCase from "./usecase/CalendarUseCase.jsx"; +import DataUseCase from "./usecase/DataUseCase.jsx"; +import {faChartLine, faServer} from "@fortawesome/free-solid-svg-icons"; +import DataPlayUseCase from "./usecase/DataPlayUseCase.jsx"; +import DataPlayersUseCase from "./usecase/DataPlayersUseCase.jsx"; +import DataPerformanceUseCase from "./usecase/DataPerformanceUseCase.jsx"; +import DataCalculatedUseCase from "./usecase/DataCalculatedUseCase.jsx"; +import DataPlayerVersusUseCase from "./usecase/DataPlayerVersusUseCase.jsx"; +import DataPlayerStatusUseCase from "./usecase/DataPlayerStatusUseCase.jsx"; +import React from "react"; +import {useTranslation} from "react-i18next"; +import CollapseWithButton from "../layout/CollapseWithButton.jsx"; +import FormsUseCase from "./usecase/FormsUseCase.jsx"; +import {addToObject} from "../../util/mutator.js"; +import {graphUseCases} from "./usecase/GraphUseCases.jsx"; +import {TableUseCase} from "./usecase/TableUseCases.jsx"; +import {formatLabel} from "./UseCase.jsx"; +import TabsUseCase from "./usecase/TabsUseCase.jsx"; +import DataPluginsUseCase from "./usecase/DataPluginsUseCase.jsx"; + +const findExample = (path, examples) => { + if (!path?.length) return undefined; + const found = examples[path]; + if (found) return found; + return findExample(path.split('.').slice(0, -1).join('.'), examples); +} + +const ExampleSection = ({displayedItem, className}) => { + const {t} = useTranslation(); + const examples = { + "sidebar": , + "layout.background": , + "layout.title": , + "tabs": , + "infoBox": , + "layout.loader": , + "data.trend": , + "cards": , + "layout.helpIcon": , + "layout.divider": , + "calendar": , + "data.servers": , + "data.play": , + "data.players": , + "data.playerPeakLast": , + "data.playerPeakAllTime": , + "data.performance": , + "data.calculated": , + "data.playerVersus": , + "data.playerStatus": , + "forms": , + "tables": , + "plugin": + } + addToObject(examples, graphUseCases); + const example = findExample(displayedItem, examples); + + const displayText = displayedItem ? displayedItem.split('.').map(formatLabel).join(' › ') : ""; + + return ( +
    + {t('html.label.themeEditor.example')}{example ? <>{' '}· {displayText} : ''}}> + {example &&
    + {example} +
    } +
    +
    + ) +} + +export default ExampleSection; \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/ThemeEditHistory.jsx b/Plan/react/dashboard/src/components/theme/ThemeEditHistory.jsx new file mode 100644 index 0000000000..8d3290fa29 --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/ThemeEditHistory.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import {useThemeEditContext} from "../../hooks/context/themeEditContextHook.jsx"; +import {useTranslation} from "react-i18next"; + +const ThemeEditHistory = () => { + const {t} = useTranslation() + const {editCount, redoCount, edits, redos} = useThemeEditContext(); + + if (editCount === 0 && redoCount === 0) { + return <> + } + + return ( +
      + {redos.map((item, index) => { + if (item.length) { + return +
    • {t('html.label.themeEditor.changes.discardedChanges')}
    • + {item.map((i, ix) =>
    • {i.name}
    • )} +
      + } else { + return
    • {item.name}
    • + } + })} + {edits.toReversed().map((item, index) =>
    • {item.name}
    • )} +
    + ) +}; + +export default ThemeEditHistory \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/ThemeEditIssues.jsx b/Plan/react/dashboard/src/components/theme/ThemeEditIssues.jsx new file mode 100644 index 0000000000..87aae3d132 --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/ThemeEditIssues.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import {useThemeEditContext} from "../../hooks/context/themeEditContextHook.jsx"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faExclamationTriangle} from "@fortawesome/free-solid-svg-icons"; +import {useTranslation} from "react-i18next"; + +const ThemeEditIssues = () => { + const {t} = useTranslation(); + const {issues} = useThemeEditContext(); + return ( +
      +
    • {issues.length} {t('html.label.themeEditor.issues.problems')}
    • + {issues.map((item, index) =>
    • {item}
    • )} +
    + ) +}; + +export default ThemeEditIssues \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/ThemeOption.jsx b/Plan/react/dashboard/src/components/theme/ThemeOption.jsx new file mode 100644 index 0000000000..c7a890f7e1 --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/ThemeOption.jsx @@ -0,0 +1,216 @@ +import React, {useEffect, useRef} from 'react'; +import {getLocallyStoredThemes, ThemeContextProvider, useTheme} from "../../hooks/themeHook.jsx"; +import {ThemeStorageContextProvider} from "../../hooks/context/themeContextHook.jsx"; +import {ThemeStyleCss} from "./ThemeStyleCss.jsx"; +import {Card, Col} from "react-bootstrap"; +import logo from "../../Flaticon_circle.png"; +import drawSine from "../../util/loginSineRenderer.js"; +import {calculateCssHexColor} from "../../util/colors.js"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faCheck, faDesktop, faPencilAlt} from "@fortawesome/free-solid-svg-icons"; +import OutlineButton from "../input/button/OutlineButton.jsx"; +import {useAuth} from "../../hooks/authenticationHook.jsx"; +import {useNavigate} from "react-router-dom"; +import {useTranslation} from "react-i18next"; + +const StorageIcon = ({theme}) => { + const {t} = useTranslation(); + const onlyLocal = getLocallyStoredThemes().includes(theme); + + if (!onlyLocal) return <> + + return ( + ( {t('html.value.localMachine')}) + ) +} + +const ThemeOption = ({theme, nightMode, selected, setSelected}) => { + const ref = useRef(); + useEffect(() => { + drawSine(`theme-plot-${theme}`, calculateCssHexColor("var(--color-data-players-online)", ref.current)); + }, [ref]); + const {authLoaded, authRequired, hasPermission} = useAuth(); + const themeHook = useTheme(); + const navigate = useNavigate(); + + const managedComponent = !!setSelected; + const canEdit = !managedComponent && authLoaded && (!authRequired || hasPermission('access.theme.editor')) + + const onClickChoose = () => { + if (managedComponent) { + setSelected(theme); + } else { + themeHook.setTheme(theme); + } + } + + const onClickEdit = () => { + themeHook.toggleColorChooser(); + navigate(`/theme-editor/${theme}`); + } + + return ( + + + {canEdit && + + + } + + ) +}; + +export default ThemeOption \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/ThemeStyleCss.jsx b/Plan/react/dashboard/src/components/theme/ThemeStyleCss.jsx new file mode 100644 index 0000000000..b9c22c59ae --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/ThemeStyleCss.jsx @@ -0,0 +1,96 @@ +import {addToObject, flattenObject} from '../../util/mutator'; +import {nameToCssVariable} from '../../util/colors'; +import {getColorConverter, getContrastColor} from "../../util/Color.js"; +import {useThemeEditContext} from "../../hooks/context/themeEditContextHook.jsx"; +import {useThemeStorage} from "../../hooks/context/themeContextHook.jsx"; + +// Function to generate CSS variables from theme data +const generateThemeCSS = ({applyToClass, colors, nightColors, useCases, nightModeUseCases, color}) => { + const baseVariables = []; + const nightModeVariables = []; + + // Helper to add both color and its contrast + const addColorWithContrast = (name, color, variables) => { + variables.push(`--color-${name}: ${color}`); + variables.push(`--contrast-color-${name}: ${getContrastColor(color)}`); + }; + + // Add regular colors + Object.entries(colors).forEach(([key, value]) => { + addColorWithContrast(key, value, baseVariables); + // Add desaturated version for night mode + const converter = getColorConverter(value); + + const nightColor = converter ? converter.reduceSaturation().toRgbaString() : value; + addColorWithContrast(key, nightColor, nightModeVariables); + }); + + // Add night mode colors + Object.entries(nightColors).forEach(([key, value]) => { + addColorWithContrast(key, value, baseVariables); + addColorWithContrast(key, value, nightModeVariables); + }); + + // Add pie chart colors + // theme.pieColors.forEach((color, index) => { + // addColorWithContrast(`pie-${index + 1}`, color, baseVariables); + // const nightColor = withReducedSaturation(color); + // addColorWithContrast(`pie-${index + 1}`, nightColor, nightModeVariables); + // }); + + // Add night mode use case variables + let flattenedNightUseCases = addToObject(flattenObject(nightModeUseCases), nightModeUseCases.referenceColors); + // Override with user selected theme color + if (color && color !== 'theme') flattenedNightUseCases = addToObject(flattenedNightUseCases, {theme: nameToCssVariable(color)}); + Object.entries(flattenedNightUseCases).forEach(([key, value]) => { + if (typeof value === 'string' && value.startsWith('var(--color-')) { + const referencedColor = value.replace('var(--color-', '').replace(')', ''); + nightModeVariables.push(`--color-${key}: var(--color-${referencedColor})`); + nightModeVariables.push(`--contrast-color-${key}: var(--contrast-color-${referencedColor})`); + } + }); + + const nightModeKeys = Object.keys(flattenedNightUseCases); + // Add use case variables + let flattenedUseCases = addToObject(flattenObject(useCases), useCases.referenceColors); + // Override with user selected theme color + if (color && color !== 'theme') flattenedUseCases = addToObject(flattenedUseCases, {theme: nameToCssVariable(color)}); + Object.entries(flattenedUseCases).forEach(([key, value]) => { + if (typeof value === 'string' && value.startsWith('var(--color-')) { + const referencedColor = value.replace('var(--color-', '').replace(')', ''); + baseVariables.push(`--color-${key}: var(--color-${referencedColor})`); + baseVariables.push(`--contrast-color-${key}: var(--contrast-color-${referencedColor})`); + if (!nightModeKeys.includes(key)) { + nightModeVariables.push(`--color-${key}: var(--color-${referencedColor})`); + nightModeVariables.push(`--contrast-color-${key}: var(--contrast-color-${referencedColor})`); + } + } + }); + + + return ` +${applyToClass ? `.${applyToClass}` : ':root'} { + ${baseVariables.join(';\n ')}; + --editor-bg-color: var(--color-white-grey); + color: var(--color-text-light); +} + +${applyToClass ? `.${applyToClass}.night-mode-colors,.${applyToClass} .night-mode-colors` : '.night-mode-colors'} { + ${nightModeVariables.join(';\n ')}; + --editor-bg-color: var(--color-night-dark-blue); + color: var(--color-night-text); +}`; +}; + +export const ThemeStyleCss = ({editMode, applyToClass}) => { + const { + loaded, color, + currentColors: colors, currentNightColors: nightColors, + currentUseCases: useCases, currentNightModeUseCases: nightModeUseCases + } = editMode ? useThemeEditContext() : useThemeStorage(); + + if (!loaded) return <> + return ( + + ) +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/UseCase.jsx b/Plan/react/dashboard/src/components/theme/UseCase.jsx new file mode 100644 index 0000000000..83c41a94e1 --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/UseCase.jsx @@ -0,0 +1,196 @@ +import React, {useEffect, useRef} from 'react'; +import ColorDropdown from "./ColorDropdown.jsx"; +import ColorMultiSelect from "./ColorMultiSelect.jsx"; +import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; +import {faTimes} from "@fortawesome/free-solid-svg-icons"; +import {useMinHeightContext} from "../../hooks/context/minHeightContextHook"; + +export const formatLabel = (key) => { + return key + .replace(/([A-Z])/g, ' $1') + .replace(/^./, str => str.toUpperCase()) + .trim(); +}; + +const SectionLabel = ({level, label, id, isNightMode, onHoverChange}) => ( + + + {(level === 0 && label !== 'Reference Colors') &&
    } +
    onHoverChange(id, 'enter', isNightMode)} + onFocus={() => onHoverChange(id, 'enter', isNightMode)} + onMouseOut={() => onHoverChange(id, 'exit', isNightMode)} + onBlur={() => onHoverChange(id, 'exit', isNightMode)}> + {label} +
    + + +); + +const UseCaseLabel = ({level, label}) => ( + + {label} + +); + +const NightModeRemovalButton = ({onRemove, path}) => ( + +); + +const UseCaseDropdown = ({id, colors, value, onChange, label, onRemoveOverride, path}) => ( + +
    +
    + +
    + {onRemoveOverride && ( + + )} +
    + +); + +const UseCaseArraySelector = ({ + value, + isNightMode, + baseValue, + id, + onHoverChange, + level, + path, + colors, + onChange, + onRemoveOverride + }) => { + const ref = useRef(); + const {registerMinHeight, unregisterMinHeight} = useMinHeightContext(); + const selector = `selector-${path.join('_')}`; + const selectedNames = value || []; + + useEffect(() => { + if (ref.current) { + if (unregisterMinHeight(selector, isNightMode, selectedNames.length)) { + setTimeout(() => { + registerMinHeight(selector, ref.current.offsetHeight, isNightMode, selectedNames.length); + }, 0); + } else { + registerMinHeight(selector, ref.current.offsetHeight, isNightMode, selectedNames.length); + } + } + }, [ref, registerMinHeight, selectedNames.length]); + + const showRemove = isNightMode && JSON.stringify(value) !== JSON.stringify(baseValue); + return ( + onHoverChange(id, 'enter', isNightMode)} + onFocus={() => onHoverChange(id, 'enter', isNightMode)} + onMouseOut={() => onHoverChange(id, 'exit', isNightMode)} + onBlur={() => onHoverChange(id, 'exit', isNightMode)}> + + +
    +
    + { + onChange(newNames, path); + }} + /> +
    + {showRemove && ( + + )} +
    + + + ); +} +const UseCase = ({path, value, onChange, onHoverChange, colors, isNightMode, baseValue, onRemoveOverride}) => { + const level = Math.max(0, path.length - 1); + const id = path.join('.'); + + if (typeof value === 'string') { + const showRemove = isNightMode && value !== baseValue; + return ( + onHoverChange(id, 'enter', isNightMode)} + onFocus={() => onHoverChange(id, 'enter', isNightMode)} + onMouseOut={() => onHoverChange(id, 'exit', isNightMode)} + onBlur={() => onHoverChange(id, 'exit', isNightMode)}> + + onChange(newValue, path)} + label={formatLabel(path[path.length - 1])} + {...(showRemove ? {onRemoveOverride, path} : {})} + /> + + ); + } + + if (Array.isArray(value)) { + // Use the array of names directly + return ; + } + + return ( + <> + {typeof value === 'object' && !Array.isArray(value) && path.length > 0 && ( + + )} + {Object.entries(value).map(([key, val]) => ( + + ))} + + ); +}; + +export default UseCase; \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/UseCaseSection.jsx b/Plan/react/dashboard/src/components/theme/UseCaseSection.jsx new file mode 100644 index 0000000000..ecca6dd0aa --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/UseCaseSection.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import {useTranslation} from "react-i18next"; +import {mergeUseCases} from "../../util/mutator.js"; +import UseCase from "./UseCase.jsx"; + +const UseCaseSection = ({ + useCases, + onHoverChange, + colors, + baseUseCases = null, + isNightMode = false, + updateUseCase, + removeOverride + }) => { + const {t} = useTranslation(); + // For night mode, we need to merge the base use cases with overrides + const mergedUseCases = isNightMode && baseUseCases ? mergeUseCases(baseUseCases, useCases) : useCases; + + return ( +
    +
    {isNightMode ? t('html.label.themeEditor.nightModeOverrides') : t('html.label.themeEditor.useCases')}
    + + + + +
    +
    + ); +}; + +export default UseCaseSection \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/Background.jsx b/Plan/react/dashboard/src/components/theme/usecase/Background.jsx new file mode 100644 index 0000000000..d8d804b725 --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/Background.jsx @@ -0,0 +1,7 @@ +import React from 'react'; + +const Background = ({children}) => { + return
    {children}
    +} + +export default Background \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/CalendarUseCase.jsx b/Plan/react/dashboard/src/components/theme/usecase/CalendarUseCase.jsx new file mode 100644 index 0000000000..a76f4598b6 --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/CalendarUseCase.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import PlayerSessionCalendar from "../../calendar/PlayerSessionCalendar.jsx"; +import {calculateCssHexColor} from "../../../util/colors.js"; +import Background from "./Background.jsx"; + +const CalendarUseCase = () => { + + const date = Date.now(); + const day = 24 * 60 * 60 * 1000; + const series = [{ + title: "html.label.playtime", + start: date - (date % day), + end: undefined, + color: calculateCssHexColor("var(--color-data-play-playtime)"), + value: 12345678 + }] + for (let i = 0; i < 10; i++) { + const playtime = 12345678 / 10; + const offset = ((date % day) / 10) * i; + series.push({ + title: "html.label.session", + start: date - offset, + end: (date - offset) + playtime, + color: calculateCssHexColor("var(--color-data-play-sessions)"), + value: playtime + }) + } + return ( + + + + ) +}; + +export default CalendarUseCase \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/CardUseCase.jsx b/Plan/react/dashboard/src/components/theme/usecase/CardUseCase.jsx new file mode 100644 index 0000000000..6a2397115c --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/CardUseCase.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import CardHeader from "../../cards/CardHeader.jsx"; +import {faImage, faUser} from "@fortawesome/free-solid-svg-icons"; +import {Card, Col, Row} from "react-bootstrap"; +import Datapoint from "../../Datapoint.jsx"; +import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; +import {faQuestionCircle} from "@fortawesome/free-regular-svg-icons"; + +const CardUseCase = () => { + return ( + + + + + + + + +
    + +
    +
    + +
    + ) +}; + +export default CardUseCase \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/DataCalculatedUseCase.jsx b/Plan/react/dashboard/src/components/theme/usecase/DataCalculatedUseCase.jsx new file mode 100644 index 0000000000..1a6a5ebc80 --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/DataCalculatedUseCase.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import {Card, Col, Row} from "react-bootstrap"; +import DataUseCase from "./DataUseCase.jsx"; +import { + faChartColumn, + faCodeCompare, + faFilterCircleXmark, + faGlobe, + faUserCircle, + faUsersViewfinder +} from "@fortawesome/free-solid-svg-icons"; +import {faLifeRing} from "@fortawesome/free-regular-svg-icons"; + +const DataCalculatedUseCase = () => { + return ( + + + + + + + + + + + + + + + + ) +}; + +export default DataCalculatedUseCase \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/DataPerformanceUseCase.jsx b/Plan/react/dashboard/src/components/theme/usecase/DataPerformanceUseCase.jsx new file mode 100644 index 0000000000..45f1097ca2 --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/DataPerformanceUseCase.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import {Card, Col, Row} from "react-bootstrap"; +import DataUseCase from "./DataUseCase.jsx"; +import { + faDragon, + faExclamationCircle, + faHdd, + faMap, + faMicrochip, + faPowerOff, + faSignal, + faTachometerAlt +} from "@fortawesome/free-solid-svg-icons"; + +const DataPerformanceUseCase = () => { + return ( + + + + + + + + + + + + + + + + + + + ) +}; + +export default DataPerformanceUseCase \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/DataPlayUseCase.jsx b/Plan/react/dashboard/src/components/theme/usecase/DataPlayUseCase.jsx new file mode 100644 index 0000000000..c4e3ea1b0e --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/DataPlayUseCase.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import {Card, Col, Row} from "react-bootstrap"; +import DataUseCase from "./DataUseCase.jsx"; +import {faCalendar, faCalendarPlus, faClock} from "@fortawesome/free-regular-svg-icons"; +import {faGamepad} from "@fortawesome/free-solid-svg-icons"; + +const DataPlayUseCase = () => { + return ( + + + + + + + +
    + + + +
    + + +
    +
    + +
    + ) +}; + +export default DataPlayUseCase \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/DataPlayerStatusUseCase.jsx b/Plan/react/dashboard/src/components/theme/usecase/DataPlayerStatusUseCase.jsx new file mode 100644 index 0000000000..88673f1a1b --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/DataPlayerStatusUseCase.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import {Card, Col, Row} from "react-bootstrap"; +import DataUseCase from "./DataUseCase.jsx"; +import {faAddressCard, faCircle, faGavel} from "@fortawesome/free-solid-svg-icons"; +import {faSuperpowers} from "@fortawesome/free-brands-svg-icons"; + +const DataPlayerStatusUseCase = () => { + return ( + + + + + + + + + + + + + + ) +}; + +export default DataPlayerStatusUseCase \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/DataPlayerVersusUseCase.jsx b/Plan/react/dashboard/src/components/theme/usecase/DataPlayerVersusUseCase.jsx new file mode 100644 index 0000000000..23b043c254 --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/DataPlayerVersusUseCase.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import {Card, Col, Row} from "react-bootstrap"; +import DataUseCase from "./DataUseCase.jsx"; +import {faCrosshairs, faKhanda, faSkull} from "@fortawesome/free-solid-svg-icons"; + +const DataPlayerVersusUseCase = () => { + return ( + + + + + + + +
    + + + +
    +
    + +
    + ) +}; + +export default DataPlayerVersusUseCase \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/DataPlayersUseCase.jsx b/Plan/react/dashboard/src/components/theme/usecase/DataPlayersUseCase.jsx new file mode 100644 index 0000000000..8a6400152f --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/DataPlayersUseCase.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import {Card, Col, Row} from "react-bootstrap"; +import DataUseCase from "./DataUseCase.jsx"; +import {faUser, faUsers} from "@fortawesome/free-solid-svg-icons"; + +const DataPlayersUseCase = () => { + return ( + + + + + + + + +
    + + + + + + +
    +
    + +
    + ) +}; + +export default DataPlayersUseCase \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/DataPluginsUseCase.jsx b/Plan/react/dashboard/src/components/theme/usecase/DataPluginsUseCase.jsx new file mode 100644 index 0000000000..97dedc8511 --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/DataPluginsUseCase.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import {Card, Col, Row} from "react-bootstrap"; +import DataUseCase from "./DataUseCase.jsx"; +import {faCircle} from "@fortawesome/free-solid-svg-icons"; + +const DataPluginsUseCase = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +}; + +export default DataPluginsUseCase \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/DataUseCase.jsx b/Plan/react/dashboard/src/components/theme/usecase/DataUseCase.jsx new file mode 100644 index 0000000000..25b09766d8 --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/DataUseCase.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import Datapoint from "../../Datapoint.jsx"; +import {Card, Col, Row} from "react-bootstrap"; + +const DataUseCase = ({icon, label, card, value}) => { + if (card) { + return ( + + + + + + + + + + ) + } else { + return ( + + ) + } +}; + +export default DataUseCase \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/FormsUseCase.jsx b/Plan/react/dashboard/src/components/theme/usecase/FormsUseCase.jsx new file mode 100644 index 0000000000..7999f0560e --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/FormsUseCase.jsx @@ -0,0 +1,81 @@ +import React from 'react'; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faArrowRight, faQuestion, faSearch, faTrash} from "@fortawesome/free-solid-svg-icons"; +import {useTranslation} from "react-i18next"; +import ActionButton from "../../input/button/ActionButton.jsx"; +import OutlineButton from "../../input/button/OutlineButton.jsx"; +import {Card, Col, Row} from "react-bootstrap"; +import DateInputField from "../../input/DateInputField.jsx"; +import TimeInputField from "../../input/TimeInputField.jsx"; +import MultiSelect from "../../input/MultiSelect.jsx"; +import Checkbox from "../../input/Checkbox.jsx"; +import {BasicDropdown} from "../../input/BasicDropdown.jsx"; +import SecondaryActionButton from "../../input/button/SecondaryActionButton.jsx"; +import DangerButton from "../../input/button/DangerButton.jsx"; +import Toggle from "../../input/Toggle.jsx"; + +const FormsUseCase = () => { + const {t} = useTranslation(); + + const label = t('html.label.themeEditor.example'); + const options = [ + {name: 'label-1', displayName: label}, + {name: 'label-2', displayName: label}, + {name: 'label-3', displayName: label} + ]; + return ( + + + + {label} + + + {label} + + + {label} + + + {label} + + + {label} + + + {label} + + + {label} + + + {label} + +
    + + + + +
    + + + + + +
    + {label} + {label} + {label} + {label} + {label} +
    + + + + + +
    +
    + ) +}; + +export default FormsUseCase \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/GraphUseCases.jsx b/Plan/react/dashboard/src/components/theme/usecase/GraphUseCases.jsx new file mode 100644 index 0000000000..122f7e5c3e --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/GraphUseCases.jsx @@ -0,0 +1,163 @@ +import React, {useEffect, useState} from 'react'; +import PunchCard from "../../graphs/PunchCard.jsx"; +import player from '../../../mockdata/player.json'; +import geolocations from '../../../mockdata/geolocations.json'; +import PlayersOnlineGraph from "../../graphs/PlayersOnlineGraph.jsx"; +import TpsPerformanceGraph from "../../graphs/performance/TpsPerformanceGraph.jsx"; +import CpuRamPerformanceGraph from "../../graphs/performance/CpuRamPerformanceGraph.jsx"; +import WorldPerformanceGraph from "../../graphs/performance/WorldPerformanceGraph.jsx"; +import DiskPerformanceGraph from "../../graphs/performance/DiskPerformanceGraph.jsx"; +import PingGraph from "../../graphs/performance/PingGraph.jsx"; +import GeolocationsCard from "../../cards/common/GeolocationsCard.jsx"; +import {calculateCssHexColor} from "../../../util/colors.js"; +import Background from "./Background.jsx"; + +const randomDate = (start, end) => { + return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())); +} + +const generatePoints = (rangeStart, rangeEnd) => { + const arr = []; + const start = new Date(new Date().getTime() - 2 * 86400000); + const startRange = rangeStart || 0; + const endRange = rangeEnd ? (rangeEnd - startRange) : 100; + for (let i = 0; i < 100; i++) { + const date = randomDate(start, new Date()); + const randNum = startRange + Math.round(Math.random() * endRange); + arr.push([date.getTime(), randNum]); + } + + arr.sort(function (a, b) { + if (a[0] < b[0]) return -1; + if (a[0] > b[0]) return 1; + return 0; + }) + + return arr; +} + +const regeneratePoints = (points, rangeStart, rangeEnd) => { + const newPoints = [] + for (let i = points.length - 1; i >= 0; i--) { + const randNum = (rangeStart || 0) + Math.round(Math.random() * (rangeEnd || 100)); + newPoints.push([points[i][0], randNum]); + } + return newPoints; +} + +const PunchCardGraphUseCase = () => { + return +}; + +const PlayersOnlineGraphUseCase = () => { + return + + +} + +const TpsGraphUseCase = () => { + return +} +const CpuGraphUseCase = () => { + const dataSeries = { + playersOnline: [], + cpu: generatePoints(0, 100), + ram: [] + }; + return +} + +const RamGraphUseCase = () => { + const dataSeries = { + playersOnline: [], + cpu: [], + ram: generatePoints(10000, 11000) + }; + return +} + +const ChunksGraphUseCase = () => { + const dataSeries = { + playersOnline: [], + chunks: generatePoints(100, 2000), + entities: [] + }; + return +} + +const EntitiesGraphUseCase = () => { + const dataSeries = { + playersOnline: [], + chunks: [], + entities: generatePoints(100, 20000) + }; + return +} + +const DiskGraphUseCase = () => { + const dataSeries = { + disk: generatePoints(50, 2000), + } + return +} + +const PingGraphUseCase = () => { + const points = generatePoints(0, 25); + const data = { + min_ping_series: points, + avg_ping_series: regeneratePoints(points, 50, 75), + max_ping_series: regeneratePoints(points, 100, 200), + } + return +} + +const WorldMapUseCase = () => { + const [identifier, setIdentifier] = useState(0); + const [currentMinColor, setCurrentMinColor] = useState(calculateCssHexColor("var(--color-graphs-world-map-low)")) + const [currentMaxColor, setCurrentMaxColor] = useState(calculateCssHexColor("var(--color-graphs-world-map-high)")) + useEffect(() => { + const interval = setInterval(() => { + const minColor = calculateCssHexColor("var(--color-graphs-world-map-low)"); + const maxColor = calculateCssHexColor("var(--color-graphs-world-map-high)"); + if (minColor !== currentMinColor) { + setIdentifier(identifier + 1); + setCurrentMinColor(minColor); + } + if (maxColor !== currentMaxColor) { + setIdentifier(identifier + 1); + setCurrentMaxColor(maxColor); + } + }, 1000); + return () => { + clearInterval(interval); + } + }, [identifier]); + + return +} + +export const graphUseCases = { + 'graphs.style': , + 'graphs.punchCard': , + 'graphs.playersOnline': , + 'graphs.tps': , + 'graphs.cpu': , + 'graphs.ram': , + 'graphs.chunks': , + 'graphs.entities': , + 'graphs.disk': , + 'graphs.ping': , + 'graphs.worldMap': , +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/InfoBoxUseCase.jsx b/Plan/react/dashboard/src/components/theme/usecase/InfoBoxUseCase.jsx new file mode 100644 index 0000000000..0bf4c0d255 --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/InfoBoxUseCase.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import {Alert, Col, Row} from "react-bootstrap"; + +const InfoBoxUseCase = () => { + return ( + + + Info + Notice + Error + + + ) +}; + +export default InfoBoxUseCase \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/SidebarUseCase.jsx b/Plan/react/dashboard/src/components/theme/usecase/SidebarUseCase.jsx new file mode 100644 index 0000000000..a7fd344c24 --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/SidebarUseCase.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import Sidebar from "../../navigation/Sidebar.jsx"; +import {faCogs, faInfoCircle, faNetworkWired, faServer} from "@fortawesome/free-solid-svg-icons"; +import {faCalendarCheck} from "@fortawesome/free-regular-svg-icons"; +import AlertPopupArea from "../../alert/AlertPopupArea.jsx"; +import Header from "../../navigation/Header.jsx"; +import {useMetadata} from "../../../hooks/metadataHook.jsx"; +import {useTranslation} from "react-i18next"; + +export const SidebarUseCase = () => { + const {t} = useTranslation(); + const {displayedServerName} = useMetadata(); + + let items = [ + { + name: 'html.label.networkOverview', + icon: faInfoCircle, + href: "javascript:void(0)", + }, + {}, + {name: 'html.label.information'}, + { + name: 'html.label.servers', + icon: faServer, + contents: [ + { + nameShort: 'html.label.overview', name: 'html.label.servers', icon: faNetworkWired, + href: "javascript:void(0)", + }, + { + name: 'html.label.sessions', icon: faCalendarCheck, href: "javascript:void(0)", + }, + { + name: 'html.label.performance', icon: faCogs, href: "javascript:void(0)", + } + ] + }]; + + return ( +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    + ) +}; \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/TableUseCases.jsx b/Plan/react/dashboard/src/components/theme/usecase/TableUseCases.jsx new file mode 100644 index 0000000000..cd6d088dc5 --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/TableUseCases.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import Row from 'react-bootstrap/Row'; +import Col from 'react-bootstrap/Col'; +import NicknamesCard from "../../cards/player/NicknamesCard.jsx"; +import PluginHistoryCard from "../../cards/common/PluginHistoryCard.jsx"; + +const BasicTableUseCase = () => { + const nicknames = []; + for (let i = 0; i < 10; i++) { + const multiplier = i * 7 * 86400000; + nicknames.push({nickname: 'Player', server: 'Server 1', date: Date.now() - multiplier}); + } + return ( + + ) +}; + +const DataTableUseCase = () => { + const history = []; + for (let i = 0; i < 10; i++) { + const multiplier = i * 7 * 86400000; + history.push({name: 'Plugin', version: '1.2.3', modified: Date.now() - multiplier}); + } + return ( + + ) +} + +export const TableUseCase = () => { + return ( + + + + + + + + + ); +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/TabsUseCase.jsx b/Plan/react/dashboard/src/components/theme/usecase/TabsUseCase.jsx new file mode 100644 index 0000000000..3a1e40e8f3 --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/TabsUseCase.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import {faBraille, faChartArea, faUser} from "@fortawesome/free-solid-svg-icons"; +import {faCalendar} from "@fortawesome/free-regular-svg-icons"; +import {Card} from "react-bootstrap"; +import CardTabs from "../../CardTabs.jsx"; +import {useTranslation} from "react-i18next"; +import Datapoint from "../../Datapoint.jsx"; + +const Body = () => { + return ( + + +
    + +
    + ) +} + +const TabsUseCase = () => { + const {t} = useTranslation(); + const tabs = [ + { + name: t('html.label.dayByDay'), icon: faChartArea, color: 'players-unique', href: '1', + element: , + }, { + name: t('html.label.hourByHour'), icon: faChartArea, color: 'players-unique', href: '2', + element: , + }, { + name: t('html.label.serverCalendar'), icon: faCalendar, color: 'sessions', href: '3', + element: , + }, { + name: t('html.label.punchcard30days'), icon: faBraille, color: 'text', href: '4', + element: , + }, + ]; + return + + +}; + +export default TabsUseCase \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/theme/usecase/TrendUseCase.jsx b/Plan/react/dashboard/src/components/theme/usecase/TrendUseCase.jsx new file mode 100644 index 0000000000..053333ff3e --- /dev/null +++ b/Plan/react/dashboard/src/components/theme/usecase/TrendUseCase.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import BigTrend from "../../trend/BigTrend.jsx"; +import SmallTrend from "../../trend/SmallTrend.jsx"; +import {Card, Col, Row} from "react-bootstrap"; + +const TrendUseCase = () => { + return ( + + + + +

    + +

    +

    + +

    +

    + +

    +
    +

    + 1234 +

    +

    + 1234 +

    +

    + 1234 +

    +
    +
    + +
    + ) +}; + +export default TrendUseCase \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/trend/BigTrend.jsx b/Plan/react/dashboard/src/components/trend/BigTrend.jsx index 0e29b0f560..1242584434 100644 --- a/Plan/react/dashboard/src/components/trend/BigTrend.jsx +++ b/Plan/react/dashboard/src/components/trend/BigTrend.jsx @@ -2,11 +2,11 @@ import React from "react"; import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; import {faCaretDown, faCaretRight, faCaretUp} from "@fortawesome/free-solid-svg-icons"; -const TrendUpGood = ({value}) => {value}; -const TrendUpBad = ({value}) => {value}; -const TrendDownBad = ({value}) => {value}; -const TrendDownGood = ({value}) => {value}; -const TrendSame = ({value}) => {value}; +const TrendUpGood = ({value}) => {value}; +const TrendUpBad = ({value}) => {value}; +const TrendDownBad = ({value}) => {value}; +const TrendDownGood = ({value}) => {value}; +const TrendSame = ({value}) => {value}; const BigTrend = ({trend, format}) => { @@ -18,9 +18,9 @@ const BigTrend = ({trend, format}) => { switch (trend.direction) { case '+': - return (trend.reversed ? : ); + return (trend.reversed ? : ); case '-': - return (trend.reversed ? : ); + return (trend.reversed ? : ); default: return ; } diff --git a/Plan/react/dashboard/src/components/trend/ComparingLabel.jsx b/Plan/react/dashboard/src/components/trend/ComparingLabel.jsx index 57e5fe0871..637d9cc5a9 100644 --- a/Plan/react/dashboard/src/components/trend/ComparingLabel.jsx +++ b/Plan/react/dashboard/src/components/trend/ComparingLabel.jsx @@ -4,8 +4,8 @@ import React from "react"; const ComparingLabel = ({children}) => { return (<> - - + + {' '}{children} ); } diff --git a/Plan/react/dashboard/src/components/trend/SmallTrend.jsx b/Plan/react/dashboard/src/components/trend/SmallTrend.jsx index a37f6b50d1..019f6c9974 100644 --- a/Plan/react/dashboard/src/components/trend/SmallTrend.jsx +++ b/Plan/react/dashboard/src/components/trend/SmallTrend.jsx @@ -2,11 +2,11 @@ import React from "react"; import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; import {faCaretDown, faCaretRight, faCaretUp} from "@fortawesome/free-solid-svg-icons"; -const TrendUpGood = ({value}) => ; -const TrendUpBad = ({value}) => ; -const TrendDownBad = ({value}) => ; -const TrendDownGood = ({value}) => ; -const TrendSame = ({value}) => ; +const TrendUpGood = ({value}) => ; +const TrendUpBad = ({value}) => ; +const TrendDownBad = ({value}) => ; +const TrendDownGood = ({value}) => ; +const TrendSame = ({value}) => ; const SmallTrend = ({trend}) => { diff --git a/Plan/react/dashboard/src/default.json b/Plan/react/dashboard/src/default.json new file mode 100644 index 0000000000..e0cec5672f --- /dev/null +++ b/Plan/react/dashboard/src/default.json @@ -0,0 +1,489 @@ +{ + "defaultTheme": "plan", + "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/react/dashboard/src/hooks/context/colorEditContextHook.jsx b/Plan/react/dashboard/src/hooks/context/colorEditContextHook.jsx new file mode 100644 index 0000000000..f0e239d64e --- /dev/null +++ b/Plan/react/dashboard/src/hooks/context/colorEditContextHook.jsx @@ -0,0 +1,79 @@ +import {createContext, useContext, useMemo, useState} from "react"; +import {hsvToHex, randomHSVColor} from "../../util/colors.js"; + +const ColorEditContext = createContext({}); + +export const ColorEditContextProvider = ({colors, saveFunction, deleteFunction, children}) => { + const [previous, setPrevious] = useState(undefined); + const [name, setName] = useState(undefined); + const [color, setColor] = useState(""); + const [deleting, setDeleting] = useState(false); + const open = name !== undefined; + + const onNameChange = (value) => { + setName(value.toLowerCase().replace(/[^a-z0-9-]/g, "-")); + } + + const onColorChange = (value) => { + setColor(value.toLowerCase()); + } + + const editColor = (name, color) => { + setName(name); + setColor(color); + setPrevious(name); + } + + const discardEdit = () => { + setName(undefined); + setColor(undefined); + setPrevious(undefined); + } + + const finishEdit = () => { + if (name.length) { + saveFunction(name, color, previous); + } else { + saveFunction('new-color-' + Math.floor(Math.random() * 1000), color, previous); + } + discardEdit(); + } + + + const deleteColor = (name) => { + deleteFunction(name); + } + + const editNewColor = () => { + setName(""); + setColor(hsvToHex(randomHSVColor(Math.floor(Math.random() * 100)))); + } + + const alreadyExists = previous !== name && !!colors[name]; + + const sharedState = useMemo(() => { + return { + alreadyExists, + name, + color, + deleting, + setDeleting, + onNameChange, + onColorChange, + open, + editColor, + finishEdit, + discardEdit, + editNewColor, + deleteColor + } + }, [alreadyExists, name, color, deleting, open]); + return ( + {children} + + ) +} + +export const useColorEditContext = () => { + return useContext(ColorEditContext); +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/hooks/context/groupEditContextHook.jsx b/Plan/react/dashboard/src/hooks/context/groupEditContextHook.jsx index bd78085bcb..6f5a54f238 100644 --- a/Plan/react/dashboard/src/hooks/context/groupEditContextHook.jsx +++ b/Plan/react/dashboard/src/hooks/context/groupEditContextHook.jsx @@ -5,6 +5,7 @@ import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; import {faCheck, faExclamationTriangle} from "@fortawesome/free-solid-svg-icons"; import {useAlertPopupContext} from "./alertPopupContext"; import {Trans, useTranslation} from "react-i18next"; +import {useAuth} from "../authenticationHook.jsx"; const GroupEditContext = createContext({}); @@ -48,6 +49,7 @@ export const GroupEditContextProvider = ({groupName, children}) => { const [lastSave, setLastSave] = useState(Date.now()); const [lastDiscard, setLastDiscard] = useState(Date.now()); const {addAlert} = useAlertPopupContext(); + const {updateLoginDetails} = useAuth(); const [allPermissions, setAllPermissions] = useState([]); useEffect(() => { @@ -205,6 +207,7 @@ export const GroupEditContextProvider = ({groupName, children}) => { }); } setLastSave(Date.now()); + updateLoginDetails(); } }, [lastSave, changed, setChanged, saveRequested, setLastSave, permissions, groupName, addAlert, t]); diff --git a/Plan/react/dashboard/src/hooks/context/minHeightContextHook.jsx b/Plan/react/dashboard/src/hooks/context/minHeightContextHook.jsx new file mode 100644 index 0000000000..f441a11d9f --- /dev/null +++ b/Plan/react/dashboard/src/hooks/context/minHeightContextHook.jsx @@ -0,0 +1,55 @@ +import React, {createContext, useCallback, useContext, useMemo, useState} from "react"; + +const MinHeightContext = createContext(); + +export const MinHeightProvider = ({children}) => { + const [minHeightRules, setMinHeightRules] = useState({}); + + const unregisterMinHeight = (selector, nightMode, count) => { + let removed = false + setMinHeightRules(rules => { + const existingRule = rules[selector]; + const existingNightMode = existingRule?.nightMode; + const existingCount = existingRule?.count || 0; + removed = existingNightMode === nightMode && count < existingCount; + if (removed) { + // Remove the current rule when count is less than existing count + const {[selector]: removed, ...remainingRules} = rules; + return remainingRules; + } + return rules; // Return the rules unchanged if condition not met + }); + return removed; + } + + const registerMinHeight = useCallback((selector, minHeight, nightMode, count) => { + setMinHeightRules(rules => { + const existingRule = rules[selector]; + const existingHeight = existingRule?.minHeight; + + // Change the height if: + // - It's taller than existing one + // - or if the height of the currently registered one changed + const shouldUpdate = !existingHeight || minHeight > existingHeight; + + if (!shouldUpdate) return rules; + return {...rules, [selector]: {minHeight, nightMode, count}}; + }); + }, []); + + const value = useMemo(() => { + return {minHeightRules, registerMinHeight, unregisterMinHeight} + }, [minHeightRules]); + return ( + + + {children} + + ); +}; + +export const useMinHeightContext = () => useContext(MinHeightContext); \ No newline at end of file diff --git a/Plan/react/dashboard/src/hooks/context/themeContextHook.jsx b/Plan/react/dashboard/src/hooks/context/themeContextHook.jsx new file mode 100644 index 0000000000..3e4f9dc69c --- /dev/null +++ b/Plan/react/dashboard/src/hooks/context/themeContextHook.jsx @@ -0,0 +1,143 @@ +import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from "react"; +import {getLocallyStoredThemes, useTheme} from "../themeHook.jsx"; +import {fetchTheme} from "../../service/metadataService.js"; +import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; +import {faExclamationTriangle} from "@fortawesome/free-solid-svg-icons"; +import {useAlertPopupContext} from "./alertPopupContext.jsx"; +import {Trans} from "react-i18next"; + +const ThemeStorageContext = createContext({}); + +// Reduce refetching theme inside the theme editor to avoid rate-limit issues. +const themeCache = {}; + +export const ThemeStorageContextProvider = ({children}) => { + const theme = useTheme(); + const {currentTheme, color} = theme; + const {addAlert} = useAlertPopupContext(); + const [loaded, setLoaded] = useState(false); + const [currentColors, setCurrentColors] = useState({}); + const [currentNightColors, setCurrentNightColors] = useState({}); + const [currentUseCases, setCurrentUseCases] = useState({}); + const [currentNightModeUseCases, setCurrentNightModeUseCases] = useState({}); + const [error, setError] = useState(null); + + const loadTheme = useCallback(async (name) => { + setError(null); + let theme; + if (getLocallyStoredThemes().includes(name)) { + const found = window.localStorage.getItem(`locally-stored-theme-${name}`); + if (found) theme = JSON.parse(found); // TODO catch json parse error + } + + if (!theme) { + if (themeCache[name]) { + theme = themeCache[name]; + } else { + const response = await fetchTheme(name); + if (response.error) { + console.error(response.error); + setError(response.error); + return; + } + theme = response.data; + themeCache[name] = theme; + } + } + setCurrentColors(theme.colors); + setCurrentNightColors(theme.nightColors); + setCurrentUseCases(theme.useCases); + setCurrentNightModeUseCases(theme.nightModeUseCases); + setLoaded(true); + }, []); + + const saveUploadedThemeLocally = (name, themeJson, originalName) => { + const locallyStoredThemes = getLocallyStoredThemes(); + window.localStorage.setItem(`locally-stored-theme-${name}`, JSON.stringify(themeJson)); + if (!locallyStoredThemes.includes(name)) { + locallyStoredThemes.push(name); + } + window.localStorage.setItem(`locally-stored-themes`, JSON.stringify(locallyStoredThemes)); + if (name !== originalName) { + deleteThemeLocally(originalName); + } + } + + const deleteThemeLocally = (name) => { + const locallyStoredThemes = getLocallyStoredThemes(); + window.localStorage.removeItem(`locally-stored-theme-${name}`); + const index = locallyStoredThemes.indexOf(name); + if (index > -1) { + locallyStoredThemes.splice(index, 1); + } + window.localStorage.setItem(`locally-stored-themes`, JSON.stringify(locallyStoredThemes)); + } + + const cloneThemeLocally = async (themeToClone, name) => { + let theme; + if (getLocallyStoredThemes().includes(name)) { + const found = window.localStorage.getItem(`locally-stored-theme-${name}`); + if (found) theme = JSON.parse(found); // TODO catch json parse error + } + if (themeCache[name]) { + theme = themeCache[name]; + } else { + const response = await fetchTheme(themeToClone); + if (response.error) { + console.error(response.error); + addAlert({ + timeout: 15000, + color: "error", + content: <> + + {" "} + + + }); + return false; + } + theme = response.data; + themeCache[name] = theme; + } + saveUploadedThemeLocally(name, theme); + return true; + } + + const reloadTheme = () => { + delete themeCache[currentTheme]; + loadTheme(currentTheme); + } + + useEffect(() => { + if (theme.loaded && currentTheme) { + loadTheme(currentTheme); + } + }, [theme.loaded, currentTheme]); + + const sharedState = useMemo(() => { + const themeOptions = (theme.nightModeEnabled + ? currentNightModeUseCases?.themeColorOptions + : currentUseCases?.themeColorOptions) || []; + const colorExistsAsOption = themeOptions.includes(color); + return { + loaded, error, + name: currentTheme, + color: colorExistsAsOption ? color : undefined, + currentColors, + currentNightColors, + currentUseCases, + currentNightModeUseCases, + usedColors: theme.nightModeEnabled ? currentColors : {...currentColors, ...currentNightColors}, + usedUseCases: theme.nightModeEnabled ? currentUseCases : {...currentUseCases, ...currentNightModeUseCases}, + cloneThemeLocally, saveUploadedThemeLocally, deleteThemeLocally, reloadTheme + } + }, [name, color, currentColors, currentNightColors, currentUseCases, currentNightModeUseCases, loaded, error, theme.nightModeEnabled]); + return ( + {children} + + ) +} + +export const useThemeStorage = () => { + return useContext(ThemeStorageContext); +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/hooks/context/themeEditContextHook.jsx b/Plan/react/dashboard/src/hooks/context/themeEditContextHook.jsx new file mode 100644 index 0000000000..3e54bf0b25 --- /dev/null +++ b/Plan/react/dashboard/src/hooks/context/themeEditContextHook.jsx @@ -0,0 +1,354 @@ +import React, {createContext, useContext, useMemo, useState} from "react"; +import {useThemeStorage} from "./themeContextHook.jsx"; +import {cssVariableToName, nameToCssVariable} from "../../util/colors.js"; +import {flattenObject, recursiveFindAndReplaceValue} from "../../util/mutator.js"; +import {Trans, useTranslation} from "react-i18next"; +import {saveTheme} from "../../service/metadataService.js"; +import {useAuth} from "../authenticationHook.jsx"; +import {useAlertPopupContext} from "./alertPopupContext.jsx"; +import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; +import {faCheck, faExclamationTriangle} from "@fortawesome/free-solid-svg-icons"; +import {getLocallyStoredThemes} from "../themeHook.jsx"; + +const ThemeEditContext = createContext({}); + +export const ThemeEditContextProvider = ({children}) => { + const {t} = useTranslation(); + const {authRequired, hasPermission} = useAuth(); + const [edits, setEdits] = useState([]); + const [redos, setRedos] = useState([]); + const {addAlert} = useAlertPopupContext(); + const { + loaded, + name: originalName, + currentColors, + currentNightColors, + currentUseCases, + currentNightModeUseCases, + saveUploadedThemeLocally, + deleteThemeLocally, + reloadTheme + } = useThemeStorage(); + + const [name, setName] = useState(originalName); + const onNameChange = value => { + setName(value.toLowerCase().replace(/[^a-z0-9-]/g, "-")); + } + + const applyEdits = (type, object) => { + console.debug('Applying edits', edits.length) + let result = object; + const applicable = edits.filter(edit => edit.type.includes(type)); + for (let applicableEdit of applicable) { + console.debug('Applying', applicableEdit.name, 'to', applicableEdit.type) + result = applicableEdit.operation(result, type); + } + return result; + } + + const addEdit = edit => { + setEdits(prev => [...prev, edit]); + setRedos([]); + } + + const undo = () => { + if (edits.length) { + const undone = edits[edits.length - 1]; + setEdits(edits.slice(0, -1)); + setRedos(prev => [...prev, undone]); + } + } + + const redo = () => { + const toRedo = redos[redos.length - 1]; + if (toRedo.length) { + toRedo.forEach(edit => { + addEdit(edit) + }) + } else { + addEdit(toRedo); + } + setRedos(redos.slice(0, -1)); + } + + const discardChanges = () => { + if (!edits.length) { + setRedos([]); + } else { + const undone = [...edits]; + setEdits([]); + setRedos(prev => [...prev, undone]); + } + setName(originalName); + } + + const updateUseCaseColorName = (current, oldName, newName) => { + const oldVariable = nameToCssVariable(oldName); + const newVariable = nameToCssVariable(newName); + return recursiveFindAndReplaceValue(current, oldVariable, newVariable); + } + + const handleColorSave = (current) => (name, color, previous) => { + const newObj = {}; + for (const [key, value] of Object.entries(current)) { + if (key === previous) { + newObj[name] = color; + } else { + newObj[key] = value; + } + } + if (newObj[name] === undefined) { + newObj[name] = color; + } + return newObj; + } + const saveColor = (name, color, previous) => { + const renamed = previous && name !== previous; + if (renamed) { + addEdit({ + name: t('html.label.themeEditor.changes.renameColor', {previous, name, color}), + type: 'color,useCase,nightModeUseCase', operation: (current, type) => { + if (type === 'color') { + return handleColorSave(current)(name, color, previous); + } else { + return updateUseCaseColorName(current, previous, name); + } + } + }) + } else { + addEdit({ + name: t(previous ? 'html.label.themeEditor.changes.setColor' : 'html.label.themeEditor.changes.addColor', { + name, + color + }), + type: 'color', operation: (current) => handleColorSave(current)(name, color, previous) + }) + } + } + const saveNightColor = (name, color, previous) => { + const renamed = name !== previous; + if (renamed) { + addEdit({ + name: t('html.label.themeEditor.changes.renameColor', {previous, name, color}), + type: 'nightColor,nightModeUseCase', operation: (current, type) => { + if (type === 'nightColor') { + return handleColorSave(current)(name, color, previous); + } else { + return updateUseCaseColorName(current, previous, name); + } + } + }) + } else { + addEdit({ + name: t(previous ? 'html.label.themeEditor.changes.setColor' : 'html.label.themeEditor.changes.addColor', { + name, + color + }), + type: 'nightColor', operation: (current) => handleColorSave(current)(name, color, previous) + }) + } + } + + const handleDelete = (current) => (name) => { + const copy = {...current}; + delete copy[name]; + return copy; + } + const deleteColor = name => addEdit({ + name: t('html.label.themeEditor.changes.deleteColor', {name}), + type: 'color', + operation: current => handleDelete(current)(name) + }) + const deleteNightColor = name => addEdit({ + name: t('html.label.themeEditor.changes.deleteColor', {name}), + type: 'nightColor', + operation: current => handleDelete(current)(name) + }) + + const handleColorChange = (currentObject, newValue, path) => { + if (path.length === 0) return newValue; + const [key, ...rest] = path; + return { + ...currentObject, + [key]: rest.length === 0 + ? newValue + : handleColorChange(currentObject?.[key] || {}, newValue, rest) + }; + }; + + const handleRemoveOverride = (current, path) => { + // Create a new object without the override, but maintain structure + const removeOverride = (obj, pathArr) => { + if (pathArr.length === 0) return obj; + + const [currentKey, ...rest] = pathArr; + const result = {...obj}; + + if (rest.length === 0) { + // We've reached the target property, remove it + delete result[currentKey]; + // If the parent object becomes empty, return null to signal removal + return Object.keys(result).length === 0 ? null : result; + } + + // Continue traversing + const nested = removeOverride(obj[currentKey] || {}, rest); + if (nested === null) { + delete result[currentKey]; + return Object.keys(result).length === 0 ? null : result; + } + result[currentKey] = nested; + return result; + }; + + // Get the new state without the override + return removeOverride(current, path) || {}; + }; + + const updateUseCase = (newValue, path) => Array.isArray(newValue) + ? addEdit({ + name: t('html.label.themeEditor.changes.changeUseCaseArray', {path: path.join('.')}), + type: 'useCase', + operation: current => handleColorChange(current, newValue, path) + }) + : addEdit({ + name: t('html.label.themeEditor.changes.changeUseCase', { + path: path.join('.'), + name: cssVariableToName(newValue) + }), + type: 'useCase', + operation: current => handleColorChange(current, newValue, path) + }); + + const updateNightUseCase = (newValue, path) => Array.isArray(newValue) + ? addEdit({ + name: t('html.label.themeEditor.changes.changeNightModeArray', {path: path.join('.')}), + type: 'nightModeUseCase', + operation: current => handleColorChange(current, newValue, path) + }) + : addEdit({ + name: t('html.label.themeEditor.changes.changeNightMode', { + path: path.join('.'), + name: cssVariableToName(newValue) + }), + type: 'nightModeUseCase', + operation: current => handleColorChange(current, newValue, path) + }); + const removeNightOverride = (path) => addEdit({ + name: t('html.label.themeEditor.changes.removeNightMode', {path: path.join('.')}), + type: 'nightModeUseCase', + operation: current => handleRemoveOverride(current, path) + }); + + const sharedState = useMemo(() => { + const editedColors = applyEdits('color', currentColors); + const editedNightColors = applyEdits('nightColor', currentNightColors); + const editedUseCases = applyEdits('useCase', currentUseCases); + const editedNightModeUseCases = applyEdits('nightModeUseCase', currentNightModeUseCases); + + const issues = []; + + const allColorsExist = () => { + const referenceColors = Object.keys(editedUseCases?.referenceColors || {}) + const colorMissing = name => { + const exists = editedColors[name] || editedNightColors[name] || referenceColors.includes(name); + if (!exists) console.warn(name, "doesn't exist on color maps") + return !exists; + } + const missingUseCase = Object.entries(flattenObject(editedUseCases)) + .filter(e => colorMissing(cssVariableToName(e[1]))); + const missingNightModeUseCase = Object.entries(flattenObject(editedNightModeUseCases)) + .filter(e => colorMissing(cssVariableToName(e[1]))); + + missingUseCase.forEach(e => issues.push( + t('html.label.themeEditor.issues.missingUseCase', {name: e[0], colorName: cssVariableToName(e[1])}))); + missingNightModeUseCase.forEach(e => issues.push( + t('html.label.themeEditor.issues.missingNightCase', {name: e[0], colorName: cssVariableToName(e[1])}))); + + return !missingUseCase.length && !missingNightModeUseCase.length + } + + const onlyLocal = getLocallyStoredThemes().includes(originalName); + const somethingToSave = edits.length > 0 || name !== originalName; + const savePossible = (somethingToSave || (onlyLocal && authRequired && hasPermission('manage.themes'))) && allColorsExist(); + + const save = async () => { + if (!savePossible) { + return + } + + const themeToSave = { + name: name, + colors: editedColors, + nightColors: editedNightColors, + useCases: editedUseCases, + nightModeUseCases: editedNightModeUseCases + }; + + saveUploadedThemeLocally(name, themeToSave, originalName); + // Save remotely + if (authRequired && hasPermission('manage.themes')) { + const {error} = await saveTheme(name, themeToSave, originalName); + if (!error) { + deleteThemeLocally(name); + addAlert({ + timeout: 5000, + color: "success", + content: <>{" "}{t('html.label.managePage.alert.saveSuccess')} + }); + } else { + addAlert({ + timeout: 15000, + color: "warning", + content: <> + + {" "} + + + }); + } + } + setEdits([]); + setRedos([]); + reloadTheme(); + } + + return { + loaded, + name, + originalName, + setName: onNameChange, + currentColors: editedColors, + currentNightColors: editedNightColors, + currentUseCases: editedUseCases, + currentNightModeUseCases: editedNightModeUseCases, + editCount: edits.length, + redoCount: redos.length, + deleteColor, + deleteNightColor, + saveColor, + saveNightColor, + updateUseCase, + updateNightUseCase, + removeNightOverride, + undo, + redo, + discardChanges, + edits, + redos, + issues, + savePossible, + onlyLocal, + discardPossible: somethingToSave, + save + } + }, [edits, name]); + return ( + {children} + + ) +} + +export const useThemeEditContext = () => { + return useContext(ThemeEditContext); +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/hooks/dataFetchHook.js b/Plan/react/dashboard/src/hooks/dataFetchHook.js index f0f2bec3fe..bccba7f5a7 100644 --- a/Plan/react/dashboard/src/hooks/dataFetchHook.js +++ b/Plan/react/dashboard/src/hooks/dataFetchHook.js @@ -55,7 +55,12 @@ export const useDataRequest = (fetchMethod, parameters, shouldRequest) => { } else if (error) { console.warn(error); datastore.finishUpdate(fetchMethod) - setLoadingError(error); + const isObject = error?.data !== null && typeof error?.data === 'object' && !Array.isArray(error?.data); + if (isObject) { + setLoadingError({...error, ...error.data, data: undefined}) + } else { + setLoadingError(error); + } finishUpdate(0, "Error: " + error.message, datastore.isSomethingUpdating()); } }; diff --git a/Plan/react/dashboard/src/hooks/interaction/graphExtremesContextHook.jsx b/Plan/react/dashboard/src/hooks/interaction/graphExtremesContextHook.jsx new file mode 100644 index 0000000000..05b0bb0375 --- /dev/null +++ b/Plan/react/dashboard/src/hooks/interaction/graphExtremesContextHook.jsx @@ -0,0 +1,36 @@ +import {createContext, useCallback, useContext, useMemo, useRef, useState} from "react"; + +const GraphExtremesContext = createContext({}); + +export const GraphExtremesContextProvider = ({children}) => { + const currentDebounce = useRef(null); + + const [min, setMin] = useState(undefined); + const [max, setMax] = useState(undefined); + + const onSetExtremes = useCallback((event) => { + if (currentDebounce.current) clearTimeout(currentDebounce.current); + currentDebounce.current = setTimeout(() => { + if (event?.trigger) { + setMin(event.min); + setMax(event.max); + } + }, 500); + }, [setMin, setMax]); + + const sharedState = useMemo(() => { + if (min !== undefined && max !== undefined) { + return {extremes: {min, max}, onSetExtremes} + } + return {onSetExtremes}; + }, [min, max]); + + return ( + {children} + + ) +} + +export const useGraphExtremesContext = () => { + return useContext(GraphExtremesContext); +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/hooks/interaction/hoverHook.jsx b/Plan/react/dashboard/src/hooks/interaction/hoverHook.jsx new file mode 100644 index 0000000000..675f2efc13 --- /dev/null +++ b/Plan/react/dashboard/src/hooks/interaction/hoverHook.jsx @@ -0,0 +1,27 @@ +import {createContext, useContext, useState} from "react"; + +const HoverContext = createContext({}); + +export const HoverTrigger = ({children}) => { + const [hovered, setHovered] = useState(false); + + const onHoverEnter = () => { + setHovered(true); + } + + const onHoverLeave = () => { + setHovered(false); + } + + return ( +
    + {children} +
    +
    + ) +} + +export const useHoverContext = () => { + return useContext(HoverContext); +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/hooks/interaction/windowWidthHook.jsx b/Plan/react/dashboard/src/hooks/interaction/windowWidthHook.jsx new file mode 100644 index 0000000000..7d5e2530de --- /dev/null +++ b/Plan/react/dashboard/src/hooks/interaction/windowWidthHook.jsx @@ -0,0 +1,11 @@ +import {useCallback, useEffect, useState} from 'react'; + +export const useWindowWidth = () => { + const [windowWidth, setWindowWidth] = useState(window.innerWidth); + const updateWidth = useCallback(() => setWindowWidth(window.innerWidth), []); + useEffect(() => { + window.addEventListener('resize', updateWidth); + return () => window.removeEventListener('resize', updateWidth); + }, [updateWidth]); + return windowWidth; +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/hooks/metadataHook.jsx b/Plan/react/dashboard/src/hooks/metadataHook.jsx index 95937b2c16..e46cf2c7e2 100644 --- a/Plan/react/dashboard/src/hooks/metadataHook.jsx +++ b/Plan/react/dashboard/src/hooks/metadataHook.jsx @@ -4,12 +4,20 @@ import {fetchNetworkMetadata, fetchPlanMetadata} from "../service/metadataServic import terminal from '../Terminal-icon.png' import {useAuth} from "./authenticationHook"; +import {getLocallyStoredThemes} from "./themeHook.jsx"; + const MetadataContext = createContext({}); export const MetadataContextProvider = ({children}) => { const [datastore] = useState({}); const [metadata, setMetadata] = useState({}); const {authRequired, authLoaded, loggedIn} = useAuth(); + const [updateThemeList, setUpdateThemeList] = useState(Date.now()); + + const refreshThemeList = () => { + setUpdateThemeList(Date.now()); + updateMetadata(); + } const updateMetadata = useCallback(async () => { if (authRequired && (!authLoaded || !loggedIn)) return; @@ -43,10 +51,21 @@ export const MetadataContextProvider = ({children}) => { updateMetadata(); }, [updateMetadata, authLoaded, loggedIn]); + const displayedServerName = metadata.isProxy ? metadata.networkName : (metadata.serverName?.startsWith('Server') ? "Plan" : metadata.serverNameserverName); + const sharedState = useMemo(() => { - return {...metadata, getPlayerHeadImageUrl, datastore} + const loaded = Object.keys(metadata).length > 0 && !metadata.metadataError; + return { + ...metadata, + getAvailableThemes: () => loaded ? [...new Set([...metadata.availableThemes, ...getLocallyStoredThemes()])] : getLocallyStoredThemes(), + getPlayerHeadImageUrl, + datastore, + displayedServerName, + loaded, + refreshThemeList + } }, - [metadata, getPlayerHeadImageUrl, datastore]); + [metadata, getPlayerHeadImageUrl, datastore, displayedServerName, updateThemeList]); return ( {children} diff --git a/Plan/react/dashboard/src/hooks/pageExtensionHook.jsx b/Plan/react/dashboard/src/hooks/pageExtensionHook.jsx index 7227ea4b03..a6cffd1533 100644 --- a/Plan/react/dashboard/src/hooks/pageExtensionHook.jsx +++ b/Plan/react/dashboard/src/hooks/pageExtensionHook.jsx @@ -3,7 +3,7 @@ import {useAuth} from "./authenticationHook"; import {useNavigation} from "./navigationHook"; import {useTheme} from "./themeHook"; import {useMetadata} from "./metadataHook"; -import {getColors, withReducedSaturation} from "../util/colors"; +import {withReducedSaturation} from "../util/colors"; import axios from "axios"; import {useTranslation} from "react-i18next"; @@ -35,7 +35,7 @@ export const PageExtensionContextProvider = ({children}) => { }, theme: { currentThemeColor: themeContext.selectedColor, - colorMap: getColors(), + colorMap: {}, // deprecated, use var(--color-plugins-red) etc. in css instead. withReducedSaturation }, metadata: { diff --git a/Plan/react/dashboard/src/hooks/themeHook.jsx b/Plan/react/dashboard/src/hooks/themeHook.jsx index f4ec89db3a..23bc7e5bc5 100644 --- a/Plan/react/dashboard/src/hooks/themeHook.jsx +++ b/Plan/react/dashboard/src/hooks/themeHook.jsx @@ -1,149 +1,128 @@ import {createContext, useContext, useEffect, useMemo, useState} from "react"; -import {createNightModeCss, getColors} from "../util/colors"; -import {getLightModeChartTheming, getNightModeChartTheming} from "../util/graphColors"; +import {getChartTheming} from "../util/graphColors"; import {useMetadata} from "./metadataHook"; -import {useLocation} from "react-router-dom"; - -const themeColors = getColors(); -themeColors.splice(themeColors.length - 4, 4); - -const getDefaultTheme = (metadata) => { - const defaultTheme = metadata.defaultTheme; - - // Use 'plan' if default or if default is undefined. - // Avoid night mode staying on if default theme is night mode - const invalidColor = !defaultTheme - || defaultTheme === 'default' - || defaultTheme === 'black' - || defaultTheme === 'white' - || (defaultTheme !== 'night' && !themeColors.map(color => color.name).includes(defaultTheme)); - - return invalidColor ? 'plan' : defaultTheme; -} const getStoredTheme = (defaultTheme) => { - const stored = window.localStorage.getItem('themeColor'); + const stored = window.localStorage.getItem('theme.name'); return stored && stored !== 'undefined' ? stored : defaultTheme; } const setStoredTheme = themeColor => { if (themeColor) { - window.localStorage.setItem('themeColor', themeColor); + window.localStorage.setItem('theme.name', themeColor); + } +} + +const getStoredColor = () => { + const stored = window.localStorage.getItem('theme.color'); + return stored && stored !== 'undefined' ? stored : 'theme'; +} + +const setStoredColor = themeColor => { + if (themeColor) { + window.localStorage.setItem('theme.color', themeColor); } } +const getStoredNightMode = () => { + const stored = window.localStorage.getItem('theme.nightMode'); + return stored && stored !== 'undefined' ? stored !== 'false' : false; +} + +const setStoredNightMode = value => { + window.localStorage.setItem('theme.nightMode', '' + value); +} + +const removeOldVariables = () => { + window.localStorage.removeItem('themeColor'); +} + const ThemeContext = createContext({}); -export const ThemeContextProvider = ({children}) => { +export const ThemeContextProvider = ({children, themeOverride}) => { const metadata = useMetadata(); const [colorChooserOpen, setColorChooserOpen] = useState(false); - const [selectedColor, setSelectedColor] = useState(getStoredTheme('plan')); - const [previousColor, setPreviousColor] = useState(undefined); + const [selectedTheme, setSelectedTheme] = useState(themeOverride); + const [selectedColor, setSelectedColor] = useState(undefined); + const [nightMode, setNightMode] = useState(getStoredNightMode()); - useEffect(() => { - if (!metadata) return; + removeOldVariables(); - setSelectedColor(getStoredTheme(getDefaultTheme(metadata))); - }, [metadata, setSelectedColor]); + useEffect(() => { + if (!metadata.loaded) return; + if (themeOverride) return; + const theme = getStoredTheme(metadata.defaultTheme); + const invalidTheme = !metadata.getAvailableThemes().includes(theme); + setSelectedTheme(invalidTheme ? 'default' : theme); + setSelectedColor(getStoredColor()); + }, [metadata.loaded, metadata, setSelectedTheme, setSelectedColor]); const sharedState = useMemo(() => { return { + selectedTheme, setSelectedTheme, selectedColor, setSelectedColor, - previousColor, setPreviousColor, - colorChooserOpen, setColorChooserOpen + colorChooserOpen, setColorChooserOpen, + nightMode, setNightMode } - }, [selectedColor, setSelectedColor, previousColor, setPreviousColor, colorChooserOpen, setColorChooserOpen]); + }, [selectedTheme, selectedColor, nightMode, setSelectedColor, colorChooserOpen, setColorChooserOpen]); return ( {children} ) } -const lightModeChartTheming = getLightModeChartTheming(); -const nightModeChartTheming = getNightModeChartTheming(); +const chartTheming = getChartTheming(); export const useTheme = () => { const { + selectedTheme, + setSelectedTheme, selectedColor, setSelectedColor, - previousColor, - setPreviousColor, colorChooserOpen, - setColorChooserOpen + setColorChooserOpen, + nightMode, + setNightMode } = useContext(ThemeContext); - const metadata = useMetadata(); - const setTheme = color => { setStoredTheme(color); - setSelectedColor(color); + setSelectedTheme(color); } - if (!selectedColor) setTheme(selectedColor); + const setColor = color => { + setStoredColor(color); + setSelectedColor(color); + } const toggleColorChooser = () => { setColorChooserOpen(!colorChooserOpen); } const isNightModeEnabled = () => { - return selectedColor === 'night'; + return nightMode; } const toggleNightMode = () => { - if (isNightModeEnabled()) { - const defaultTheme = getDefaultTheme(metadata); - const lightTheme = defaultTheme === 'night' ? 'plan' : defaultTheme; - setTheme(previousColor ? previousColor : lightTheme); - } else { - setPreviousColor(selectedColor); - setTheme('night'); - } + setStoredNightMode(!nightMode); + setNightMode(!nightMode); } const nightModeEnabled = isNightModeEnabled(); return { + loaded: selectedTheme !== undefined, + currentTheme: selectedTheme, color: selectedColor, - setColor: setTheme, - nightModeEnabled: nightModeEnabled, - colorChooserOpen: colorChooserOpen, - nightModeCss: nightModeEnabled ? createNightModeCss() : undefined, - toggleNightMode: toggleNightMode, - toggleColorChooser: toggleColorChooser, - themeColors: themeColors, - graphTheming: nightModeEnabled ? nightModeChartTheming : lightModeChartTheming + setTheme, + setColor, + nightModeEnabled, + colorChooserOpen, + toggleNightMode, + toggleColorChooser, + graphTheming: chartTheming }; } - -export const ThemeCss = () => { - const {color} = useTheme(); - - if (!color) return <>; - - return ( - - ) -} - -export const NightModeCss = () => { - const theme = useTheme(); - const location = useLocation(); - - if (location.pathname.startsWith('/docs')) { - return <> - - - } - - return ( - <> - {theme.nightModeEnabled ? : ''} - - ); +export const getLocallyStoredThemes = () => { + return JSON.parse(window.localStorage.getItem('locally-stored-themes') || '[]'); } \ No newline at end of file diff --git a/Plan/react/dashboard/src/mockdata/geolocations.json b/Plan/react/dashboard/src/mockdata/geolocations.json new file mode 100644 index 0000000000..6b9b1dc9f4 --- /dev/null +++ b/Plan/react/dashboard/src/mockdata/geolocations.json @@ -0,0 +1,367 @@ +{ + "geolocation_bar_series": [ + { + "label": "Finland", + "value": 100 + }, + { + "label": "United States", + "value": 75 + }, + { + "label": "Germany", + "value": 47 + }, + { + "label": "Netherlands", + "value": 39 + }, + { + "label": "Norway", + "value": 34 + }, + { + "label": "Sweden", + "value": 30 + }, + { + "label": "United Kingdom", + "value": 30 + }, + { + "label": "Estonia", + "value": 24 + }, + { + "label": "Spain", + "value": 19 + }, + { + "label": "Canada", + "value": 18 + }, + { + "label": "France", + "value": 14 + }, + { + "label": "Denmark", + "value": 14 + }, + { + "label": "Poland", + "value": 11 + }, + { + "label": "Italy", + "value": 10 + }, + { + "label": "Russia", + "value": 10 + }, + { + "label": "Turkey", + "value": 9 + }, + { + "label": "Belgium", + "value": 8 + }, + { + "label": "Australia", + "value": 7 + }, + { + "label": "India", + "value": 6 + }, + { + "label": "Slovakia", + "value": 6 + } + ], + "geolocations_enabled": true, + "geolocation_series": [ + { + "code": "DNK", + "value": 14 + }, + { + "code": "NZL", + "value": 1 + }, + { + "code": "CHE", + "value": 5 + }, + { + "code": "CHL", + "value": 1 + }, + { + "code": "CHN", + "value": 1 + }, + { + "code": "HRV", + "value": 2 + }, + { + "code": "IDN", + "value": 2 + }, + { + "code": "PRT", + "value": 4 + }, + { + "code": "VNM", + "value": 1 + }, + { + "code": "CYP", + "value": 1 + }, + { + "code": "MDV", + "value": 1 + }, + { + "code": "AUS", + "value": 7 + }, + { + "code": "AUT", + "value": 2 + }, + { + "code": "HKG", + "value": 2 + }, + { + "code": "MYS", + "value": 1 + }, + { + "code": "ZWE", + "value": 1 + }, + { + "code": "POL", + "value": 11 + }, + { + "code": "BGR", + "value": 3 + }, + { + "code": "JOR", + "value": 1 + }, + { + "code": "CZE", + "value": 5 + }, + { + "code": "GEO", + "value": 2 + }, + { + "code": "NOR", + "value": 34 + }, + { + "code": "OMN", + "value": 1 + }, + { + "code": "CAN", + "value": 18 + }, + { + "code": "ARE", + "value": 5 + }, + { + "code": "ARG", + "value": 2 + }, + { + "code": "GRC", + "value": 3 + }, + { + "code": "ROU", + "value": 3 + }, + { + "code": "ARM", + "value": 1 + }, + { + "code": "IND", + "value": 6 + }, + { + "code": "MAR", + "value": 4 + }, + { + "code": "MEX", + "value": 3 + }, + { + "code": "IRN", + "value": 1 + }, + { + "code": "KAZ", + "value": 1 + }, + { + "code": "QAT", + "value": 1 + }, + { + "code": "SAU", + "value": 1 + }, + { + "code": "NLD", + "value": 39 + }, + { + "code": "JPN", + "value": 1 + }, + { + "code": "BLR", + "value": 1 + }, + { + "code": "SVK", + "value": 6 + }, + { + "code": "SVN", + "value": 1 + }, + { + "code": "THA", + "value": 3 + }, + { + "code": "LTU", + "value": 1 + }, + { + "code": "PHL", + "value": 2 + }, + { + "code": "GBR", + "value": 30 + }, + { + "code": "URY", + "value": 1 + }, + { + "code": "HUN", + "value": 1 + }, + { + "code": "SWE", + "value": 30 + }, + { + "code": "USA", + "value": 75 + }, + { + "code": "ISL", + "value": 3 + }, + { + "code": "ESP", + "value": 19 + }, + { + "code": "EST", + "value": 24 + }, + { + "code": "BEL", + "value": 8 + }, + { + "code": "ISR", + "value": 6 + }, + { + "code": "FIN", + "value": 100 + }, + { + "code": "TUN", + "value": 1 + }, + { + "code": "LUX", + "value": 1 + }, + { + "code": "KOR", + "value": 2 + }, + { + "code": "BRA", + "value": 3 + }, + { + "code": "TUR", + "value": 9 + }, + { + "code": "RUS", + "value": 10 + }, + { + "code": "FRA", + "value": 14 + }, + { + "code": "DEU", + "value": 47 + }, + { + "code": "LVA", + "value": 5 + }, + { + "code": "EGY", + "value": 1 + }, + { + "code": "ITA", + "value": 10 + }, + { + "code": "PAK", + "value": 2 + }, + { + "code": "PER", + "value": 2 + }, + { + "code": "SGP", + "value": 1 + }, + { + "code": "ZAF", + "value": 2 + } + ] +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/mockdata/player.json b/Plan/react/dashboard/src/mockdata/player.json index e3d83a260b..c8b8255956 100644 --- a/Plan/react/dashboard/src/mockdata/player.json +++ b/Plan/react/dashboard/src/mockdata/player.json @@ -2659,7 +2659,7 @@ "y": 3, "z": 0, "marker": { - "radius": 0 + "radius": 12 } }, { @@ -2899,7 +2899,7 @@ "y": 4, "z": 0, "marker": { - "radius": 0 + "radius": 5 } }, { @@ -2963,7 +2963,7 @@ "y": 4, "z": 0, "marker": { - "radius": 0 + "radius": 8 } }, { @@ -3123,7 +3123,7 @@ "y": 5, "z": 0, "marker": { - "radius": 0 + "radius": 2 } }, { @@ -3163,7 +3163,7 @@ "y": 5, "z": 0, "marker": { - "radius": 0 + "radius": 5 } }, { diff --git a/Plan/react/dashboard/src/service/localeService.js b/Plan/react/dashboard/src/service/localeService.js index fb3daabc64..89c1a38ce8 100644 --- a/Plan/react/dashboard/src/service/localeService.js +++ b/Plan/react/dashboard/src/service/localeService.js @@ -53,7 +53,6 @@ export const localeService = { if (!this.clientLocale) { this.clientLocale = this.defaultLanguage; } - let loadPath = baseAddress + '/v1/locale/{{lng}}'; if (staticSite) loadPath = baseAddress + '/locale/{{lng}}.json' await i18next @@ -99,7 +98,8 @@ export const localeService = { } window.localStorage.setItem("locale", langCode); - await i18next.changeLanguage(langCode) + await i18next.changeLanguage(langCode); + this.clientLocale = langCode; }, getLanguages: function () { @@ -115,5 +115,23 @@ export const localeService = { .map(entry => { return {name: entry[0], displayName: entry[1]} }); + }, + + getIntlFriendlyLocale: () => { + return localeService.clientLocale === 'CN' ? 'zh-cn' : localeService.clientLocale.toLocaleLowerCase().replace('_', '-') + } +} + +const generateGeolocationMap = () => { + const regions = new Intl.DisplayNames(['en'], {type: 'region'}); + const map = {} + for (let i = 0; i < 26; i++) { + for (let j = 0; j < 26; j++) { + let code = String.fromCharCode(97 + i) + String.fromCharCode(97 + j) + const result = regions.of(`${code}`); + map[result] = code; + } } + return map; } +export const reverseRegionLookupMap = generateGeolocationMap(); \ No newline at end of file diff --git a/Plan/react/dashboard/src/service/metadataService.js b/Plan/react/dashboard/src/service/metadataService.js index 1698c88574..39ae7e14fc 100644 --- a/Plan/react/dashboard/src/service/metadataService.js +++ b/Plan/react/dashboard/src/service/metadataService.js @@ -1,4 +1,10 @@ -import {doGetRequest, doSomePostRequest, standard200option, staticSite} from "./backendConfiguration"; +import { + doGetRequest, + doSomeDeleteRequest, + doSomePostRequest, + standard200option, + staticSite +} from "./backendConfiguration"; export const fetchPlanMetadata = async () => { let url = '/v1/metadata'; @@ -39,4 +45,22 @@ export const savePreferences = async preferences => { if (staticSite) return; let url = '/v1/storePreferences' return doSomePostRequest(url, [standard200option], preferences); +} + +export const fetchTheme = async name => { + let url = `/v1/theme?theme=${name}`; + if (staticSite) url = `/theme/${name}.json` + return doGetRequest(url); +} + +export const saveTheme = async (name, theme, originalName) => { + if (staticSite) return; + let url = `/v1/saveTheme?theme=${name}&originalName=${originalName}`; + return doSomePostRequest(url, [standard200option], JSON.stringify(theme)); +} + +export const deleteTheme = async (name) => { + if (staticSite) return; + let url = `/v1/deleteTheme?theme=${name}`; + return doSomeDeleteRequest(url, [standard200option]); } \ No newline at end of file diff --git a/Plan/react/dashboard/src/style/default-colors.css b/Plan/react/dashboard/src/style/default-colors.css new file mode 100644 index 0000000000..087d393ec6 --- /dev/null +++ b/Plan/react/dashboard/src/style/default-colors.css @@ -0,0 +1,1110 @@ +:root { + --color-black: #555555; + --color-white: #ffffff; + --color-text-dark: #fff; + --color-text-light: #333; + --color-white-15-percent: rgba(255, 255, 255, 0.15); + --color-black-10-percent: rgba(0, 0, 0, 0.1); + --color-plan: #368F17; + --color-red: #F44336; + --color-pink: #E91E63; + --color-purple: #9C27B0; + --color-deep-purple: #673AB7; + --color-indigo: #3F51B5; + --color-blue: #2196F3; + --color-light-blue: #03A9F4; + --color-cyan: #00BCD4; + --color-teal: #009688; + --color-green: #4CAF50; + --color-light-green: #8BC34A; + --color-lime: #CDDC39; + --color-yellow: #ffe821; + --color-amber: #FFC107; + --color-orange: #FF9800; + --color-deep-orange: #FF5722; + --color-brown: #795548; + --color-blue-grey: #607D8B; + --color-yellow-15-percent: rgba(255, 220, 40, 0.15); + --color-cpu-yellow: #e0d264; + --color-ram-green: #7dcc24; + --color-chunk-brown: #b58310; + --color-entity-purple: #ac69ef; + --color-bright-blue: #1E90FF; + --color-ping-amber: #ffd54f; + --color-map-green: #EEFFEE; + --color-high-green: #267F00; + --color-medium-yellow: #e5cc12; + --color-low-red: #b74343; + --color-alert-success: #d2f4e8; + --color-alert-warning: #fdf3d8; + --color-alert-danger: #fadbd8; + --color-success: #1CC88A; + --color-warning: #F6C23E; + --color-danger: #e74A3B; + --color-alert-success-text: #0f6848; + --color-alert-warning-text: #806520; + --color-alert-danger-text: #78261f; + --color-secondary: #6c757d; + --color-cool-grey: #6e707e; + --color-grey: #9E9E9E; + --color-ink: #222222; + --color-dark-slate: #212529; + --color-medium-slate: #3a3b45; + --color-table-slate: #44454e; + --color-border-slate: #4b4d5a; + --color-light-slate: #5a5c69; + --color-light-grey: #dddddd; + --color-table-grey: #f3f3f3; + --color-white-grey: #f8f9fc; + --color-pale-grey: #eaecf4; + --color-cement-grey: #e3e6f0; + --color-border-grey: #dddfeb; + --color-pie-0: #0099C6; + --color-pie-1: #66AA00; + --color-pie-2: #316395; + --color-pie-3: #994499; + --color-pie-4: #22AA99; + --color-pie-5: #AAAA11; + --color-pie-6: #6633CC; + --color-pie-7: #E67300; + --color-pie-8: #329262; + --color-pie-9: #5574A6; + --color-drilldown-0: #438c99; + --color-drilldown-1: #639A67; + --color-drilldown-2: #D8EBB5; + --color-drilldown-3: #D9BF77; + + --color-night-black: #282a36; + --color-night-dark-blue: #44475a; + --color-night-blue: #6272a4; + --color-night-grey-blue: #646e8c; + --color-night-dark-grey-blue: #606270; + --color-night-text: #eee8d5; + --color-night-text-50-percent: rgba(238, 232, 213, 0.5); + + --color-theme: var(--color-plan); + --color-themeText: var(--color-theme); + --color-text: var(--color-text-light); + + --color-layout-background: var(--color-white-grey); + --color-layout-title: var(--color-light-slate); + --color-layout-divider: var(--color-black-10-percent); + --color-layout-help-icon: var(--color-light-blue); + --color-layout-loader-border: var(--color-theme); + --color-layout-loader-background: var(--color-theme); + --color-sidebar-background: var(--color-theme); + --color-sidebar-text: var(--color-text-dark); + --color-sidebar-divider: var(--color-white-15-percent); + --color-sidebar-collapsible-section-background: var(--color-white); + --color-sidebar-collapsible-section-text: var(--color-text); + --color-sidebar-collapsible-section-hover: var(--color-pale-grey); + --color-sidebar-collapsible-section-border: var(--color-pale-grey); + --color-sidebar-navigation-item-background: var(--color-theme); + --color-sidebar-navigation-item-icon: var(--color-text-dark); + --color-cards-background: var(--color-white); + --color-cards-border: var(--color-cement-grey); + --color-cards-header-background: var(--color-white-grey); + --color-cards-header-border: var(--color-cement-grey); + --color-tabs-background: var(--color-white); + --color-tabs-selected: var(--color-white); + --color-tabs-border: var(--color-border-grey); + --color-info-box-info: var(--color-alert-success); + --color-info-box-info-text: var(--color-alert-success-text); + --color-info-box-notice: var(--color-alert-warning); + --color-info-box-notice-text: var(--color-alert-warning-text); + --color-info-box-error: var(--color-alert-danger); + --color-info-box-error-text: var(--color-alert-danger-text); + --color-calendar-today: var(--color-yellow-15-percent); + --color-calendar-border: var(--color-cement-grey); + --color-calendar-popover-body: var(--color-white); + --color-tables-text: var(--color-text); + --color-tables-colored-header-text: var(--color-text-dark); + --color-tables-odd-row: var(--color-table-grey); + --color-tables-even-row: var(--color-white); + --color-tables-border: var(--color-border-grey); + --color-forms-buttons-action-button: var(--color-theme); + --color-forms-buttons-dangerous-button: var(--color-danger); + --color-forms-buttons-secondary-action-button: var(--color-grey); + --color-forms-buttons-outline-button-border: var(--color-secondary); + --color-forms-input-background: var(--color-white); + --color-forms-input-border: var(--color-border-grey); + --color-forms-input-text: var(--color-cool-grey); + --color-forms-dropdown-hover: var(--color-white-grey); + --color-forms-multi-select-item-background: var(--color-pale-grey); + --color-forms-checkbox-checked: var(--color-theme); + --color-graphs-style-grid-line: var(--color-cement-grey); + --color-graphs-style-minor-grid-line: var(--color-black); + --color-graphs-style-border: var(--color-light-grey); + --color-graphs-style-tooltip-background: var(--color-white-grey); + --color-graphs-style-selector-button-background: var(--color-white-grey); + --color-graphs-style-selector-button-hover: var(--color-cement-grey); + --color-graphs-style-selector-button-selected: var(--color-cement-grey); + --color-graphs-style-selector-text-input-background: var(--color-white-grey); + --color-graphs-style-selector-text-input-border: var(--color-light-grey); + --color-graphs-style-selector-range-handle-background: var(--color-white-grey); + --color-graphs-style-selector-range-handle-border: var(--color-grey); + --color-graphs-style-selector-range-outline: var(--color-light-grey); + --color-graphs-style-selector-range-selected-area: var(--color-bright-blue); + --color-graphs-style-selector-range-series-line: var(--color-bright-blue); + --color-graphs-style-scrollbar-decoration: var(--color-text); + --color-graphs-style-scrollbar-bar-background: var(--color-light-grey); + --color-graphs-style-scrollbar-button-background: var(--color-cement-grey); + --color-graphs-style-scrollbar-track-background: var(--color-white-grey); + --color-graphs-punch-card: var(--color-ink); + --color-graphs-players-online: var(--color-bright-blue); + --color-graphs-tps-high: var(--color-high-green); + --color-graphs-tps-medium: var(--color-medium-yellow); + --color-graphs-tps-low: var(--color-low-red); + --color-graphs-cpu: var(--color-cpu-yellow); + --color-graphs-ram: var(--color-ram-green); + --color-graphs-chunks: var(--color-chunk-brown); + --color-graphs-entities: var(--color-entity-purple); + --color-graphs-disk-high: var(--color-high-green); + --color-graphs-disk-medium: var(--color-medium-yellow); + --color-graphs-disk-low: var(--color-low-red); + --color-graphs-ping-max: var(--color-amber); + --color-graphs-ping-avg: var(--color-warning); + --color-graphs-ping-min: var(--color-ping-amber); + --color-graphs-world-map-high: var(--color-high-green); + --color-graphs-world-map-low: var(--color-map-green); + --color-graphs-world-map-bars: var(--color-green); + --color-data-servers: var(--color-light-green); + --color-data-trend-better: var(--color-success); + --color-data-trend-same: var(--color-warning); + --color-data-trend-worse: var(--color-danger); + --color-data-play-playtime: var(--color-green); + --color-data-play-playtime-active: var(--color-green); + --color-data-play-playtime-afk: var(--color-grey); + --color-data-play-sessions: var(--color-teal); + --color-data-play-session-length: var(--color-teal); + --color-data-play-gamemode: var(--color-teal); + --color-data-play-first-seen: var(--color-light-green); + --color-data-play-last-seen: var(--color-teal); + --color-data-players-count: var(--color-black); + --color-data-players-online: var(--color-blue); + --color-data-players-unique: var(--color-light-blue); + --color-data-players-new: var(--color-light-green); + --color-data-players-activity-index: var(--color-amber); + --color-data-players-very-active: var(--color-green); + --color-data-players-active: var(--color-light-green); + --color-data-players-regular: var(--color-lime); + --color-data-players-irregular: var(--color-amber); + --color-data-players-inactive: var(--color-blue-grey); + --color-data-player-peak-last: var(--color-light-blue); + --color-data-player-peak-all-time: var(--color-light-green); + --color-data-performance-uptime: var(--color-light-green); + --color-data-performance-downtime: var(--color-red); + --color-data-performance-tps: var(--color-red); + --color-data-performance-tps-low-spikes: var(--color-red); + --color-data-performance-tps-average: var(--color-orange); + --color-data-performance-cpu: var(--color-amber); + --color-data-performance-ram: var(--color-light-green); + --color-data-performance-entities: var(--color-purple); + --color-data-performance-chunks: var(--color-blue-grey); + --color-data-performance-disk: var(--color-green); + --color-data-performance-ping: var(--color-amber); + --color-data-calculated-insights: var(--color-red); + --color-data-calculated-join-addresses: var(--color-amber); + --color-data-calculated-retention: var(--color-indigo); + --color-data-calculated-retention-new-players: var(--color-light-green); + --color-data-calculated-geolocation: var(--color-green); + --color-data-calculated-allow-list: var(--color-orange); + --color-data-calculated-plugin-versions: var(--color-indigo); + --color-data-player-versus-player-kills: var(--color-red); + --color-data-player-versus-mob-kills: var(--color-green); + --color-data-player-versus-deaths: var(--color-black); + --color-data-player-versus-top-3-first: var(--color-amber); + --color-data-player-versus-top-3-second: var(--color-grey); + --color-data-player-versus-top-3-third: var(--color-brown); + --color-data-player-status-online: var(--color-green); + --color-data-player-status-offline: var(--color-red); + --color-data-player-status-banned: var(--color-red); + --color-data-player-status-operator: var(--color-blue); + --color-data-player-status-kicks: var(--color-brown); + --color-data-player-status-nicknames: var(--color-purple); + --color-plugin-red: var(--color-red); + --color-plugin-pink: var(--color-pink); + --color-plugin-purple: var(--color-purple); + --color-plugin-deep-purple: var(--color-deep-purple); + --color-plugin-indigo: var(--color-indigo); + --color-plugin-blue: var(--color-blue); + --color-plugin-light-blue: var(--color-light-blue); + --color-plugin-cyan: var(--color-cyan); + --color-plugin-teal: var(--color-teal); + --color-plugin-green: var(--color-green); + --color-plugin-light-green: var(--color-light-green); + --color-plugin-lime: var(--color-lime); + --color-plugin-yellow: var(--color-yellow); + --color-plugin-amber: var(--color-amber); + --color-plugin-orange: var(--color-orange); + --color-plugin-deep-orange: var(--color-deep-orange); + --color-plugin-brown: var(--color-brown); + --color-plugin-grey: var(--color-grey); + --color-plugin-blue-grey: var(--color-blue-grey); + --color-plugin-black: var(--color-black); +} + +.dropdown .dropdown-menu { + --bg-dropdown-bg: var(--color-forms-input-background) !important; +} + +.sidebar { + background: var(--color-sidebar-background); + background-color: var(--color-sidebar-background); +} + +.sidebar-collapse { + background: var(--color-sidebar-collapsible-section-background); + background-color: var(--color-sidebar-collapsible-section-background); + color: var(--color-sidebar-collapsible-section-text); +} + +.navigation-item { + background: var(--color-sidebar-navigation-item-background); + background-color: var(--color-sidebar-navigation-item-background); + color: var(--color-sidebar-navigation-item-icon); +} + +.header { + color: var(--color-layout-title); +} + +.form-control, .form-control:focus { + background: var(--color-forms-input-background); + background-color: var(--color-forms-input-background); +} + +.btn-action { + background: var(--color-forms-buttons-action-button); + background-color: var(--color-forms-buttons-action-button); + color: var(--contrast-color-forms-buttons-action-button) !important; + --bs-btn-hover-bg: color-mix(in srgb, var(--color-forms-buttons-action-button) 90%, black); + --bs-btn-active-bg: color-mix(in srgb, var(--color-forms-buttons-action-button) 90%, black); + --bs-btn-disabled-bg: var(--color-forms-buttons-action-button); + --bs-btn-disabled-border-color: var(--color-forms-buttons-action-button); + --bs-btn-hover-color: color-mix(in srgb, var(--contrast-color-forms-buttons-action-button) 90%, black); + --bs-btn-active-color: color-mix(in srgb, var(--contrast-color-forms-buttons-action-button) 90%, black); + --bs-btn-disabled-color: var(--contrast-color-forms-buttons-action-button); +} + +.btn-secondary-action { + background: var(--color-forms-buttons-secondary-action-button); + background-color: var(--color-forms-buttons-secondary-action-button); + color: var(--contrast-color-forms-buttons-secondary-action-button); + --bs-btn-hover-bg: color-mix(in srgb, var(--color-forms-buttons-secondary-action-button) 90%, black); + --bs-btn-active-bg: color-mix(in srgb, var(--color-forms-buttons-secondary-action-button) 90%, black); + --bs-btn-disabled-bg: var(--color-forms-buttons-secondary-action-button); + --bs-btn-disabled-border-color: var(--color-forms-buttons-secondary-action-button); + --bs-btn-hover-color: color-mix(in srgb, var(--contrast-color-forms-buttons-secondary-action-button) 70%, black); + --bs-btn-active-color: color-mix(in srgb, var(--contrast-color-forms-buttons-secondary-action-button) 70%, black); + --bs-btn-disabled-color: var(--contrast-color-forms-buttons-secondary-action-button); +} + +.btn-danger { + background: var(--color-forms-buttons-dangerous-button); + background-color: var(--color-forms-buttons-dangerous-button); + border-color: var(--color-forms-buttons-dangerous-button); + color: var(--contrast-color-forms-buttons-dangerous-button); + --bs-btn-hover-bg: var(--color-forms-buttons-dangerous-button); + --bs-btn-disabled-bg: var(--color-forms-buttons-dangerous-button); + --bs-btn-disabled-border-color: var(--color-forms-buttons-dangerous-button); + --bs-btn-hover-color: color-mix(in srgb, var(--contrast-color-forms-buttons-dangerous-button) 90%, black); + --bs-btn-hover-border-color: color-mix(in srgb, var(--color-forms-buttons-dangerous-button) 90%, black); + --bs-btn-disabled-color: var(--contrast-color-forms-buttons-dangerous-button); +} + +.btn.bg-transparent { + --bs-btn-hover-color: currentcolor !important; + --bs-btn-hover-bg: transparent; +} + +/* Calendar overrides */ +.fc-title, .fc-time { + color: var(--color-text); +} + +.fc-day-today { + background: var(--color-calendar-today) !important; + background-color: var(--color-calendar-today) !important; +} + +.fc-popover-body, .fc-popover-header { + background: var(--color-calendar-popover-body) !important; + background-color: var(--color-calendar-popover-body) !important; + color: var(--color-text) !important; +} + +.fc td, .fc tr, .fc th, .fc table, .fc-popover { + border-color: var(--color-calendar-border) !important; +} + +.fc a { + color: var(--color-text) !important; +} + +.fc-button { + background: var(--color-forms-buttons-action-button) !important; + background-color: var(--color-forms-buttons-action-button) !important; +} + +/* Minecraft color codes */ +.black { + color: #000000; +} + +.darkblue { + color: #0000AA; +} + +.darkgreen { + color: #00AA00; +} + +.darkaqua { + color: #00AAAA; +} + +.darkred { + color: #AA0000; +} + +.darkpurple { + color: #AA00AA; +} + +.gold { + color: #FFAA00; +} + +.gray { + color: #AAAAAA; +} + +.darkgray { + color: var(--color-black); +} + +.blue { + color: #5555FF; +} + +.green { + color: #55FF55; +} + +.aqua { + color: #55FFFF; +} + +.red { + color: #FF5555; +} + +.pink { + color: #FF55FF; +} + +.yellow { + color: #FFFF55; + text-shadow: 0 0 6px #000; +} + +.white { + color: #FFFFFF; + text-shadow: 0 0 8px #000; +} + +.col-help-icon { + color: var(--color-layout-help-icon); +} + +.col-servers { + color: var(--color-data-servers); +} + +.bg-servers { + background-color: var(--color-data-servers); + --bs-btn-hover-bg: var(--color-data-servers); +} + +.bg-servers-outline { + border-color: var(--color-data-servers); + border-style: solid; + outline: var(--color-data-servers) solid 1px; +} + +.col-trend-better { + color: var(--color-data-trend-better); +} + +.bg-trend-better { + background-color: var(--color-data-trend-better); +} + +.col-trend-same { + color: var(--color-data-trend-same); +} + +.bg-trend-same { + background-color: var(--color-data-trend-same); +} + +.col-trend-worse { + color: var(--color-data-trend-worse); +} + +.bg-trend-worse { + background-color: var(--color-data-trend-worse); +} + +.col-playtime { + color: var(--color-data-play-playtime); +} + +.bg-playtime { + background-color: var(--color-data-play-playtime); +} + +.col-playtime-active { + color: var(--color-data-play-playtime-active); +} + +.bg-playtime-active { + background-color: var(--color-data-play-playtime-active); +} + +.col-playtime-afk { + color: var(--color-data-play-playtime-afk); +} + +.bg-playtime-afk { + background-color: var(--color-data-play-playtime-afk); +} + +.col-sessions { + color: var(--color-data-play-sessions); +} + +.bg-sessions { + background-color: var(--color-data-play-sessions); +} + +.bg-sessions-outline { + border-color: var(--color-data-play-sessions); + border-style: solid; + border-width: 3px; + outline: var(--color-data-play-sessions) solid 1px; +} + +.col-session-length { + color: var(--color-data-play-session-length); +} + +.bg-session-length { + background-color: var(--color-data-play-session-length); +} + +.col-gamemode { + color: var(--color-data-play-gamemode); +} + +.bg-gamemode { + background-color: var(--color-data-play-gamemode); +} + +.col-first-seen { + color: var(--color-data-play-first-seen); +} + +.bg-first-seen { + background-color: var(--color-data-play-first-seen); +} + +.col-last-seen { + color: var(--color-data-play-last-seen); +} + +.bg-last-seen { + background-color: var(--color-data-play-last-seen); +} + +.col-players-count { + color: var(--color-data-players-count); +} + +.bg-players-count { + background-color: var(--color-data-players-count); +} + +.col-players-online { + color: var(--color-data-players-online); +} + +.bg-players-online { + background-color: var(--color-data-players-online); + --bs-btn-hover-bg: var(--color-data-players-online); +} + +.col-players-unique { + color: var(--color-data-players-unique); +} + +.bg-players-unique { + background-color: var(--color-data-players-unique); +} + +.col-players-new { + color: var(--color-data-players-new); +} + +.bg-players-new { + background-color: var(--color-data-players-new); +} + +.col-players-activity-index { + color: var(--color-data-players-activity-index); +} + +.bg-players-activity-index { + background-color: var(--color-data-players-activity-index); +} + +.col-players-very-active { + color: var(--color-data-players-very-active); +} + +.bg-players-very-active { + background-color: var(--color-data-players-very-active); +} + +.col-players-active { + color: var(--color-data-players-active); +} + +.bg-players-active { + background-color: var(--color-data-players-active); +} + +.col-players-regular { + color: var(--color-data-players-regular); +} + +.bg-players-regular { + background-color: var(--color-data-players-regular); +} + +.col-players-irregular { + color: var(--color-data-players-irregular); +} + +.bg-players-irregular { + background-color: var(--color-data-players-irregular); +} + +.col-players-inactive { + color: var(--color-data-players-inactive); +} + +.bg-players-inactive { + background-color: var(--color-data-players-inactive); +} + +.col-player-peak-last { + color: var(--color-data-player-peak-last); +} + +.bg-player-peak-last { + background-color: var(--color-data-player-peak-last); +} + +.col-player-peak-all-time { + color: var(--color-data-player-peak-all-time); +} + +.bg-player-peak-all-time { + background-color: var(--color-data-player-peak-all-time); +} + +.col-uptime { + color: var(--color-data-performance-uptime); +} + +.bg-uptime { + background-color: var(--color-data-performance-uptime); +} + +.col-downtime { + color: var(--color-data-performance-downtime); +} + +.bg-downtime { + background-color: var(--color-data-performance-downtime); +} + +.col-tps { + color: var(--color-data-performance-tps); +} + +.bg-tps { + background-color: var(--color-data-performance-tps); +} + +.col-tps-low-spikes { + color: var(--color-data-performance-tps-low-spikes); +} + +.bg-tps-low-spikes { + background-color: var(--color-data-performance-tps-low-spikes); +} + +.col-tps-average { + color: var(--color-data-performance-tps-average); +} + +.bg-tps-average { + background-color: var(--color-data-performance-tps-average); +} + +.col-cpu { + color: var(--color-data-performance-cpu); +} + +.bg-cpu { + background-color: var(--color-data-performance-cpu); +} + +.col-ram { + color: var(--color-data-performance-ram); +} + +.bg-ram { + background-color: var(--color-data-performance-ram); +} + +.col-entities { + color: var(--color-data-performance-entities); +} + +.bg-entities { + background-color: var(--color-data-performance-entities); +} + +.col-chunks { + color: var(--color-data-performance-chunks); +} + +.bg-chunks { + background-color: var(--color-data-performance-chunks); +} + +.col-disk { + color: var(--color-data-performance-disk); +} + +.bg-disk { + background-color: var(--color-data-performance-disk); +} + +.col-ping { + color: var(--color-data-performance-ping); +} + +.bg-ping { + background-color: var(--color-data-performance-ping); +} + +.col-insights { + color: var(--color-data-calculated-insights); +} + +.bg-insights { + background-color: var(--color-data-calculated-insights); +} + +.col-join-addresses { + color: var(--color-data-calculated-join-addresses); +} + +.bg-join-addresses { + background-color: var(--color-data-calculated-join-addresses); + --color-forms-checkbox-checked: var(--color-data-calculated-join-addresses); +} + +.col-retention { + color: var(--color-data-calculated-retention); +} + +.bg-retention { + background-color: var(--color-data-calculated-retention); +} + +.col-retention-new-players { + color: var(--color-data-calculated-retention-new-players); +} + +.bg-retention-new-players { + background-color: var(--color-data-calculated-retention-new-players); +} + +.col-geolocation { + color: var(--color-data-calculated-geolocation); +} + +.bg-geolocation { + background-color: var(--color-data-calculated-geolocation); +} + +.col-allow-list { + color: var(--color-data-calculated-allow-list); +} + +.bg-allow-list { + background-color: var(--color-data-calculated-allow-list); +} + +.col-plugin-versions { + color: var(--color-data-calculated-plugin-versions); +} + +.bg-plugin-versions { + background-color: var(--color-data-calculated-plugin-versions); +} + +.col-player-kills { + color: var(--color-data-player-versus-player-kills); +} + +.bg-player-kills { + background-color: var(--color-data-player-versus-player-kills); +} + +.col-mob-kills { + color: var(--color-data-player-versus-mob-kills); +} + +.bg-mob-kills { + background-color: var(--color-data-player-versus-mob-kills); +} + +.col-deaths { + color: var(--color-data-player-versus-deaths); +} + +.bg-deaths { + background-color: var(--color-data-player-versus-deaths); +} + +.col-top-3-first { + color: var(--color-data-player-versus-top-3-first); +} + +.bg-top-3-first { + background-color: var(--color-data-player-versus-top-3-first); +} + +.col-top-3-second { + color: var(--color-data-player-versus-top-3-second); +} + +.bg-top-3-second { + background-color: var(--color-data-player-versus-top-3-second); +} + +.col-top-3-third { + color: var(--color-data-player-versus-top-3-third); +} + +.bg-top-3-third { + background-color: var(--color-data-player-versus-top-3-third); +} + +.col-online { + color: var(--color-data-player-status-online); +} + +.bg-online { + background-color: var(--color-data-player-status-online); +} + +.col-offline { + color: var(--color-data-player-status-offline); +} + +.bg-offline { + background-color: var(--color-data-player-status-offline); +} + +.col-banned { + color: var(--color-data-player-status-banned); +} + +.bg-banned { + background-color: var(--color-data-player-status-banned); +} + +.col-operator { + color: var(--color-data-player-status-operator); +} + +.bg-operator { + background-color: var(--color-data-player-status-operator); +} + +.col-kicks { + color: var(--color-data-player-status-kicks); +} + +.bg-kicks { + background-color: var(--color-data-player-status-kicks); +} + +.col-nicknames { + color: var(--color-data-player-status-nicknames); +} + +.bg-nicknames { + background-color: var(--color-data-player-status-nicknames); + --bs-btn-hover-bg: var(--color-data-player-status-nicknames); +} + +.bg-plugin-red { + background: var(--color-plugin-red); +} + +.col-plugin-red { + color: var(--color-plugin-red); +} + +.bg-plugin-pink { + background: var(--color-plugin-pink); +} + +.col-plugin-pink { + color: var(--color-plugin-pink); +} + +.bg-plugin-purple { + background: var(--color-plugin-purple); +} + +.col-plugin-purple { + color: var(--color-plugin-purple); +} + +.bg-plugin-deep-purple { + background: var(--color-plugin-deep-purple); +} + +.col-plugin-deep-purple { + color: var(--color-plugin-deep-purple); +} + +.bg-plugin-indigo { + background: var(--color-plugin-indigo); +} + +.col-plugin-indigo { + color: var(--color-plugin-indigo); +} + +.bg-plugin-blue { + background: var(--color-plugin-blue); +} + +.col-plugin-blue { + color: var(--color-plugin-blue); +} + +.bg-plugin-light-blue { + background: var(--color-plugin-light-blue); +} + +.col-plugin-light-blue { + color: var(--color-plugin-light-blue); +} + +.bg-plugin-cyan { + background: var(--color-plugin-cyan); +} + +.col-plugin-cyan { + color: var(--color-plugin-cyan); +} + +.bg-plugin-teal { + background: var(--color-plugin-teal); +} + +.col-plugin-teal { + color: var(--color-plugin-teal); +} + +.bg-plugin-green { + background: var(--color-plugin-green); +} + +.col-plugin-green { + color: var(--color-plugin-green); +} + +.bg-plugin-light-green { + background: var(--color-plugin-light-green); +} + +.col-plugin-light-green { + color: var(--color-plugin-light-green); +} + +.bg-plugin-lime { + background: var(--color-plugin-lime); +} + +.col-plugin-lime { + color: var(--color-plugin-lime); +} + +.bg-plugin-yellow { + background: var(--color-plugin-yellow); +} + +.col-plugin-yellow { + color: var(--color-plugin-yellow); +} + +.bg-plugin-amber { + background: var(--color-plugin-amber); +} + +.col-plugin-amber { + color: var(--color-plugin-amber); +} + +.bg-plugin-orange { + background: var(--color-plugin-orange); +} + +.col-plugin-orange { + color: var(--color-plugin-orange); +} + +.bg-plugin-deep-orange { + background: var(--color-plugin-deep-orange); +} + +.col-plugin-deep-orange { + color: var(--color-plugin-deep-orange); +} + +.bg-plugin-brown { + background: var(--color-plugin-brown); +} + +.col-plugin-brown { + color: var(--color-plugin-brown); +} + +.bg-plugin-grey { + background: var(--color-plugin-grey); +} + +.col-plugin-grey { + color: var(--color-plugin-grey); +} + +.bg-plugin-blue-grey { + background: var(--color-plugin-blue-grey); +} + +.col-plugin-blue-grey { + color: var(--color-plugin-blue-grey); +} + +.bg-plugin-black { + background: var(--color-plugin-black); +} + +.col-plugin-black { + color: var(--color-plugin-black); +} + +table { + color: var(--color-tables-text); +} + +thead { + color: var(--color-tables-text); + background: var(--color-tables-even-row); + background-color: var(--color-tables-even-row); +} + +thead[class^='bg'], thead[class*=' bg'] { + color: var(--color-tables-colored-header-text) !important; +} + +.table-striped, .table-striped thead { + border-color: var(--color-tables-border); +} + +.table-striped tbody tr:nth-of-type(2n+1) { + color: var(--color-tables-text); + background: var(--color-tables-odd-row); + background-color: var(--color-tables-odd-row); +} + +.table-striped tbody tr:nth-of-type(2n), .table-striped { + color: var(--color-tables-text); + background: var(--color-tables-even-row); + background-color: var(--color-tables-even-row); +} + +.form-check-input[type="checkbox"] { + --bs-form-check-bg: var(--color-forms-input-background); + --bs-border-color: var(--color-forms-input-border); +} + +.form-check-input[type="checkbox"]:checked, .form-check-input[type="checkbox"]:indeterminate { + background-color: var(--color-forms-checkbox-checked); + border-color: var(--color-forms-checkbox-checked); +} + +.form-check-label { + color: var(--color-forms-input-text); +} + +.btn.col-text:hover, .btn.col-text:focus { + color: color-mix(in srgb, var(--color-text), black 20%) +} + +.visualizer-button { + color: var(--color-text); + --bs-btn-color: var(--color-text); +} + +.card-body, .card-header, h1, h2, h3, h4, h5, h6, p, .form-text, .bg-grey-outline { + color: var(--color-text); +} + +.btn { + --bs-btn-color: var(--color-text); + --bs-btn-hover-color: var(--color-text); + --bs-btn-active-border-color: var(--color-forms-input-border); +} + +.btn[class^='bg'], .btn[class*=' bg'] { + --bs-btn-color: var(--color-tables-colored-header-text); + --bs-btn-hover-color: var(--color-tables-colored-header-text); +} + +.btn-close { + --bs-btn-close-color: var(--color-text); + --bs-btn-close-fill: var(--color-text); +} + +.modal-dialog * { + --bs-modal-bg: var(--color-cards-background); + --bs-border-color: var(--color-cards-border); + --bs-modal-footer-border-color: var(--color-cards-border); + --bs-modal-header-border-color: var(--color-cards-border); + --bs-modal-color: var(--color-text); + +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/style/main.sass b/Plan/react/dashboard/src/style/main.sass index a5dae798c1..1c1a0382d0 100644 --- a/Plan/react/dashboard/src/style/main.sass +++ b/Plan/react/dashboard/src/style/main.sass @@ -38,4 +38,148 @@ p, span, td, .h3, a, button height: 5px position: absolute bottom: 0 - margin-left: -1rem \ No newline at end of file + margin-left: -1rem + +// Color Box Styles +.color-box-wrapper + width: 100% + height: 40px + border: 1px solid #ddd + border-radius: 4px + padding: 8px 0 8px 12px + display: flex + align-items: center + justify-content: space-between + cursor: default + + > span + overflow: hidden + text-overflow: ellipsis + white-space: nowrap + font-size: 0.9rem + margin-right: 8px + + > div + padding-right: 10px + height: 100% + display: flex + align-items: center + justify-content: center + font-size: 0.8rem + font-family: monospace + +.background-color-box + .color-box-wrapper + background: var(--box-color) + color: var(--box-contrast-color) + border-color: var(--color-tables-border) + +.text-color-box + .color-box-wrapper + background-color: var(--box-contrast-color, white) + border-color: var(--color-tables-border) + + > span, + > div + color: var(--box-color) + + &.night-mode + .color-box-wrapper + background-color: var(--color-night-dark-blue) + +.theme-editor + .use-case-section + background-color: var(--editor-bg-color) + + .example-section + position: static + bottom: 0 + background-color: var(--color-layout-background) + border-top: 2px solid var(--color-layout-divider) + padding: 1rem + width: 100% + height: 100% + + transition: color 0.2s, background-color 0.2s, border-color 0.1s + + * + transition: color 0.2s, background-color 0.2s, border-color 0.1s + + .example + min-height: 40vh + max-height: 40vh + overflow-y: auto + overflow-x: hidden + padding-right: 1rem + + .dropdown-toggle + width: 100% + display: flex + border-color: transparent !important + + > .dropdown-toggle:focus + border: none + outline: none + box-shadow: none + + > .dropdown-toggle::after + position: absolute + right: 2rem + top: 1.7rem + font-size: 2rem + color: var(--color-text) + + .editor-toast + position: fixed + top: 8.5rem + right: 1.9rem + transition: top 0.5s + z-index: 100 + width: 22.5rem + --bs-toast-bg: var(--color-cards-background) + + &.scrolled + top: 1em + +.disabled-feedback + width: 100% + margin-top: 0.25rem + font-size: 0.875em + color: var(--color-secondary) + +.edit-history + margin: 0 + margin-top: 1rem + padding-left: 0 + list-style: none + max-height: 30vh + overflow-y: auto + + .edit + color: var(--color-text) + + .redo + color: color-mix(in srgb, var(--color-text), transparent 50%) + + &.nested + margin-left: 1rem + +.issues + margin: 0 + margin-top: 1rem + padding-left: 0 + list-style: none + max-height: 30vh + overflow-y: auto + + .issue + color: var(--color-danger) + +.theme-option + transition: opacity 0.2s + + &.selected + font-weight: bold + + &:hover + opacity: 0.7 \ No newline at end of file diff --git a/Plan/react/dashboard/src/style/mobile.css b/Plan/react/dashboard/src/style/mobile.css index fe991a1cff..d4ba59f668 100644 --- a/Plan/react/dashboard/src/style/mobile.css +++ b/Plan/react/dashboard/src/style/mobile.css @@ -6,7 +6,7 @@ #wrapper { font-size: small; - background-image: var(--color-theme); + background-image: var(--color-layout-background); } .sidebar { diff --git a/Plan/react/dashboard/src/style/sb-admin-2.css b/Plan/react/dashboard/src/style/sb-admin-2.css index cec2644093..689fe80e02 100644 --- a/Plan/react/dashboard/src/style/sb-admin-2.css +++ b/Plan/react/dashboard/src/style/sb-admin-2.css @@ -15,24 +15,24 @@ img { .table th, .table td { - border-top: 1px solid #dddfeb; + border-top: 1px solid var(--color-tables-border); } .table thead th { - border-bottom: 2px solid #dddfeb !important; + border-bottom: 2px solid var(--color-tables-border) !important; } .table tbody + tbody { - border-top: 2px solid #dddfeb; + border-top: 2px solid var(--color-tables-border); } .table-bordered { - border: 1px solid #dddfeb; + border: 1px solid var(--color-tables-border); } .table-bordered th, .table-bordered td { - border: 1px solid #dddfeb; + border: 1px solid var(--color-tables-border); } /* SB Admin 2 table changes start */ @@ -212,7 +212,7 @@ img { .table .thead-light th { color: #6e707e; background-color: #eaecf4; - border-color: #dddfeb; + border-color: var(--color-tables-border); } .table-dark { @@ -241,7 +241,7 @@ img { } .modal-footer { - border-top: 1px solid #eaecf4; + border-top: 1px solid var(--color-cards-border); } .bg-white { @@ -253,11 +253,11 @@ img { .table-dark td, .table-dark thead th, .table-dark tbody + tbody { - border-color: #dddfeb; + border-color: var(--color-tables-border); } .table .thead-dark th { - border-color: #dddfeb; + border-color: var(--color-tables-border); } } @@ -362,8 +362,8 @@ pre { } .form-control { - color: #6e707e; - border: 1px solid #d1d3e2; + color: var(--color-forms-input-text); + border: 1px solid var(--color-forms-input-border); border-radius: 0.35rem; } @@ -372,33 +372,33 @@ pre { } .form-control:focus { - color: #6e707e; - border-color: #bac8f3; - box-shadow: 0 0 0 0.2rem rgba(78, 115, 223, 0.25); + color: var(--color-forms-input-text); + border-color: color-mix(in hsl, var(--color-forms-input-border), #bac8f3 20%); + box-shadow: 0 0 0 0.2rem color-mix(in hsl, var(--color-forms-input-border), rgba(78, 115, 223, 0.25) 75%); } .form-control::-webkit-input-placeholder { - color: #858796; + color: color-mix(in hsl, var(--color-forms-input-text), #fff 20%); } .form-control::-moz-placeholder { - color: #858796; + color: color-mix(in hsl, var(--color-forms-input-text), #fff 20%); } .form-control:-ms-input-placeholder { - color: #858796; + color: color-mix(in hsl, var(--color-forms-input-text), #fff 20%); } .form-control::-ms-input-placeholder { - color: #858796; + color: color-mix(in hsl, var(--color-forms-input-text), #fff 20%); } .form-control::placeholder { - color: #858796; + color: color-mix(in hsl, var(--color-forms-input-text), #fff 20%); } .form-control:disabled, .form-control[readonly] { - background-color: #eaecf4; + background-color: color-mix(in hsl, var(--color-forms-input-background), var(--contrast-color-forms-input-background) 10%); } select.form-control:focus::-ms-value { @@ -511,12 +511,11 @@ select.form-control:focus::-ms-value { } .btn { - color: #858796; border-radius: 0.35rem; } -.btn:hover { - color: #858796; +.btn:disabled { + background-image: linear-gradient(rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 100%); } .btn:focus, .btn.focus { @@ -688,38 +687,6 @@ select.form-control:focus::-ms-value { box-shadow: 0 0 0 0.2rem rgba(247, 203, 91, 0.5); } -.btn-danger { - background-color: #e74a3b; - border-color: #e74a3b; -} - -.btn-danger:hover { - background-color: #e02d1b; - border-color: #d52a1a; -} - -.btn-danger:focus, .btn-danger.focus { - background-color: #e02d1b; - border-color: #d52a1a; - box-shadow: 0 0 0 0.2rem rgba(235, 101, 88, 0.5); -} - -.btn-danger.disabled, .btn-danger:disabled { - background-color: #e74a3b; - border-color: #e74a3b; -} - -.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active, -.show > .btn-danger.dropdown-toggle { - background-color: #d52a1a; - border-color: #ca2819; -} - -.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus, -.show > .btn-danger.dropdown-toggle:focus { - box-shadow: 0 0 0 0.2rem rgba(235, 101, 88, 0.5); -} - .btn-light { color: #3a3b45; background-color: #f8f9fc; @@ -819,32 +786,39 @@ select.form-control:focus::-ms-value { } .btn-outline-secondary { - color: #858796; - border-color: #858796; + color: var(--color-forms-buttons-outline-button-border); + border-color: var(--color-forms-buttons-outline-button-border); + --bs-btn-hover-bg: var(--color-forms-buttons-outline-button-border); + --bs-btn-active-bg: transparent; + --bs-btn-disabled-bg: transparent; + --bs-btn-hover-color: color-mix(in srgb, var(--color-forms-buttons-outline-button-border) 90%, black); + --bs-btn-active-color: color-mix(in srgb, var(--color-forms-buttons-outline-button-border) 90%, black); + --bs-btn-disabled-color: color-mix(in srgb, var(--color-forms-buttons-outline-button-border) 75%, transparent); + --bs-btn-disabled-border-color: var(--color-forms-buttons-outline-button-border); } .btn-outline-secondary:hover { - background-color: #858796; - border-color: #858796; + background-color: color-mix(in srgb, var(--color-forms-buttons-outline-button-border), rgba(0, 0, 0, 0) 80%); + border-color: var(--color-forms-buttons-outline-button-border); } .btn-outline-secondary:focus, .btn-outline-secondary.focus { - box-shadow: 0 0 0 0.2rem rgba(133, 135, 150, 0.5); + box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--color-forms-buttons-outline-button-border), rgba(0, 0, 0, 0) 70%); } .btn-outline-secondary.disabled, .btn-outline-secondary:disabled { - color: #858796; + color: var(--color-forms-buttons-outline-button-border); } .btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active, .show > .btn-outline-secondary.dropdown-toggle { - background-color: #858796; - border-color: #858796; + background-color: var(--color-forms-buttons-outline-button-border); + border-color: var(--color-forms-buttons-outline-button-border); } .btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-secondary.dropdown-toggle:focus { - box-shadow: 0 0 0 0.2rem rgba(133, 135, 150, 0.5); + box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--color-forms-buttons-outline-button-border), rgba(0, 0, 0, 0) 70%); } .btn-outline-success { @@ -1043,22 +1017,22 @@ select.form-control:focus::-ms-value { .dropdown-menu { font-size: 0.85rem; - color: #858796; - border: 1px solid #e3e6f0; + color: var(--color-forms-input-text); + border: 1px solid var(--color-forms-input-border); border-radius: 0.35rem; } .dropdown-divider { - border-top: 1px solid #eaecf4; + border-top: 1px solid var(--color-sidebar-collapsible-section-border); } .dropdown-item { - color: #3a3b45; + color: var(--color-forms-input-text); } .dropdown-item:hover, .dropdown-item:focus { - color: #2e2f37; - background-color: #f8f9fc; + color: color-mix(in hsl, var(--color-forms-input-text), #fff 20%); + background-color: var(--color-forms-dropdown-hover); } .dropdown-item.active, .dropdown-item:active { @@ -1070,24 +1044,24 @@ select.form-control:focus::-ms-value { } .dropdown-header { - color: #858796; + color: var(--color-forms-input-text); } .dropdown-item-text { - color: #3a3b45; + color: var(--color-forms-input-text); } .input-group-text { - color: #6e707e; - background-color: #eaecf4; - border: 1px solid #d1d3e2; + color: var(--color-text); + background-color: var(--color-forms-input-background); + border: 1px solid var(--color-forms-input-border); border-radius: 0.35rem; } .form-select { - color: #6e707e; - background: #fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%235a5c69' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px; - border: 1px solid #d1d3e2; + color: var(--color-forms-input-text); + background: var(--color-forms-input-background) url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%235a5c69' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px; + border: 1px solid var(--color-forms-input-border); border-radius: 0.35rem; } @@ -1200,7 +1174,7 @@ select.form-control:focus::-ms-value { } .nav-tabs { - border-bottom: 1px solid #dddfeb; + border-bottom: 1px solid var(--color-tabs-border); } .nav-tabs .nav-link { @@ -1209,17 +1183,19 @@ select.form-control:focus::-ms-value { } .nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { - border-color: #eaecf4 #eaecf4 #dddfeb; + --bs-nav-link-hover-color: color-mix(in srgb, var(--color-text), transparent 25%); + border-color: var(--color-tabs-border); } .nav-tabs .nav-link.disabled { - color: #858796; + color: color-mix(in srgb, var(--color-text), transparent 20%); } .nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link { - color: #6e707e; - border-color: #dddfeb #dddfeb #fff; + color: var(--color-text); + border-color: var(--color-tabs-border) var(--color-tabs-border) var(--color-tabs-selected) !important; + --bs-nav-tabs-link-active-bg: var(--color-tabs-selected); } .nav-pills .nav-link { @@ -1231,7 +1207,8 @@ select.form-control:focus::-ms-value { } .card { - border: 1px solid #e3e6f0; + --bs-card-bg: var(--color-cards-background) !important; + border: 1px solid var(--color-cards-border); border-radius: 0.35rem; } @@ -1246,8 +1223,8 @@ select.form-control:focus::-ms-value { } .card-header { - background-color: #f8f9fc; - border-bottom: 1px solid #e3e6f0; + background-color: var(--color-cards-header-background); + border-bottom: 1px solid var(--color-cards-header-border); } .card-header:first-child { @@ -1255,8 +1232,8 @@ select.form-control:focus::-ms-value { } .card-footer { - background-color: #f8f9fc; - border-top: 1px solid #e3e6f0; + background-color: var(--color-cards-header-background); + border-top: 1px solid var(--color-cards-header-border); } .card-footer:last-child { @@ -1365,13 +1342,13 @@ select.form-control:focus::-ms-value { } .alert-success { - color: #0f6848; - background-color: #d2f4e8; - border-color: #bff0de; + color: var(--color-info-box-info-text); + background-color: var(--color-info-box-info); + border-color: color-mix(in hsl, var(--color-info-box-info), var(--color-info-box-info-text) 10%); } .alert-success hr { - border-top-color: #aaebd3; + border-top-color: color-mix(in hsl, var(--color-info-box-info), var(--color-info-box-info-text) 10%); } .alert-success .alert-link { @@ -1389,35 +1366,35 @@ select.form-control:focus::-ms-value { } .alert-info .alert-link { - color: #113b42; + color: color-mix(in hsl, var(--color-info-box-info-text), #000 10%); } .alert-warning { - color: #806520; - background-color: #fdf3d8; - border-color: #fceec9; + color: var(--color-info-box-notice-text); + background-color: var(--color-info-box-notice); + border-color: color-mix(in hsl, var(--color-info-box-notice), var(--color-info-box-notice-text) 10%); } .alert-warning hr { - border-top-color: #fbe6b1; + border-top-color: color-mix(in hsl, var(--color-info-box-notice), var(--color-info-box-notice-text) 10%); } .alert-warning .alert-link { - color: #574516; + color: color-mix(in hsl, var(--color-info-box-notice-text), #000 10%); } .alert-danger { - color: #78261f; - background-color: #fadbd8; - border-color: #f8ccc8; + color: var(--color-info-box-error-text); + background-color: var(--color-info-box-error); + border-color: color-mix(in hsl, var(--color-info-box-error), var(--color-info-box-error-text) 10%); } .alert-danger hr { - border-top-color: #f5b7b1; + border-top-color: color-mix(in hsl, var(--color-info-box-error), var(--color-info-box-error-text) 10%); } .alert-danger .alert-link { - color: #4f1915; + color: color-mix(in hsl, var(--color-info-box-error-text), #000 10%); } .alert-light { @@ -1651,7 +1628,7 @@ select.form-control:focus::-ms-value { } .modal-header { - border-bottom: 1px solid #e3e6f0; + border-bottom: 1px solid var(--color-cards-border); } .tooltip-inner { @@ -1850,14 +1827,14 @@ a.text-dark:hover, a.text-dark:focus { .table-bordered th, .table-bordered td { - border: 1px solid #dddfeb !important; + border: 1px solid var(--color-tables-border) !important; } } #wrapper #content-wrapper { - background-color: #f8f9fc; + background-color: var(--color-layout-background); + color: var(--color-text); width: 100%; - overflow-x: hidden; } #wrapper #content-wrapper #content { @@ -2166,12 +2143,13 @@ a.text-dark:hover, a.text-dark:focus { .dropdown .dropdown-menu { font-size: 0.85rem; + background-color: var(--color-forms-input-background); } .dropdown .dropdown-menu .dropdown-header { font-weight: 800; font-size: 0.65rem; - color: #b7b9cc; + color: var(--color-forms-input-text); } .dropdown.no-arrow .dropdown-toggle::after { @@ -2439,7 +2417,7 @@ a.text-dark:hover, a.text-dark:focus { padding: 0.5rem 1rem; margin: 0 0.5rem; display: block; - color: #3a3b45; + color: var(--color-sidebar-collapsible-section-text); text-decoration: none; border-radius: 0.35rem; white-space: nowrap; @@ -2447,7 +2425,7 @@ a.text-dark:hover, a.text-dark:focus { .sidebar .nav-item .collapse .collapse-inner .collapse-item:hover, .sidebar .nav-item .collapsing .collapse-inner .collapse-item:hover { - background-color: #eaecf4; + background-color: var(--color-sidebar-collapsible-section-hover); } .sidebar .nav-item .collapse .collapse-inner .collapse-item:active, @@ -2520,6 +2498,7 @@ a.text-dark:hover, a.text-dark:focus { .sidebar hr.sidebar-divider { margin: 0 1rem 1rem; + border-color: var(--color-sidebar-divider); } .sidebar .sidebar-heading { @@ -2705,7 +2684,7 @@ a.text-dark:hover, a.text-dark:focus { } .sidebar-light hr.sidebar-divider { - border-top: 1px solid #eaecf4; + border-top: 1px solid var(--color-sidebar-divider); } .sidebar-light .sidebar-heading { @@ -2757,23 +2736,23 @@ a.text-dark:hover, a.text-dark:focus { } .sidebar-dark hr.sidebar-divider { - border-top: 1px solid rgba(255, 255, 255, 0.15); + border-top: 1px solid var(--color-sidebar-divider); } .sidebar-dark .sidebar-heading { - color: rgba(255, 255, 255, 0.4); + color: color-mix(in srgb, var(--color-sidebar-text), transparent 60%); } .sidebar-dark .nav-item .nav-link { - color: rgba(255, 255, 255, 0.8); + color: color-mix(in srgb, var(--color-sidebar-text), transparent 20%); } .sidebar-dark .nav-item .nav-link i, .sidebar-dark .nav-item .nav-link svg { - color: rgba(255, 255, 255, 0.3); + color: color-mix(in srgb, var(--color-sidebar-text), transparent 70%); } .sidebar-dark .nav-item .nav-link:active, .sidebar-dark .nav-item .nav-link:focus, .sidebar-dark .nav-item .nav-link:hover { - color: #fff; + color: var(--color-sidebar-text); } .sidebar-dark .nav-item .nav-link:active i, diff --git a/Plan/react/dashboard/src/style/style.css b/Plan/react/dashboard/src/style/style.css index ec4c869fd4..5edc566bef 100644 --- a/Plan/react/dashboard/src/style/style.css +++ b/Plan/react/dashboard/src/style/style.css @@ -1,52 +1,10 @@ -:root { - --color-red: #F44336; - --color-pink: #E91E63; - --color-purple: #9C27B0; - --color-deep-purple: #673AB7; - --color-indigo: #3F51B5; - --color-blue: #2196F3; - --color-light-blue: #03A9F4; - --color-cyan: #00BCD4; - --color-teal: #009688; - --color-green: #4CAF50; - --color-light-green: #8BC34A; - --color-lime: #CDDC39; - --color-yellow: #ffe821; - --color-amber: #FFC107; - --color-orange: #FF9800; - --color-deep-orange: #FF5722; - --color-brown: #795548; - --color-grey: #9E9E9E; - --color-blue-grey: #607D8B; - --color-black: #555555; - --color-white: #ffffff; - --color-plan: #368F17; - --color-text-light-bg: #333; - --color-text-dark-bg: #fff; - --color-text-dark-bg-disabled: #ccc; - - --color-success: #1CC88A; - --color-warning: #F6C23E; - --color-danger: #e74A3B; - - --color-night-black: #282a36; - --color-night-dark-blue: #44475a; - --color-night-blue: #6272a4; - --color-night-grey-blue: #646e8c; - --color-night-dark-grey-blue: #606270; - --color-night-text-dark-bg: #eee8d5; - - --color-theme: var(--color-plan); - --color-night: var(--color-night-dark-blue); -} - a { text-decoration: none; } hr { background-color: transparent; - border-top: 1px solid rgba(0, 0, 0, 0.1); + border-top: 1px solid var(--color-layout-divider); opacity: 1; } @@ -76,12 +34,8 @@ small, .small { } .table { - --bs-table-striped-color: none; -} - -.table-dark { - --bs-table-striped-bg: rgba(255, 255, 255, 0.05); - --bs-table-bg: none; + --bs-table-striped-bg: transparent; + --bs-table-bg: transparent; } .table-dark.table-bordered { @@ -174,8 +128,9 @@ p.collapsing { display: inline-block; width: 2rem; height: 2rem; - border: 4px solid var(--color-theme); - background-color: var(--color-theme); + border: 4px solid var(--color-layout-loader-border); + background: var(--color-layout-loader-background); + background-color: var(--color-layout-loader-background); border-radius: 5px; animation: loader 2s infinite ease; } @@ -805,151 +760,151 @@ div#navSrvContainer::-webkit-scrollbar-thumb { .bg-transparent-light { background-color: transparent; - color: #fff; + color: var(--color-sidebar-text); } .bg-red, body.theme-red .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-red); --bs-btn-disabled-bg: var(--color-red); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-pink, body.theme-pink .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-pink); --bs-btn-disabled-bg: var(--color-pink); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-purple, body.theme-purple .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-purple); --bs-btn-disabled-bg: var(--color-purple); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-deep-purple, body.theme-deep-purple .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-deep-purple); --bs-btn-disabled-bg: var(--color-deep-purple); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-indigo, body.theme-indigo .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-indigo); --bs-btn-disabled-bg: var(--color-indigo); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-blue, body.theme-blue .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-blue); --bs-btn-disabled-bg: var(--color-blue); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-light-blue, body.theme-light-blue .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-light-blue); --bs-btn-disabled-bg: var(--color-light-blue); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-cyan, body.theme-cyan .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-cyan); --bs-btn-disabled-bg: var(--color-cyan); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-teal, body.theme-teal .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-teal); --bs-btn-disabled-bg: var(--color-teal); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-green, body.theme-green .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-green); --bs-btn-disabled-bg: var(--color-green); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-light-green, body.theme-light-green .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-light-green); --bs-btn-disabled-bg: var(--color-light-green); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-lime, body.theme-lime .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-lime); --bs-btn-disabled-bg: var(--color-lime); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-yellow, body.theme-yellow .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-yellow); --bs-btn-disabled-bg: var(--color-yellow); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-amber, body.theme-amber .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-amber); --bs-btn-disabled-bg: var(--color-amber); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-orange, body.theme-orange .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-orange); --bs-btn-disabled-bg: var(--color-orange); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-deep-orange, body.theme-deep-orange .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-deep-orange); --bs-btn-disabled-bg: var(--color-deep-orange); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-brown, body.theme-brown .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-brown); --bs-btn-disabled-bg: var(--color-brown); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-grey, body.theme-grey .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-grey); --bs-btn-disabled-bg: var(--color-grey); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-blue-grey, body.theme-blue-grey .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-blue-grey); --bs-btn-disabled-bg: var(--color-blue-grey); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-black, body.theme-black .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-black); --bs-btn-disabled-bg: var(--color-black); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-white, body.theme-white .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-white); --bs-btn-disabled-bg: var(--color-white); - color: var(--color-text-light-bg); + color: var(--color-text-light); } .bg-plan, body.theme-plan .fc-toolbar-chunk .btn.btn-primary { background-color: var(--color-plan); --bs-btn-disabled-bg: var(--color-plan); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-theme, body.theme .fc-toolbar-chunk .btn.btn-primary { - background-color: var(--color-theme); + background: var(--color-theme); --bs-btn-disabled-bg: var(--color-theme); - color: var(--color-text-dark-bg); + color: var(--color-text-dark); } .bg-theme.btn:hover, body.theme .fc-toolbar-chunk .btn.btn-primary:hover, .fc-button:hover { background-color: color-mix(in srgb, var(--color-theme) 90%, black) !important; --bs-btn-disabled-bg: var(--color-theme); - color: color-mix(in srgb, var(--color-text-dark-bg) 85%, black) !important; + color: color-mix(in srgb, var(--color-text-dark) 85%, black) !important; } .bg-success { @@ -986,7 +941,7 @@ div#navSrvContainer::-webkit-scrollbar-thumb { .btn.bg-grey:hover, .btn.bg-blue-grey:hover, .btn.bg-transparent-light:hover { - color: var(--color-text-dark-bg-disabled) !important; + color: color-mix(in srgb, var(--color-sidebar-text), transparent 35%); background-color: var(--bs-btn-disabled-bg); } @@ -1217,78 +1172,15 @@ div#navSrvContainer::-webkit-scrollbar-thumb { } .col-theme { - color: var(--color-theme); -} - -button.col-theme:hover, a.col-theme:hover { - color: color-mix(in srgb, var(--color-theme) 60%, black) -} - -/* Minecraft color codes */ -.black { - color: #000000; -} - -.darkblue { - color: #0000AA; -} - -.darkgreen { - color: #00AA00; -} - -.darkaqua { - color: #00AAAA; -} - -.darkred { - color: #AA0000; -} - -.darkpurple { - color: #AA00AA; -} - -.gold { - color: #FFAA00; + color: var(--color-themeText); } -.gray { - color: #AAAAAA; +.col-text { + color: var(--color-text); } -.darkgray { - color: var(--color-black); -} - -.blue { - color: #5555FF; -} - -.green { - color: #55FF55; -} - -.aqua { - color: #55FFFF; -} - -.red { - color: #FF5555; -} - -.pink { - color: #FF55FF; -} - -.yellow { - color: #FFFF55; - text-shadow: 0 0 6px #000; -} - -.white { - color: #FFFFFF; - text-shadow: 0 0 8px #000; +button.col-theme:hover, a.col-theme:hover { + color: color-mix(in srgb, var(--color-themeText) 60%, var(--contrast-color-themeText)) } .italic { @@ -1463,7 +1355,7 @@ ul.filters { #wrapper { display: flex; min-height: 100vh; - background-image: linear-gradient(to right, var(--color-theme) 0%, var(--color-theme) 14rem, #f8f9fc 14.01rem, #f8f9fc 100%); + background-image: linear-gradient(to right, var(--color-theme) 0%, var(--color-theme) 14rem, var(--color-layout-background) 14.01rem, var(--color-layout-background) 100%); } .contributor { @@ -1508,3 +1400,15 @@ ul.filters { .link:hover { color: var(--bs-link-hover-color) } + +.card-header .btn.float-end { + margin-top: -0.5rem; +} + +/* Fix vertical alignment of dropdown toggle arrow */ +.dropdown-toggle { + height: 100%; + display: flex; + justify-content: space-between; + align-items: center; +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/util/Color.js b/Plan/react/dashboard/src/util/Color.js new file mode 100644 index 0000000000..a896e73646 --- /dev/null +++ b/Plan/react/dashboard/src/util/Color.js @@ -0,0 +1,229 @@ +import { + calculateCssHexColor, + hexToRgb, + hslaStringToArray, + hslToHsv, + hslToString, + hsvToRgb, + hsxStringToArray, + rgbaStringToArray, + rgbaToString, + rgbStringToArray, + rgbToHexString, + rgbToHsl, + rgbToString +} from "./colors.js"; + +export const getColorConverter = color => { + try { + if (typeof color === 'string') { + if (color.startsWith("var(")) return new HexColor(calculateCssHexColor(color)); + if (color.startsWith('#')) return new HexColor(color); + if (color.startsWith("rgb(") || color.startsWith("rgba(") && color.endsWith(')')) return new RgbaColor(color); + if (color.startsWith("hsl(") || color.startsWith("hsla(") && color.endsWith(')')) return new HslaColor(color); + if (color.startsWith("hsv(") && color.endsWith(')')) return new HsvColor(color); + } + } catch (e) { + console.warn('failed to parse color', color, e); + } + return undefined; +} + +export const getColorArrayConverter = (color, type) => { + switch (type) { + case 'hex': + return new HexColor(color); + case 'rgb': + case 'rgba': + return new RgbaColor(color); + case 'hsl': + case 'hsla': + return new HslaColor(color); + case 'hsv': + return new HsvColor(color); + default: + return undefined; + } +} + +export const withReducedSaturationRgba = (rgba, reduceSaturationPercentage) => { + const saturationReduction = reduceSaturationPercentage || 0.70; + + const [h, s, l] = getColorArrayConverter(rgba, 'rgba').toHslArray(); + if (isNaN(h)) console.log(rgba, [h, s, l]); + + return 'hsl(' + h * 360 + ',' + s * 100 * saturationReduction + '%,' + l * 95 + '%)'; +} +export const getContrastColor = (color) => { + const converter = getColorConverter(color); + if (!converter) return undefined; + const luminance = converter.toLuminance(); + return luminance < 0.5 ? '#ffffff' : '#000000'; +}; + +export class Color { + toHex() { + return rgbToHexString(this.toRgbArray()) + } + + toRgbString() { + return rgbToString(this.toRgbArray()) + } + + toRgbArray() { + return this.toRgbaArray().slice(0, 3); + } + + toRgbaString() { + return rgbaToString(this.toRgbaArray()) + } + + toRgbaArray() { + return [...this.toRgbArray(), 1]; + } + + toHslString() { + return hslToString(this.toHslArray()) + } + + toHslArray() { + return rgbToHsl(this.toRgbArray()) + } + + toHsvString() { + return hsvToString(this.toHsvArray()); + } + + toHsvArray() { + return hslToHsv(this.toHslArray()); + } + + toHsvaArray() { + const rgba = this.toRgbaArray(); + return [...this.toHslArray(), rgba[3]]; + } + + reduceSaturation(reductionPercentage) { + const rgba = this.toRgbaArray(); + const hslArray = new HslaColor(withReducedSaturationRgba(rgba, reductionPercentage)).toHslArray(); + return new HslaColor([...hslArray, rgba[3]]); + } + + increaseHue(amount) { + const rgba = this.toRgbaArray(); + const asHsv = getColorArrayConverter(this.toHsvArray(), 'hsv'); + asHsv.hsv[0] = (asHsv.hsv[0] + amount) % 1; + const asRgba = asHsv.toRgbaArray(); + asRgba[3] = rgba[3]; + return getColorArrayConverter(asRgba, 'rgba'); + } + + toLuminance() { + const [r, g, b] = this.toRgbArray(); + return (0.299 * r + 0.587 * g + 0.114 * b) / 255; + } +} + +class HexColor extends Color { + constructor(hexString) { + super(); + this.hexString = hexString; + } + + toHex() { + return this.hexString; + } + + toRgbArray() { + return hexToRgb(this.hexString) + } +} + +class RgbaColor extends Color { + constructor(rgba) { + super(); + if (typeof rgba === 'string') { + if (rgba.startsWith("rgba")) { + this.rgba = rgbaStringToArray(rgba); + } else { + this.rgba = [...rgbStringToArray(rgba), 1]; + } + } else { + if (rgba.length === 4) { + this.rgba = rgba; + } else { + this.rgba = [...rgba, 1]; + } + } + } + + toHex() { + return rgbToHexString(this.toRgbArray()); + } + + toRgbArray() { + return this.rgba.slice(0, 3); + } + + toRgbaArray() { + return this.rgba; + } +} + +class HslaColor extends Color { + constructor(hsla) { + super(); + if (typeof hsla === 'string') { + if (hsla.startsWith("hsla")) { + this.hsla = hslaStringToArray(hsla); + } else { + this.hsla = [...hsxStringToArray(hsla), 1]; + } + } else { + if (hsla.length === 4) { + this.hsla = hsla; + } else { + this.hsla = [...hsla, 1]; + } + } + } + + toHex() { + return rgbToHexString(this.toRgbArray()); + } + + toRgbArray() { + return hsvToRgb(this.toHsvArray()) + } + + toRgbaArray() { + return [...this.toRgbArray(), this.hsla[3]] + } + + toHslArray() { + return this.hsla.slice(0, 3); + } + + toHsvArray() { + return hslToHsv(this.toHslArray()) + } +} + +class HsvColor extends Color { + constructor(hsv) { + super(); + if (typeof hsv === 'string') { + this.hsv = hsxStringToArray(hsv); + } else { + this.hsv = hsv; + } + } + + toHsvArray() { + return this.hsv; + } + + toRgbArray() { + return hsvToRgb(this.hsv) + } +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/util/colors.js b/Plan/react/dashboard/src/util/colors.js index 6205aff26d..3a53a8a234 100644 --- a/Plan/react/dashboard/src/util/colors.js +++ b/Plan/react/dashboard/src/util/colors.js @@ -1,130 +1,17 @@ -const colorMap = { - PLAN: { - name: "plan", - hex: "#468F17" - }, - RED: { - name: "red", - hex: "#F44336" - }, - PINK: { - name: "pink", - hex: "#E91E63" - }, - PURPLE: { - name: "purple", - hex: "#9C27B0" - }, - DEEP_PURPLE: { - name: "deep-purple", - hex: "#673AB7" - }, - INDIGO: { - name: "indigo", - hex: "#3F61B5" - }, - BLUE: { - name: "blue", - hex: "#2196F3" - }, - LIGHT_BLUE: { - name: "light-blue", - hex: "#03A9F4" - }, - CYAN: { - name: "cyan", - hex: "#00BCD4" - }, - TEAL: { - name: "teal", - hex: "#009688" - }, - GREEN: { - name: "green", - hex: "#4CAF50" - }, - LIGHT_GREEN: { - name: "light-green", - hex: "#8BC34A" - }, - LIME: { - name: "lime", - hex: "#CDDC39" - }, - YELLOW: { - name: "yellow", - hex: "#FFE821" - }, - AMBER: { - name: "amber", - hex: "#FFC107" - }, - ORANGE: { - name: "orange", - hex: "#FF9800" - }, - DEEP_ORANGE: { - name: "deep-orange", - hex: "#FF5722" - }, - BROWN: { - name: "brown", - hex: "#795548" - }, - GREY: { - name: "grey", - hex: "#9E9E9E" - }, - BLUE_GREY: { - name: "blue-grey", - hex: "#607D8B" - }, - BLACK: { - name: "black", - hex: "#555555" - }, - SUCCESS: { - name: "success", - hex: "#1CC88A" - }, - WARNING: { - name: "warning", - hex: "#F6C23E" - }, - DANGER: { - name: "danger", - hex: "#e74A3B" - }, - NONE: "" -}; - -export const getColors = () => { - return Object.values(colorMap).filter(color => color); +export const nameToCssVariable = name => { + return `var(--color-${name})`; } -export const colorEnumToColorClass = color => { - const mapped = "col-" + colorMap[color].name; - return mapped ? mapped : ""; +export const nameToContrastCssVariable = name => { + return `var(--contrast-color-${name})`; } -export const bgClassToColorClass = bgClass => { - return "col-" + bgClass.substring(3); +export const cssVariableToName = cssVariable => { + return cssVariable?.replace('var(--color-', '').replace(')', '') } -export const colorClassToColorName = (colorClass) => { - return colorClass.substring(4); -} - -export const colorEnumToBgClass = color => { - return "bg-" + color; -} - -export const colorClassToBgClass = colorClass => { - return "bg-" + colorClassToColorName(colorClass); -} - -export const hsxStringToArray = (hsvString) => { - const color = hsvString.substring(4, hsvString.length - 1); +export const hsxStringToArray = (hsxString) => { + const color = hsxString.substring(4, hsxString.length - 1); const split = color.split(','); const h = Number(split[0]); const s = Number(split[1].substring(0, split[1].length - 1)); @@ -132,17 +19,41 @@ export const hsxStringToArray = (hsvString) => { return [h, s, x]; } +export const hslaStringToArray = (hslaString) => { + const color = hslaString.substring(4, hslaString.length - 1); + const split = color.split(','); + const h = Number(split[0]); + const s = Number(split[1].substring(0, split[1].length - 1)); + const l = Number(split[2].substring(0, split[2].length - 1)); + const a = Number(split[3].substring(0, split[3].length - 1)); + return [h, s, l, a]; +} + export const hslToHsv = ([h, s, l]) => { - const hsv1 = s * (l < 50 ? l : 100 - l) / 100; - const hsvS = hsv1 === 0 ? 0 : 2 * hsv1 / (l + hsv1) * 100; - const hsvV = l + hsv1; - return [h, hsvS, hsvV]; + // Normalize s and l if they are > 1 (i.e., in [0, 100]) + if (h > 1 || s > 1 || l > 1) { + h = h / 360; + s = s / 100; + l = l / 100; + } + const v = l + s * Math.min(l, 1 - l); + const newS = v === 0 ? 0 : 2 * (1 - l / v); + // Clamp to [0, 1] + return [ + h, + Math.max(0, Math.min(1, newS)), + Math.max(0, Math.min(1, v)) + ]; +} + +export const hsvToHex = (hsv) => { + return rgbToHexString(hsvToRgb(hsv)); } export const hsvToRgb = ([h, s, v]) => { let r, g, b; - if (s > 1) { + if (h > 1 || s > 1 || v > 1) { h = h / 360; s = s / 100; v = v / 100; @@ -200,6 +111,27 @@ export const randomHSVColor = (i) => { return [hue, saturation, value] } +export const rgbStringToArray = (rgbString) => { + const colors = rgbString.substring(4, rgbString.length - 1); + const split = colors.split(','); + return [ + Number(split[0].trim()), + Number(split[1].trim()), + Number(split[2].trim()) + ]; +} + +export const rgbaStringToArray = (rgbaString) => { + const colors = rgbaString.substring(5, rgbaString.length - 1); + const split = colors.split(','); + return [ + Number(split[0].trim()), + Number(split[1].trim()), + Number(split[2].trim()), + split.length === 4 ? Number(split[3].trim()) : 1 + ]; +} + export const rgbToHexString = ([r, g, b]) => { return '#' + rgbToHex(r) + rgbToHex(g) + rgbToHex(b); } @@ -209,10 +141,22 @@ const rgbToHex = (component) => { } export const hexToRgb = (hexString) => { - const r = parseInt(hexString.substring(1, 3), 16); - const g = parseInt(hexString.substring(3, 5), 16); - const b = parseInt(hexString.substring(5, 7), 16); - return [r, g, b]; + const hex = hexString.replace('#', ''); + if (hex.length === 6) { + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + return [r, g, b]; + } else { + // 3 digit hex + const rLetter = hex.substring(0, 1); + const gLetter = hex.substring(1, 2); + const bLetter = hex.substring(2, 3); + const r = parseInt(rLetter + rLetter, 16); + const g = parseInt(gLetter + gLetter, 16); + const b = parseInt(bLetter + bLetter, 16); + return [r, g, b]; + } } // https://css-tricks.com/converting-color-spaces-in-javascript/ @@ -229,6 +173,7 @@ export const rgbToHsl = ([r, g, b]) => { } else { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + s = Math.max(0, Math.min(1, s)); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); @@ -252,43 +197,83 @@ export const withReducedSaturation = (hex, reduceSaturationPercentage) => { const saturationReduction = reduceSaturationPercentage ? reduceSaturationPercentage : 0.70; const rgb = hexToRgb(hex); - const [h, s, l] = rgbToHsl(rgb); + let [h, s, l] = rgbToHsl(rgb); + + // Ensure s and l are in [0, 1] + if (s > 1) s = s / 100; + if (l > 1) l = l / 100; // To css property return 'hsl(' + h * 360 + ',' + s * 100 * saturationReduction + '%,' + l * 95 + '%)'; } -const createNightModeColorCss = () => { - return ':root {' + getColors() - .filter(color => color.name !== 'white' && color.name !== 'black' && color.name !== 'plan') - .map(color => { - const desaturatedColor = withReducedSaturation(color.hex); - return `--color-${color.name}: ${desaturatedColor} !important;` - }).join('') + '}'; +export const rgbToString = ([r, g, b]) => { + return `rgb(${r}, ${g}, ${b})`; +} + +export const rgbaToString = ([r, g, b, a]) => { + return `rgba(${r}, ${g}, ${b}, ${a})`; +} + +export const hslToString = ([h, s, l]) => { + return `hsl(${h}, ${s}%, ${l}%)`; +} + +export const hsvToString = ([h, s, v]) => { + return `hsv(${h}, ${s}%, ${v}%)`; +} + +export const calculateCssHexColor = (cssColor, inElement) => { + const colorCalculationElement = document.createElement('div'); + colorCalculationElement.style.display = 'none'; + colorCalculationElement.style.color = cssColor; + const element = inElement || document.body; + element.appendChild(colorCalculationElement); + const rgbString = window.getComputedStyle(colorCalculationElement, null).getPropertyValue("color"); + const hex = rgbToHexString(rgbStringToArray(rgbString)); + element.removeChild(colorCalculationElement); + return hex; +} + +export const calculateCssColors = (cssSelector) => { + const colors = { + color: null, + backgroundColor: null, + borderColor: null + }; + + // Search through all document stylesheets + for (const stylesheet of document.styleSheets) { + try { + // Skip if we can't access the rules (e.g., cross-origin stylesheets) + if (!stylesheet.cssRules) continue; + + // Look through all rules in the stylesheet + for (const rule of stylesheet.cssRules) { + if (rule instanceof CSSStyleRule && rule.selectorText === cssSelector) { + const style = rule.style; + + // Get color if set + if (style.color) { + colors.color = style.color; + } + + // Get background-color if set + if (style.backgroundColor) { + colors.backgroundColor = style.backgroundColor; + } + + // Get border-color if set + if (style.borderColor) { + colors.borderColor = style.borderColor; + } + } + } + } catch (e) { + // Skip stylesheets we can't access + } + } + + return colors; } -export const createNightModeCss = () => { - return `#content-wrapper {background-color:var(--color-night-black)!important;}` + - `#wrapper {background-image: linear-gradient(to right, var(--color-night-dark-blue) 0%, var(--color-night-dark-blue) 14rem, var(--color-night-black) 14.01rem, var(--color-night-black) 100%);}` + - `body,.btn,.bg-transparent-light {color: var(--color-night-text-dark-bg) !important;}` + - `.card,.bg-white,.modal-content,.page-loader,.nav-tabs .nav-link:hover,.nav-tabs,hr,form .btn, .btn-outline-secondary{background-color:var(--color-night-dark-blue)!important;border-color:var(--color-night-blue)!important;}` + - `.bg-white.collapse-inner {border:1px solid;}` + - `.card-header {background-color:var(--color-night-dark-blue);border-color:var(--color-night-blue);}` + - `#content,.col-black,.text-gray-900,.text-gray-800,.collapse-item,.modal-title,.modal-body,.page-loader,.fc-title,.fc-time,pre,.table-dark,input::placeholder{color:var(--color-night-text-dark-bg) !important;}` + - `.collapse-item:hover,.nav-link.active {background-color: var(--color-night-dark-grey-blue) !important;}` + - `.nav-tabs .nav-link.active {background-color: var(--color-night-dark-blue) !important;border-color:var(--color-night-blue) var(--color-night-blue) var(--color-night-dark-blue) !important;}` + - `.fc-today {background:var(--color-night-grey-blue) !important}` + - `.fc-popover-body,.fc-popover-header{background-color: var(--color-night-dark-blue) !important;color: var(--color-night-text-dark-bg) !important;}` + - `select,input,.dataTables_paginate .page-item:not(.active) a,.input-group-text,.input-group-text > * {background-color:var(--color-night-dark-blue) !important;border-color:var(--color-night-blue) !important;color: var(--color-night-text-dark-bg) !important;}` + - `input.form-check-input:checked {background-color:var(--color-night-blue) !important;border-color:var(--color-night-blue) !important;color: var(--color-night-text-dark-bg) !important;}` + - `.topbar-divider,.fc td,.fc tr,.fc th, .fc table, .modal-header,.modal-body,.modal-footer{border-color:var(--color-night-blue) !important;}` + - `.fc a{color:var(--color-night-text-dark-bg) !important;}` + - `.fc-button{ background-color: ${withReducedSaturation(colorMap.PLAN.hex)} !important;}` + - `.loader{border: 4px solid var(--color-plan); background-color: var(--color-plan);}` + - `.dropdown-item,.dropdown-header{color: var(--color-night-text-dark-bg) !important;}` + - `.dropdown-item:hover{background-color: var(--color-night-blue) !important;}` + - `.dropdown-menu{border-color:var(--color-night-blue);color: var(--color-night-blue) !important;}` + - `.col-theme{--color-theme: var(--color-night-text-dark-bg)}` + - `:root {--bs-heading-color:var(--color-night-text-dark-bg); --bs-card-color:var(--color-night-text-dark-bg); --bs-body-color:var(--color-night-text-dark-bg); --bs-body-bg:var(--color-night-dark-grey-blue); --bs-btn-active-border-color:var(--color-night-blue);}` + - createNightModeColorCss() -} \ No newline at end of file diff --git a/Plan/react/dashboard/src/util/format/SimpleDateFormat.js b/Plan/react/dashboard/src/util/format/SimpleDateFormat.js index 35889b906f..85d4bac576 100644 --- a/Plan/react/dashboard/src/util/format/SimpleDateFormat.js +++ b/Plan/react/dashboard/src/util/format/SimpleDateFormat.js @@ -1,4 +1,20 @@ import moment from 'moment'; +import 'moment/dist/locale/es'; +import 'moment/dist/locale/zh-cn'; +import 'moment/dist/locale/cs'; +import 'moment/dist/locale/de'; +import 'moment/dist/locale/fi'; +import 'moment/dist/locale/fr'; +import 'moment/dist/locale/it'; +import 'moment/dist/locale/ja'; +import 'moment/dist/locale/ko'; +import 'moment/dist/locale/nl'; +import 'moment/dist/locale/ru'; +import 'moment/dist/locale/tr'; +import 'moment/dist/locale/uk'; +import 'moment/dist/locale/pt-br'; +import 'moment/dist/locale/zh-tw'; +import {localeService} from "../../service/localeService.js"; export const SimpleDateFormat = function (pattern) { this.pattern = pattern; @@ -20,6 +36,7 @@ SimpleDateFormat.prototype.applyPattern = function (pattern) { SimpleDateFormat.prototype.format = function (date) { let formattedString = ""; + moment.locale(localeService.getIntlFriendlyLocale()); const d = moment.utc(date); let p = this._or(this.pattern, this._defaultPatternsByLocale[d.locale()]); diff --git a/Plan/react/dashboard/src/util/format/TimeAmountFormat.js b/Plan/react/dashboard/src/util/format/TimeAmountFormat.js index 0480db52b5..1f550eae7d 100644 --- a/Plan/react/dashboard/src/util/format/TimeAmountFormat.js +++ b/Plan/react/dashboard/src/util/format/TimeAmountFormat.js @@ -1,3 +1,5 @@ +import {localeService} from "../../service/localeService.js"; + const ZERO_PH = "%zero%"; const SECONDS_PH = "%seconds%"; const MINUTES_PH = "%minutes%"; @@ -10,7 +12,6 @@ const YEARS_PH = "%years%"; * Based on the Java equivalent TimeAmountFormatter.java */ export function formatTimeAmount(options, timeMs) { - const apply = (ms) => { if (ms === null || ms < 0) { return "-"; @@ -28,100 +29,27 @@ export function formatTimeAmount(options, timeMs) { x /= 365; let years = x; - let builder = ""; - builder += appendYears(Math.floor(years)); - builder += appendMonths(Math.floor(months)); - builder += appendDays(Math.floor(days)); - - const hourFormat = options.HOURS; - const minuteFormat = options.MINUTES; - const secondFormat = options.SECONDS; - - builder += appendHours(Math.floor(hours), hourFormat); - builder += appendMinutes(Math.floor(minutes), Math.floor(hours), hourFormat, minuteFormat); - builder += appendSeconds(Math.floor(seconds), Math.floor(minutes), Math.floor(hours), hourFormat, minuteFormat, secondFormat); - - const formattedTime = builder.replaceAll(ZERO_PH, ''); - if (formattedTime.length === 0) { - return options.ZERO; - } - return formattedTime; - } - - const appendHours = (hours, fHours) => { - if (hours !== 0 || fHours.includes(ZERO_PH)) { - let h = fHours.replace(HOURS_PH, String(hours)); - if (h.includes(ZERO_PH) && String(hours).length === 1) { - h = '0' + h; - } - return h; - } - return ''; - } - - const appendMinutes = (minutes, hours, fHours, fMinutes) => { - if (minutes !== 0 || fMinutes.includes(ZERO_PH)) { - let m = fMinutes.replace(MINUTES_PH, String(minutes)); - if (hours === 0 && m.includes(HOURS_PH)) { - m = fHours.replace(ZERO_PH, "0").replace(HOURS_PH, "0") + m; - m = m.replace(HOURS_PH, ""); - } - m = m.replace(HOURS_PH, ""); - if (m.includes(ZERO_PH) && String(minutes).length === 1) { - m = '0' + m; - } - return m; - } - return ''; - } - - const appendSeconds = (seconds, minutes, hours, fHours, fMinutes, fSeconds) => { - if (seconds !== 0 || fSeconds.includes(ZERO_PH)) { - let s = fSeconds.replace(SECONDS_PH, String(seconds)); - if (minutes === 0 && s.includes(MINUTES_PH)) { - if (hours === 0 && fMinutes.includes(HOURS_PH)) { - s = fHours.replace(ZERO_PH, "0").replace(HOURS_PH, "0") + s; - } - s = fMinutes.replace(HOURS_PH, "").replace(ZERO_PH, "0").replace(MINUTES_PH, "0") + s; - } - s = s.replace(MINUTES_PH, ""); - if (s.includes(ZERO_PH) && String(seconds).length === 1) { - s = '0' + s; - } - return s; - } - return ''; - } - - const appendDays = (days) => { - const singular = options.DAY; - const plural = options.DAYS; - return appendValue(days, singular, plural, DAYS_PH); - } - - const appendMonths = (months) => { - const singular = options.MONTH; - const plural = options.MONTHS; - - return appendValue(months, singular, plural, MONTHS_PH); - } - - const appendYears = (years) => { - const singular = options.YEAR; - const plural = options.YEARS; - - return appendValue(years, singular, plural, YEARS_PH); - } - - const appendValue = (amount, singular, plural, replace) => { - if (amount !== 0) { - if (amount === 1) { - return singular; - } else { - return plural.replace(replace, String(amount)); - } + const format = { + style: 'narrow', + years: 'long', + yearsDisplay: 'auto', + months: 'long', + monthsDisplay: 'auto', + secondsDisplay: ms < 1000 ? 'always' : 'auto', + }; + // Temporary support for old format, we can later move the whole thing to the preferences menu, + // so that there's a dropdown of options that are same as above, but modified for each option. + if (options.HOURS.includes(ZERO_PH) || options.MINUTES.includes(ZERO_PH)) { + format.style = 'digital'; } - return ''; + return new Intl.DurationFormat(localeService.getIntlFriendlyLocale(), format).format({ + years: Math.floor(years), + months: Math.floor(months), + days: Math.floor(days), + hours: Math.floor(hours), + minutes: Math.floor(minutes), + seconds: years >= 1 || months > 1 || days > 1 ? undefined : Math.floor(seconds) + }); } return apply(timeMs); diff --git a/Plan/react/dashboard/src/util/formatters.js b/Plan/react/dashboard/src/util/formatters.js index 7a7ae33a35..6145095339 100644 --- a/Plan/react/dashboard/src/util/formatters.js +++ b/Plan/react/dashboard/src/util/formatters.js @@ -1,32 +1,6 @@ -export const formatTimeAmount = (ms) => { - let out = ""; - - let seconds = Math.floor(ms / 1000); - - const dd = Math.floor(seconds / 86400); - seconds -= (dd * 86400); - const dh = Math.floor(seconds / 3600); - seconds -= (dh * 3600); - const dm = Math.floor(seconds / 60); - seconds -= (dm * 60); - seconds = Math.floor(seconds); - if (dd !== 0) { - out += dd.toString() + "d "; - } - if (dh !== 0) { - out += dh.toString() + "h "; - } - if (dm !== 0) { - out += dm.toString() + "m "; - } - out += seconds.toString() + "s "; - - return out; -} - export const formatDecimals = (value, formatPattern) => { - if (!formatPattern) return value; - const split = formatPattern.split('.'); + if (!formatPattern || isNaN(value)) return value; + const split = formatPattern.includes('.') ? formatPattern.split('.') : formatPattern.split(','); if (split.length <= 1) return value.toFixed(0); return value.toFixed(split[1].length); } \ No newline at end of file diff --git a/Plan/react/dashboard/src/util/graphColors.js b/Plan/react/dashboard/src/util/graphColors.js index ef5facbbb6..2286728f54 100644 --- a/Plan/react/dashboard/src/util/graphColors.js +++ b/Plan/react/dashboard/src/util/graphColors.js @@ -1,231 +1,127 @@ -export const getLightModeChartTheming = () => { +export const getChartTheming = () => { return { // Defaults chart: { backgroundColor: null, - plotBorderColor: '#cccccc' + plotBorderColor: 'var(--color-text)' }, title: { - style: {color: '#3E576F'} + style: {color: 'var(--color-text)'} }, subtitle: { - style: {color: '#3E576F'} + style: {color: 'var(--color-text)'} }, xAxis: { - gridLineColor: '#E6E6E6', + gridLineColor: 'var(--color-graphs-style-grid-line)', labels: { - style: {color: '#333333'} + style: {color: 'var(--color-text)'} }, - lineColor: '#E6E6E6', - minorGridLineColor: '#505053', - tickColor: '#E6E6E6', + lineColor: 'var(--color-graphs-style-grid-line)', + minorGridLineColor: 'var(--color-graphs-style-minor-grid-line)', + tickColor: 'var(--color-graphs-style-grid-line)', title: { - style: {color: '#333333'} + style: {color: 'var(--color-text)'} } }, yAxis: { - gridLineColor: '#E6E6E6', + gridLineColor: 'var(--color-graphs-style-grid-line)', labels: { - style: {color: '#333333'} + style: {color: 'var(--color-text)'} }, - lineColor: '#E6E6E6', - minorGridLineColor: '#505053', - tickColor: '#E6E6E6', + lineColor: 'var(--color-graphs-style-grid-line)', + minorGridLineColor: 'var(--color-graphs-style-minor-grid-line)', + tickColor: 'var(--color-graphs-style-grid-line)', tickWidth: 1, title: { - style: {color: '#333333'} + style: {color: 'var(--color-text)'} } }, tooltip: { - backgroundColor: 'rgba(247,247,247,0.85)', - style: {color: '#333333'} + backgroundColor: 'var(--color-graphs-style-tooltip-background)', + style: {color: 'var(--color-text)'} }, plotOptions: { series: { dataLabels: {color: undefined}, - marker: {lineColor: undefined} + marker: {lineColor: undefined}, + borderColor: 'var(--color-graphs-style-border)' } }, legend: { - itemStyle: {color: '#333333'}, - itemHoverStyle: {color: '#000000'}, - itemHiddenStyle: {color: '#cccccc'} + itemStyle: {color: 'var(--color-text)'}, + itemHoverStyle: {color: 'var(--color-text)'}, + itemHiddenStyle: {color: 'color-mix(in srgb, var(--color-text), transparent 50%)'} }, labels: { - style: {color: '#333333'} + style: {color: 'var(--color-text)'} }, drilldown: { - activeAxisLabelStyle: {color: '#333333'}, - activeDataLabelStyle: {color: '#333333'} + activeAxisLabelStyle: {color: 'var(--color-text)'}, + activeDataLabelStyle: {color: 'var(--color-text)'} }, navigation: { buttonOptions: { - symbolStroke: '#333333', - theme: {fill: '#CCCCCC'} + symbolStroke: 'var(--color-text)', + theme: {fill: 'var(--color-text)'} } }, // scroll charts rangeSelector: { buttonTheme: { - fill: '#F7F7F7', - stroke: '#333', - style: {color: '#4B336A'}, + fill: 'var(--color-graphs-style-selector-button-background)', + stroke: 'var(--color-text)', + style: {color: 'var(--color-text)'}, states: { hover: { - fill: '#E6EBF5', - stroke: '#333', - style: {color: 'black'} + fill: 'var(--color-graphs-style-selector-button-hover)', + stroke: 'var(--color-text)', + style: {color: 'var(--contrast-color-graphs-style-selector-button-hover)'} }, select: { - fill: '#E6EBF5', - stroke: '#333', - style: {color: 'black'} + fill: 'var(--color-graphs-style-selector-button-selected)', + stroke: 'var(--color-text)', + style: {color: 'var(--contrast-color-graphs-style-selector-button-selected)'} } } }, - inputBoxBorderColor: '#CCCCCC', + inputBoxBorderColor: 'var(--color-graphs-style-selector-text-input-border)', inputStyle: { - backgroundColor: '#333', - color: '#666666' + backgroundColor: 'var(--color-graphs-style-selector-text-input-background)', + color: 'var(--color-text)' }, - labelStyle: {color: "#666666"} + labelStyle: {color: 'var(--color-text)'} }, navigator: { handles: { - backgroundColor: '#f2f2f2', - borderColor: '#999999' + backgroundColor: 'var(--color-graphs-style-selector-range-handle-background)', + borderColor: 'var(--color-graphs-style-selector-range-handle-border)' }, - outlineColor: '#cccccc', - maskFill: 'rgba(102,133,194,0.3)', - series: {lineColor: "#3FA0FF"}, - xAxis: {gridLineColor: '#e6e6e6'} + outlineColor: 'var(--color-graphs-style-selector-range-outline)', + maskFill: 'color-mix(in srgb, var(--color-graphs-style-selector-range-selected-area), transparent 85%)', + series: {lineColor: "var(--color-graphs-style-selector-range-series-line)"}, + xAxis: {gridLineColor: 'var(--color-graphs-style-grid-line)'} }, scrollbar: { - barBackgroundColor: '#cccccc', - barBorderColor: '#cccccc', - buttonArrowColor: '#333333', - buttonBackgroundColor: '#e6e6e6', - buttonBorderColor: '#cccccc', - rifleColor: '#333333', - trackBackgroundColor: '#f2f2f2', - trackBorderColor: '#f2f2f2' + barBackgroundColor: 'var(--color-graphs-style-scrollbar-bar-background)', + barBorderColor: 'var(--color-graphs-style-scrollbar-bar-background)', + buttonArrowColor: 'var(--color-text)', + buttonBackgroundColor: 'var(--color-graphs-style-scrollbar-button-background)', + buttonBorderColor: 'var(--color-graphs-style-scrollbar-button-background)', + rifleColor: 'var(--color-graphs-style-scrollbar-decoration)', + trackBackgroundColor: 'var(--color-graphs-style-scrollbar-track-background)', + trackBorderColor: 'var(--color-graphs-style-scrollbar-track-background)', } + // mapNavigation: { 4114 TODO look at some point, only color and fill works + // buttonOptions: { + // style: { + // color: 'var(--color-text)' + // }, + // theme: { + // fill: 'var(--color-graphs-style-selector-button-background)' + // } + // } + // } }; } -export const getNightModeChartTheming = () => { - return { - chart: { - backgroundColor: null, - plotBorderColor: '#606063' - }, - title: { - style: {color: '#eee8d5'} - }, - subtitle: { - style: {color: '#eee8d5'} - }, - xAxis: { - gridLineColor: '#707073', - labels: { - style: {color: '#eee8d5'} - }, - lineColor: '#707073', - minorGridLineColor: '#505053', - tickColor: '#707073', - title: { - style: {color: '#eee8d5'} - } - }, - yAxis: { - gridLineColor: '#707073', - labels: { - style: {color: '#eee8d5'} - }, - lineColor: '#707073', - minorGridLineColor: '#505053', - tickColor: '#707073', - tickWidth: 1, - title: { - style: {color: '#eee8d5'} - } - }, - tooltip: { - backgroundColor: '#44475a', - style: {color: '#eee8d5'} - }, - plotOptions: { - series: { - dataLabels: {color: '#B0B0B3'}, - marker: {lineColor: '#333'} - } - }, - legend: { - itemStyle: {color: '#eee8d5'}, - itemHoverStyle: {color: '#eee8d5'}, - itemHiddenStyle: {color: '#606063'} - }, - labels: { - style: {color: '#eee8d5'} - }, - drilldown: { - activeAxisLabelStyle: {color: '#eee8d5'}, - activeDataLabelStyle: {color: '#eee8d5'} - }, - navigation: { - buttonOptions: { - symbolStroke: '#eee8d5', - theme: {fill: '#44475a'} - } - }, - // scroll charts - rangeSelector: { - buttonTheme: { - fill: '#505053', - stroke: '#646e8c', - style: {color: '#CCC'}, - states: { - hover: { - fill: '#646e9d', - stroke: '#646e8c', - style: {color: 'white'} - }, - select: { - fill: '#646e9d', - stroke: '#646e8c', - style: {color: 'white'} - } - } - }, - inputBoxBorderColor: '#505053', - inputStyle: { - backgroundColor: '#333', - color: 'silver' - }, - labelStyle: {color: 'silver'} - }, - - navigator: { - handles: { - backgroundColor: '#666', - borderColor: '#AAA' - }, - outlineColor: '#CCC', - maskFill: 'rgba(255,255,255,0.1)', - series: {lineColor: '#A6C7ED'}, - xAxis: {gridLineColor: '#505053'} - }, - - scrollbar: { - barBackgroundColor: '#808083', - barBorderColor: '#808083', - buttonArrowColor: '#CCC', - buttonBackgroundColor: '#606063', - buttonBorderColor: '#606063', - rifleColor: '#FFF', - trackBackgroundColor: '#404043', - trackBorderColor: '#404043' - } - }; -} \ No newline at end of file diff --git a/Plan/react/dashboard/src/util/loginSineRenderer.js b/Plan/react/dashboard/src/util/loginSineRenderer.js index bda8145e84..01de4b1dfc 100644 --- a/Plan/react/dashboard/src/util/loginSineRenderer.js +++ b/Plan/react/dashboard/src/util/loginSineRenderer.js @@ -1,5 +1,5 @@ // https://gist.github.com/gkhays/e264009c0832c73d5345847e673a64ab -export default function drawSine(canvasId) { +export default function drawSine(canvasId, fillColor) { let step; function drawPoint(ctx, x, y) { @@ -9,7 +9,7 @@ export default function drawSine(canvasId) { // Hold x constant at 4 so the point only moves up and down. ctx.arc(x - 5, y, radius, 0, 2 * Math.PI, false); - ctx.fillStyle = '#fff'; + ctx.fillStyle = fillColor || '#fff'; ctx.fill(); ctx.lineWidth = 1; ctx.stroke(); @@ -21,7 +21,7 @@ export default function drawSine(canvasId) { ctx.beginPath(); ctx.lineWidth = 2; - ctx.strokeStyle = "#fff"; + ctx.strokeStyle = fillColor || '#fff'; // Drawing point diff --git a/Plan/react/dashboard/src/util/mutator.js b/Plan/react/dashboard/src/util/mutator.js new file mode 100644 index 0000000000..0c58cee163 --- /dev/null +++ b/Plan/react/dashboard/src/util/mutator.js @@ -0,0 +1,53 @@ +// Function to flatten nested object into dot notation +import {cssVariableToName} from "./colors.js"; + +export const flattenObject = (obj, prefix = '') => { + return Object.entries(obj).reduce((acc, [key, value]) => { + const newKey = prefix ? `${prefix}-${key}` : key; + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + Object.assign(acc, flattenObject(value, newKey)); + } else if (!Array.isArray(value)) { + // Convert camelCase to kebab-case + const cssKey = newKey.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); + acc[cssKey] = value; + } + return acc; + }, {}); +}; + +// Function to merge two objects recursively, with override taking precedence +export const mergeUseCases = (base, override) => { + const merged = {...base}; + for (const key in override) { + if (typeof override[key] === 'object' && !Array.isArray(override[key])) { + merged[key] = mergeUseCases(base[key] || {}, override[key]); + } else { + merged[key] = override[key]; + } + } + return merged; +}; + +export const addToObject = (base, toAdd) => { + if (!toAdd) return base; + Object.entries(toAdd).forEach(([key, value]) => { + base[key] = value + }); + return base; +} + +export const recursiveFindAndReplaceValue = (object, toReplace, replaceWith) => { + if (Array.isArray(object)) { + const toReplaceName = cssVariableToName(toReplace); + const toReplaceWithName = cssVariableToName(replaceWith) + return object.map(item => item === toReplaceName ? toReplaceWithName : item); + } else if (object !== null && typeof object === 'object') { + const result = {}; + for (const [key, value] of Object.entries(object)) { + result[key] = recursiveFindAndReplaceValue(value, toReplace, replaceWith); + } + return result; + } else { + return toReplace === object ? replaceWith : object; + } +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/views/ErrorView.jsx b/Plan/react/dashboard/src/views/ErrorView.jsx index bf4b3fbb15..588cbbe3ba 100644 --- a/Plan/react/dashboard/src/views/ErrorView.jsx +++ b/Plan/react/dashboard/src/views/ErrorView.jsx @@ -29,7 +29,7 @@ export const ErrorViewCard = ({error}) => { -
    +
    Error information
    diff --git a/Plan/react/dashboard/src/views/layout/LoginPage.jsx b/Plan/react/dashboard/src/views/layout/LoginPage.jsx index 736aaf9a9e..d5d0d7e7b8 100644 --- a/Plan/react/dashboard/src/views/layout/LoginPage.jsx +++ b/Plan/react/dashboard/src/views/layout/LoginPage.jsx @@ -12,6 +12,7 @@ import drawSine from "../../util/loginSineRenderer"; import {fetchLogin} from "../../service/authenticationService"; import ForgotPasswordModal from "../../components/modal/ForgotPasswordModal"; import {useAuth} from "../../hooks/authenticationHook"; +import ActionButton from "../../components/input/button/ActionButton.jsx"; const Logo = () => { return ( @@ -65,9 +66,9 @@ const LoginForm = ({login}) => { id="inputPassword" placeholder={t('html.login.password')} type="password" value={password} onChange={event => setPassword(event.target.value)}/>
  • - + ); } diff --git a/Plan/react/dashboard/src/views/layout/ManagePage.jsx b/Plan/react/dashboard/src/views/layout/ManagePage.jsx index 2ffee99644..2a4a67a938 100644 --- a/Plan/react/dashboard/src/views/layout/ManagePage.jsx +++ b/Plan/react/dashboard/src/views/layout/ManagePage.jsx @@ -17,7 +17,7 @@ const HelpModal = React.lazy(() => import("../../components/modal/HelpModal")); const ManagePage = () => { const {t, i18n} = useTranslation(); - const {isProxy, networkName, serverName} = useMetadata(); + const {displayedServerName} = useMetadata(); const [error] = useState(undefined); const {sidebarItems, setSidebarItems, currentTab, setCurrentTab} = useNavigation(); @@ -41,7 +41,6 @@ const ManagePage = () => { if (authRequired && !loggedIn) return ; if (error) return ; - const displayedServerName = isProxy ? networkName : (serverName?.startsWith('Server') ? "Plan" : serverName); return ( <> diff --git a/Plan/react/dashboard/src/views/layout/PlayerPage.jsx b/Plan/react/dashboard/src/views/layout/PlayerPage.jsx index de024ce595..338d1c574a 100644 --- a/Plan/react/dashboard/src/views/layout/PlayerPage.jsx +++ b/Plan/react/dashboard/src/views/layout/PlayerPage.jsx @@ -12,6 +12,8 @@ import {useDataRequest} from "../../hooks/dataFetchHook"; import ErrorPage from "./ErrorPage"; import {useAuth} from "../../hooks/authenticationHook"; import MainPageRedirect from "../../components/navigation/MainPageRedirect"; +import {SwitchTransition} from "react-transition-group"; +import {ChartLoader} from "../../components/navigation/Loader.jsx"; const HelpModal = React.lazy(() => import("../../components/modal/HelpModal")); @@ -67,14 +69,17 @@ const PlayerPage = () => { if (authRequired && !loggedIn) return ; if (loadingError) return ; - return player ? ( + return ( <> - +
    - + {player && + + } + {!player && }
    - ) : <> -
    -
    - -

    Please wait..

    -
    -
    - + ) } export const usePlayer = () => { diff --git a/Plan/react/dashboard/src/views/layout/RegisterPage.jsx b/Plan/react/dashboard/src/views/layout/RegisterPage.jsx index 7ef20d19b8..24f2ee7f92 100644 --- a/Plan/react/dashboard/src/views/layout/RegisterPage.jsx +++ b/Plan/react/dashboard/src/views/layout/RegisterPage.jsx @@ -12,6 +12,7 @@ import {useAuth} from "../../hooks/authenticationHook"; import FinalizeRegistrationModal from "../../components/modal/FinalizeRegistrationModal"; import {fetchRegisterCheck, postRegister} from "../../service/authenticationService"; import {useMetadata} from "../../hooks/metadataHook"; +import ActionButton from "../../components/input/button/ActionButton.jsx"; const Logo = () => { return ( @@ -74,9 +75,9 @@ const RegisterForm = ({register}) => { value={password} onChange={event => setPassword(event.target.value)}/>
    {t('html.register.passwordTip')}
    - + ); } @@ -188,7 +189,7 @@ const RegisterPage = () => {
    -

    {t('html.register.createNewUser')}

    +

    {t('html.register.createNewUser')}

    {failMessage && {failMessage}} diff --git a/Plan/react/dashboard/src/views/layout/ServerPage.jsx b/Plan/react/dashboard/src/views/layout/ServerPage.jsx index a567b000c6..99e68f5c4a 100644 --- a/Plan/react/dashboard/src/views/layout/ServerPage.jsx +++ b/Plan/react/dashboard/src/views/layout/ServerPage.jsx @@ -211,25 +211,23 @@ const ServerPage = () => { } return ( - <> - - -
    -
    -
    -
    - - - -
    - -
    + + +
    +
    +
    +
    + + + +
    +
    - - +
    +
    ) } diff --git a/Plan/react/dashboard/src/views/layout/ThemeEditorPage.jsx b/Plan/react/dashboard/src/views/layout/ThemeEditorPage.jsx new file mode 100644 index 0000000000..984256d39a --- /dev/null +++ b/Plan/react/dashboard/src/views/layout/ThemeEditorPage.jsx @@ -0,0 +1,83 @@ +import React from 'react'; +import Sidebar from "../../components/navigation/Sidebar"; +import Header from "../../components/navigation/Header"; +import {useTranslation} from "react-i18next"; +import ColorSelectorModal from "../../components/modal/ColorSelectorModal"; +import {ThemeStyleCss} from "../../components/theme/ThemeStyleCss"; +import {ThemeEditContextProvider} from "../../hooks/context/themeEditContextHook.jsx"; +import {SwitchTransition} from "react-transition-group"; +import {Outlet, useParams} from "react-router-dom"; +import {ThemeContextProvider, useTheme} from "../../hooks/themeHook.jsx"; +import {ThemeStorageContextProvider, useThemeStorage} from "../../hooks/context/themeContextHook.jsx"; +import {ChartLoader} from "../../components/navigation/Loader.jsx"; +import {useMetadata} from "../../hooks/metadataHook.jsx"; +import {faInfoCircle, faPlus, faSwatchbook, faTrash} from "@fortawesome/free-solid-svg-icons"; +import ErrorView from "../ErrorView.jsx"; +import AlertPopupArea from "../../components/alert/AlertPopupArea.jsx"; +import ErrorPage from "./ErrorPage.jsx"; +import {Alert} from "react-bootstrap"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {useAuth} from "../../hooks/authenticationHook.jsx"; + +const ThemeEditorPage = () => { + const {t} = useTranslation(); + const metadata = useMetadata(); + const title = t("html.label.themeEditor.title"); + const {identifier} = useParams(); + const {nightModeEnabled} = useTheme(); + const {authRequired, loggedIn} = useAuth(); + + if (authRequired && !loggedIn) return ; + if (metadata.metadataError) { + return + } + + const items = metadata.loaded ? metadata.getAvailableThemes().map(theme => { + return {name: theme, icon: faSwatchbook, href: theme} + }) : []; + items.push({name: t('html.label.themeEditor.addTheme'), icon: faPlus, href: 'new'}); + items.push({name: t('html.label.themeEditor.deleteThemes'), icon: faTrash, href: 'delete'}); + return ( + <> + +
    + +
    +
    +
    + {nightModeEnabled && + {t('html.label.themeEditor.lightModeInfo')} + } + + + + + +
    + +
    +
    + + ); +}; + +const WaitUntilThemeLoads = () => { + const theme = useThemeStorage(); + if (theme.error) return + if (!theme.loaded) return + + return ( + + +
    + + + +
    +
    + ) +} + +export default ThemeEditorPage; \ No newline at end of file diff --git a/Plan/react/dashboard/src/views/manage/GroupsView.jsx b/Plan/react/dashboard/src/views/manage/GroupsView.jsx index a4f2fdd68c..dbfb7a3eb1 100644 --- a/Plan/react/dashboard/src/views/manage/GroupsView.jsx +++ b/Plan/react/dashboard/src/views/manage/GroupsView.jsx @@ -31,6 +31,13 @@ import {DropdownStatusContextProvider, useDropdownStatusContext} from "../../hoo import {useNavigation} from "../../hooks/navigationHook"; import {faQuestionCircle} from "@fortawesome/free-regular-svg-icons"; import {useAuth} from "../../hooks/authenticationHook"; +import Checkbox from "../../components/input/Checkbox.jsx"; +import TextInput from "../../components/input/TextInput.jsx"; +import UnsavedChangesText from "../../components/text/UnsavedChangesText.jsx"; +import ActionButton from "../../components/input/button/ActionButton.jsx"; +import DangerButton from "../../components/input/button/DangerButton.jsx"; +import OutlineButton from "../../components/input/button/OutlineButton.jsx"; +import SecondaryActionButton from "../../components/input/button/SecondaryActionButton.jsx"; const GroupsHeader = ({groupName, icon}) => { return ( @@ -56,14 +63,11 @@ const PermissionDropdown = ({permission, checked, indeterminate, togglePermissio event.preventDefault(); toggle(permission); }}> - { - if (input) input.indeterminate = indeterminate - }} - onChange={() => togglePermission(permission)} - /> {permission} {permission && translated !== translationKey && - · {translated}} + togglePermission(permission)} + >{permission} {permission && translated !== translationKey && + · {translated}} +
    @@ -74,9 +78,7 @@ const PermissionDropdown = ({permission, checked, indeterminate, togglePermissio } else { return (
  • - togglePermission(permission)} + togglePermission(permission)} /> {permission} {permission && translated !== translationKey && · {translated}}
  • @@ -144,11 +146,11 @@ const SaveButton = () => { const {dirty, requestSave} = useConfigurationStorageContext(); return ( - + ) } @@ -178,57 +180,57 @@ const DeleteGroupButton = ({groupName, groups, reloadGroupNames}) => { values={{groupName, moveTo: groupOptions[moveToGroup]}}/>

    - - + ) } return ( - + ) } @@ -238,62 +240,33 @@ const DiscardButton = () => { return ( <> - {dirty && } + } ) } -const UnsavedChangesText = ({visible}) => { - const {t} = useTranslation(); - const {dirty} = useConfigurationStorageContext(); - const show = visible !== undefined ? visible : dirty; - if (show) { - return ( -

    {t('html.label.managePage.changes.unsaved')}

    - ) - } else { - return <> - } -} - const AddGroupBody = ({groups, reloadGroupNames}) => { const {t} = useTranslation(); - const [invalid, setInvalid] = useState(false); const [value, setValue] = useState(undefined); const {addAlert} = useAlertPopupContext(); - const onChange = (event) => { - const newValue = event.target.value; - setValue(newValue.toLowerCase().replace(" ", "_")); - setInvalid(newValue.length > 100); - } - + const isInvalid = newValue => newValue && (newValue.length > 100 || groups.find(group => group.name === newValue)); + const invalid = isInvalid(value); return ( - -
    - -
    - - {invalid &&
    - {t('html.label.managePage.addGroup.invalidName')} -
    } -
    - +
    ) @@ -355,7 +328,7 @@ const GroupsCard = ({groups, reloadGroupNames}) => { return ( - diff --git a/Plan/react/dashboard/src/views/network/NetworkOverview.jsx b/Plan/react/dashboard/src/views/network/NetworkOverview.jsx index 16c76e946a..7de4b30a67 100644 --- a/Plan/react/dashboard/src/views/network/NetworkOverview.jsx +++ b/Plan/react/dashboard/src/views/network/NetworkOverview.jsx @@ -24,25 +24,25 @@ const RecentPlayersCard = ({data}) => { return ( -
    +
    {t('html.label.players')}

    {t('html.label.last24hours')}

    - -

    {t('html.label.last7days')}

    - -

    {t('html.label.last30days')}

    - -
    diff --git a/Plan/react/dashboard/src/views/network/NetworkPerformance.jsx b/Plan/react/dashboard/src/views/network/NetworkPerformance.jsx index ada54bfe08..f09d73efe4 100644 --- a/Plan/react/dashboard/src/views/network/NetworkPerformance.jsx +++ b/Plan/react/dashboard/src/views/network/NetworkPerformance.jsx @@ -14,6 +14,7 @@ import {mapPerformanceDataToSeries} from "../../util/graphs"; import PerformanceGraphsCard from "../../components/cards/network/PerformanceGraphsCard"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; import {useAuth} from "../../hooks/authenticationHook"; +import ActionButton from "../../components/input/button/ActionButton.jsx"; const NetworkPerformance = () => { const {hasPermission} = useAuth(); @@ -106,14 +107,13 @@ const NetworkPerformance = () => { - + server.serverName)} selectedIndexes={selectedOptions} setSelectedIndexes={setSelectedOptions}/> - + diff --git a/Plan/react/dashboard/src/views/player/PlayerOverview.jsx b/Plan/react/dashboard/src/views/player/PlayerOverview.jsx index b6f52275e3..de8108aa1b 100644 --- a/Plan/react/dashboard/src/views/player/PlayerOverview.jsx +++ b/Plan/react/dashboard/src/views/player/PlayerOverview.jsx @@ -14,13 +14,14 @@ import LoadIn from "../../components/animation/LoadIn"; import ExtendableRow from "../../components/layout/extension/ExtendableRow"; import ConnectionsCard from "../../components/cards/player/ConnectionsCard"; import {useAuth} from "../../hooks/authenticationHook"; +import FormattedTime from "../../components/text/FormattedTime.jsx"; const PunchCardCard = ({player}) => { const {t} = useTranslation(); return ( -
    +
    {t('html.label.punchcard')}
    @@ -34,26 +35,34 @@ const OnlineActivityCard = ({player}) => { return ( -
    +
    {t('html.label.onlineActivity')}
    - - - - - , + ]}/> + , + ]}/> + , + ]}/> + , + ]}/> + - - -
    diff --git a/Plan/react/dashboard/src/views/player/PlayerPvpPve.jsx b/Plan/react/dashboard/src/views/player/PlayerPvpPve.jsx index 2310e6e973..35d2dc4eee 100644 --- a/Plan/react/dashboard/src/views/player/PlayerPvpPve.jsx +++ b/Plan/react/dashboard/src/views/player/PlayerPvpPve.jsx @@ -18,16 +18,16 @@ const InsightsCard = ({player}) => { return ( -
    - {t('html.label.insights')} +
    + {t('html.label.insights')}
    - - - @@ -39,11 +39,11 @@ const PvpDeathsTableCard = ({player}) => { return ( -
    - {t('html.label.recentPvpDeaths')} +
    + {t('html.label.recentPvpDeaths')}
    - + ) } diff --git a/Plan/react/dashboard/src/views/player/PlayerServers.jsx b/Plan/react/dashboard/src/views/player/PlayerServers.jsx index 444839a33b..5eb26839a5 100644 --- a/Plan/react/dashboard/src/views/player/PlayerServers.jsx +++ b/Plan/react/dashboard/src/views/player/PlayerServers.jsx @@ -21,8 +21,8 @@ const PingGraphCard = ({player}) => { return ( -
    - {t('html.label.ping')} +
    + {t('html.label.ping')}
    {hasPingData && } @@ -36,8 +36,8 @@ const ServersCard = ({player}) => { return ( -
    - {t('html.label.servers')} +
    + {t('html.label.servers')} {t('html.text.clickToExpand')} @@ -55,8 +55,8 @@ const ServerPieCard = ({player}) => { return ( -
    - {t('html.label.serverPlaytime')} +
    + {t('html.label.serverPlaytime')}
    { return ( -
    - {t('html.label.sessionCalendar')} +
    + {t('html.label.sessionCalendar')}
    diff --git a/Plan/react/dashboard/src/views/query/QueryResultView.jsx b/Plan/react/dashboard/src/views/query/QueryResultView.jsx index 573c3107bf..744d1a0e3f 100644 --- a/Plan/react/dashboard/src/views/query/QueryResultView.jsx +++ b/Plan/react/dashboard/src/views/query/QueryResultView.jsx @@ -21,7 +21,7 @@ const serverCount = (count, t) => { } else if (count === 2) { return t('html.query.label.servers.two'); } else { - return t('html.query.label.servers.many').replace('{number}', count); + return t('html.query.label.servers.many', {number: count}); } } @@ -89,8 +89,11 @@ const QueryResultView = () => { ', result.view.beforeDate)}/> + title={t('html.query.title.activityOnDate', + { + activityDate: result.view.beforeDate, + interpolation: {escapeValue: false} + })}/> diff --git a/Plan/react/dashboard/src/views/server/ServerOverview.jsx b/Plan/react/dashboard/src/views/server/ServerOverview.jsx index dbf7e0dfa0..3aa62e68e7 100644 --- a/Plan/react/dashboard/src/views/server/ServerOverview.jsx +++ b/Plan/react/dashboard/src/views/server/ServerOverview.jsx @@ -35,7 +35,7 @@ const Last7DaysCard = ({data}) => { return ( -
    +
    {t('html.label.last7days')}
    @@ -44,27 +44,27 @@ const Last7DaysCard = ({data}) => { }
    }/>
    diff --git a/Plan/react/dashboard/src/views/theme/AddThemeView.jsx b/Plan/react/dashboard/src/views/theme/AddThemeView.jsx new file mode 100644 index 0000000000..bca8bffd1e --- /dev/null +++ b/Plan/react/dashboard/src/views/theme/AddThemeView.jsx @@ -0,0 +1,107 @@ +import React, {useState} from 'react'; +import {useMetadata} from "../../hooks/metadataHook.jsx"; +import {faFileSignature, faInfoCircle, faPlusCircle} from "@fortawesome/free-solid-svg-icons"; +import CardHeader from "../../components/cards/CardHeader.jsx"; +import {Card, Col, Row} from "react-bootstrap"; +import TextInput from "../../components/input/TextInput.jsx"; +import ThemeOption from "../../components/theme/ThemeOption.jsx"; +import {ChartLoader} from "../../components/navigation/Loader.jsx"; +import {useTheme} from "../../hooks/themeHook.jsx"; +import ActionButton from "../../components/input/button/ActionButton.jsx"; +import {useTranslation} from "react-i18next"; +import {useThemeStorage} from "../../hooks/context/themeContextHook.jsx"; +import {useNavigate} from "react-router-dom"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import LoadIn from "../../components/animation/LoadIn.jsx"; +import ErrorView from "../ErrorView.jsx"; +import {useAuth} from "../../hooks/authenticationHook.jsx"; + +const AddThemeView = () => { + const {hasPermission} = useAuth(); + const {t} = useTranslation(); + const navigate = useNavigate() + const theme = useTheme(); + const metadata = useMetadata(); + const themeStorage = useThemeStorage(); + const [name, setName] = useState(''); + const [basedOnTheme, setBasedOnTheme] = useState('default'); + + if (metadata.metadataError) { + return + } + if (!metadata.loaded) { + return + } + + const createTheme = async () => { + if (await themeStorage.cloneThemeLocally(basedOnTheme, name)) { + metadata.refreshThemeList(); + navigate("/theme-editor/" + name); + } + } + + const onUploadFinished = (event) => { + const theme = JSON.parse(event.target.result); + themeStorage.saveUploadedThemeLocally(name, theme); + navigate("/theme-editor/" + name); + } + const onUpload = (event) => { + const reader = new FileReader(); + reader.onload = onUploadFinished; + reader.readAsText(event.target.files[0]); + } + + const isNameInvalid = newValue => { + return !newValue.length + || newValue.length > 100 + || metadata.getAvailableThemes()?.includes(newValue) + || name === 'new' || name === 'delete' + } + const nameIsInvalid = isNameInvalid(name); + return ( + + {hasPermission('access.theme.editor') && + + + + +
    {t('html.label.themeEditor.basedOnTheme')}
    + + {metadata.getAvailableThemes().map(themeName => )} + + +
    + + + setName(newValue.toLowerCase().replace(/[^a-z0-9-]/g, "-"))} + /> + + + + + {nameIsInvalid &&
    + {t('html.label.themeEditor.nameWarning')} +
    } + {t('html.label.themeEditor.openEditor')} +

    {t('html.label.themeEditor.uploadTheme')}

    + + +
    +
    +
    } +
    + ) +}; + +export default AddThemeView \ No newline at end of file diff --git a/Plan/react/dashboard/src/views/theme/DeleteThemesView.jsx b/Plan/react/dashboard/src/views/theme/DeleteThemesView.jsx new file mode 100644 index 0000000000..179cd310ee --- /dev/null +++ b/Plan/react/dashboard/src/views/theme/DeleteThemesView.jsx @@ -0,0 +1,110 @@ +import React, {useState} from 'react'; +import {useMetadata} from "../../hooks/metadataHook.jsx"; +import {faExclamationTriangle, faInfoCircle, faTrash} from "@fortawesome/free-solid-svg-icons"; +import CardHeader from "../../components/cards/CardHeader.jsx"; +import {Card, Col, Row} from "react-bootstrap"; +import ThemeOption from "../../components/theme/ThemeOption.jsx"; +import {ChartLoader} from "../../components/navigation/Loader.jsx"; +import {getLocallyStoredThemes, ThemeContextProvider, useTheme} from "../../hooks/themeHook.jsx"; +import {useTranslation} from "react-i18next"; +import {ThemeStorageContextProvider, useThemeStorage} from "../../hooks/context/themeContextHook.jsx"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import LoadIn from "../../components/animation/LoadIn.jsx"; +import Checkbox from "../../components/input/Checkbox.jsx"; +import DangerButton from "../../components/input/button/DangerButton.jsx"; +import {ThemeEditContextProvider} from "../../hooks/context/themeEditContextHook.jsx"; +import DownloadButton from "../../components/theme/DownloadButton.jsx"; +import {useAuth} from "../../hooks/authenticationHook.jsx"; +import {deleteTheme} from "../../service/metadataService.js"; + +const DeleteThemesView = () => { + const {t} = useTranslation(); + const theme = useTheme(); + const metadata = useMetadata(); + const themeStorage = useThemeStorage(); + const [themeToDelete, setThemeToDelete] = useState('default'); + const [confirm, setConfirm] = useState(false); + const {authRequired, hasPermission} = useAuth(); + + if (!metadata.loaded) { + return + } + + const onlyLocal = getLocallyStoredThemes().includes(themeToDelete); + const canDelete = onlyLocal || authRequired && hasPermission('manage.themes'); + + const onDelete = async () => { + if (onlyLocal) { + await themeStorage.deleteThemeLocally(themeToDelete); + } else if (canDelete) { + await deleteTheme(themeToDelete); + } + setConfirm(false); + metadata.refreshThemeList(); + setThemeToDelete('default'); + } + + return ( + + {hasPermission('access.theme.editor') && + + + + +
    {t('html.label.themeEditor.themeToDelete')}
    + {!onlyLocal && {t('html.label.themeEditor.canNotDeleteBuiltIn')}} + + {metadata.getAvailableThemes().map(themeName => { + setThemeToDelete(value); + setConfirm(false); + }}/>)} + + +
    + + +

    {t('html.label.themeEditor.downloadThemeBeforeDeleting', {theme: themeToDelete})}

    + + + + + + + + +
    +
    + {canDelete && + + setConfirm(event.target.checked)}> + {t('html.label.themeEditor.confirmDelete', {theme: themeToDelete})} + + + } + {canDelete && + + + {t(onlyLocal ? 'html.label.themeEditor.deleteLocalTheme' : 'html.label.themeEditor.deleteTheme')} + + + } + {!canDelete && + +

    {t('html.label.themeEditor.noPermissionToDelete')}

    + +
    } +
    +
    } +
    + ) +}; + +export default DeleteThemesView \ No newline at end of file diff --git a/Plan/react/dashboard/src/views/theme/ThemeEditorView.jsx b/Plan/react/dashboard/src/views/theme/ThemeEditorView.jsx new file mode 100644 index 0000000000..e13159f551 --- /dev/null +++ b/Plan/react/dashboard/src/views/theme/ThemeEditorView.jsx @@ -0,0 +1,157 @@ +import React, {useState} from 'react'; +import {useTranslation} from "react-i18next"; +import {useThemeEditContext} from "../../hooks/context/themeEditContextHook.jsx"; +import {Card, Col, Row} from "react-bootstrap"; +import EditorMenuToast from "../../components/theme/EditorMenuToast.jsx"; +import CardHeader from "../../components/cards/CardHeader.jsx"; +import TextInput from "../../components/input/TextInput.jsx"; +import {ColorEditContextProvider} from "../../hooks/context/colorEditContextHook.jsx"; +import ColorSection from "../../components/theme/ColorSection.jsx"; +import ColorEditForm from "../../components/theme/ColorEditForm.jsx"; +import UseCaseSection from "../../components/theme/UseCaseSection.jsx"; +import ExampleSection from "../../components/theme/ExampleSection.jsx"; +import {faExclamationCircle, faFileSignature, faSwatchbook} from "@fortawesome/free-solid-svg-icons"; +import ActionButton from "../../components/input/button/ActionButton.jsx"; +import UnsavedChangesText from "../../components/text/UnsavedChangesText.jsx"; +import SecondaryActionButton from "../../components/input/button/SecondaryActionButton.jsx"; +import {MinHeightProvider} from "../../hooks/context/minHeightContextHook.jsx"; +import {useMetadata} from "../../hooks/metadataHook.jsx"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import LoadIn from "../../components/animation/LoadIn.jsx"; +import DownloadButton from "../../components/theme/DownloadButton.jsx"; +import {useAuth} from "../../hooks/authenticationHook.jsx"; + +const ThemeEditorView = () => { + const {hasPermission} = useAuth(); + const {t} = useTranslation(); + const metadata = useMetadata(); + const { + name, originalName, currentColors, currentNightColors, currentUseCases, currentNightModeUseCases, + setName, + deleteColor, + deleteNightColor, + saveColor, + saveNightColor, + updateUseCase, + updateNightUseCase, + removeNightOverride, + discardChanges, editCount, discardPossible, savePossible, onlyLocal, save + } = useThemeEditContext(); + const [hoveredItem, setHoveredItem] = useState(undefined); + const [nightHover, setNightHover] = useState(false); + const onHoverChange = (id, state, night) => { + if (state === 'enter') { + setHoveredItem(id); + setNightHover(night); + } + } + + const referenceColors = currentUseCases.referenceColors; + const nightReferenceColors = currentNightModeUseCases.referenceColors; + + const title = t("html.label.themeEditor.title"); + const colors = {...referenceColors, '-': "", ...currentColors}; + const nightColors = {...nightReferenceColors, '-': "", ...currentNightColors, ...colors}; + + const isNameInvalid = newValue => { + return !newValue.length + || newValue.length > 100 + || metadata.getAvailableThemes()?.filter(n => originalName !== n)?.includes(newValue) + || name === 'new' || name === 'delete' + } + const invalidName = isNameInvalid(name); + + return ( + + {hasPermission('access.theme.editor') && + + + + + {t('html.label.managePage.changes.discard')} + + {t('html.label.managePage.changes.save')} + {onlyLocal && } + 0} className={"float-end me-3"}/> + {onlyLocal && {t('html.label.themeEditor.themeStoredOnlyLocally')}} + + + onHoverChange(undefined, 'enter', false)} className={'mb-4'}> + +
    {t('html.label.themeEditor.themeName')}
    + setName(newValue)} + /> + +
    + + + + + onHoverChange(undefined, 'enter', false)}/> + + + + onHoverChange(undefined, 'enter', false)}/> + + + +
    + + + + + + + + + + + + +
    + +
    +
    } +
    + ) +}; + +export default ThemeEditorView \ No newline at end of file diff --git a/Plan/react/dashboard/yarn.lock b/Plan/react/dashboard/yarn.lock index 191471f5d5..2111ffcc44 100644 --- a/Plan/react/dashboard/yarn.lock +++ b/Plan/react/dashboard/yarn.lock @@ -15,6 +15,15 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== + dependencies: + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" + "@babel/code-frame@^7.25.9": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" @@ -81,6 +90,17 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^3.0.2" +"@babel/generator@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.0.tgz#9cc2f7bd6eb054d77dc66c2664148a0c5118acd2" + integrity sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg== + dependencies: + "@babel/parser" "^7.28.0" + "@babel/types" "^7.28.0" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4" @@ -112,6 +132,11 @@ "@babel/traverse" "^7.25.9" semver "^6.3.1" +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + "@babel/helper-member-expression-to-functions@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3" @@ -120,6 +145,14 @@ "@babel/traverse" "^7.25.9" "@babel/types" "^7.25.9" +"@babel/helper-module-imports@^7.16.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" + integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + "@babel/helper-module-imports@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" @@ -191,6 +224,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== + "@babel/helper-validator-option@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" @@ -218,6 +256,13 @@ dependencies: "@babel/types" "^7.27.3" +"@babel/parser@^7.27.2", "@babel/parser@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.0.tgz#979829fbab51a29e13901e5a80713dbcb840825e" + integrity sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g== + dependencies: + "@babel/types" "^7.28.0" + "@babel/plugin-proposal-private-property-in-object@^7.21.11": version "7.21.11" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz#69d597086b6760c4126525cfa154f34631ff272c" @@ -256,6 +301,11 @@ dependencies: core-js-pure "^3.30.2" +"@babel/runtime@^7.12.0", "@babel/runtime@^7.18.3": + version "7.27.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.6.tgz#ec4070a04d76bae8ddbb10770ba55714a417b7c6" + integrity sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q== + "@babel/runtime@^7.12.5", "@babel/runtime@^7.22.15", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.27.6", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7": version "7.27.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.6.tgz#ec4070a04d76bae8ddbb10770ba55714a417b7c6" @@ -279,6 +329,15 @@ "@babel/parser" "^7.27.2" "@babel/types" "^7.27.1" +"@babel/template@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" + integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.2" + "@babel/types" "^7.27.1" + "@babel/traverse@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84" @@ -305,6 +364,19 @@ debug "^4.3.1" globals "^11.1.0" +"@babel/traverse@^7.27.1": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.0.tgz#518aa113359b062042379e333db18380b537e34b" + integrity sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.0" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.0" + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.0" + debug "^4.3.1" + "@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" @@ -321,6 +393,102 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.27.1" +"@babel/types@^7.27.1", "@babel/types@^7.28.0": + version "7.28.1" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.1.tgz#2aaf3c10b31ba03a77ac84f52b3912a0edef4cf9" + integrity sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + +"@emotion/babel-plugin@^11.13.5": + version "11.13.5" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz#eab8d65dbded74e0ecfd28dc218e75607c4e7bc0" + integrity sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/serialize" "^1.3.3" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" + +"@emotion/cache@^11.14.0", "@emotion/cache@^11.4.0": + version "11.14.0" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.14.0.tgz#ee44b26986eeb93c8be82bb92f1f7a9b21b2ed76" + integrity sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA== + dependencies: + "@emotion/memoize" "^0.9.0" + "@emotion/sheet" "^1.4.0" + "@emotion/utils" "^1.4.2" + "@emotion/weak-memoize" "^0.4.0" + stylis "4.2.0" + +"@emotion/hash@^0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" + integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== + +"@emotion/memoize@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.9.0.tgz#745969d649977776b43fc7648c556aaa462b4102" + integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== + +"@emotion/react@^11.8.1": + version "11.14.0" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.14.0.tgz#cfaae35ebc67dd9ef4ea2e9acc6cd29e157dd05d" + integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.13.5" + "@emotion/cache" "^11.14.0" + "@emotion/serialize" "^1.3.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.2.0" + "@emotion/utils" "^1.4.2" + "@emotion/weak-memoize" "^0.4.0" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.3.3.tgz#d291531005f17d704d0463a032fe679f376509e8" + integrity sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA== + dependencies: + "@emotion/hash" "^0.9.2" + "@emotion/memoize" "^0.9.0" + "@emotion/unitless" "^0.10.0" + "@emotion/utils" "^1.4.2" + csstype "^3.0.2" + +"@emotion/sheet@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c" + integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== + +"@emotion/unitless@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" + integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== + +"@emotion/use-insertion-effect-with-fallbacks@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz#8a8cb77b590e09affb960f4ff1e9a89e532738bf" + integrity sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg== + +"@emotion/utils@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.4.2.tgz#6df6c45881fcb1c412d6688a311a98b7f59c1b52" + integrity sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA== + +"@emotion/weak-memoize@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" + integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== + "@esbuild/aix-ppc64@0.25.1": version "0.25.1" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz#c33cf6bbee34975626b01b80451cbb72b4c6c91d" @@ -446,6 +614,26 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz#7d79922cb2d88f9048f06393dbf62d2e4accb584" integrity sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg== +"@floating-ui/core@^1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.2.tgz#3d1c35263950b314b6d5a72c8bfb9e3c1551aefd" + integrity sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw== + dependencies: + "@floating-ui/utils" "^0.2.10" + +"@floating-ui/dom@^1.0.1": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.2.tgz#3540b051cf5ce0d4f4db5fb2507a76e8ea5b4a45" + integrity sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA== + dependencies: + "@floating-ui/core" "^1.7.2" + "@floating-ui/utils" "^0.2.10" + +"@floating-ui/utils@^0.2.10": + version "0.2.10" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c" + integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ== + "@fortawesome/fontawesome-common-types@6.7.2": version "6.7.2" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz#7123d74b0c1e726794aed1184795dbce12186470" @@ -523,6 +711,14 @@ resolved "https://registry.yarnpkg.com/@highcharts/map-collection/-/map-collection-2.3.1.tgz#2c9d96560b3921cd935ad1c947de55c2b0de2dc9" integrity sha512-49+nZrHNxIBpCxhSkaKw3zBEuEoxkI4cXDbl+IvEG9UNAR7G6+S8xtXh4V86zpxZP16SLes31JM7tasx5lScGA== +"@jridgewell/gen-mapping@^0.3.12": + version "0.3.12" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz#2234ce26c62889f03db3d7fea43c1932ab3e927b" + integrity sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/gen-mapping@^0.3.5": version "0.3.5" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" @@ -547,6 +743,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== +"@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz#7358043433b2e5da569aa02cbc4c121da3af27d7" + integrity sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw== + "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" @@ -555,6 +756,14 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jridgewell/trace-mapping@^0.3.28": + version "0.3.29" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz#a58d31eaadaf92c6695680b2e1d464a9b8fbf7fc" + integrity sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@parcel/watcher-android-arm64@2.5.0": version "2.5.0" resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz#e32d3dda6647791ee930556aee206fcd5ea0fb7a" @@ -1320,6 +1529,11 @@ dependencies: "@types/unist" "^2" +"@types/parse-json@^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== + "@types/prop-types@*", "@types/prop-types@^15.7.12": version "15.7.14" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2" @@ -1332,6 +1546,11 @@ dependencies: types-ramda "^0.30.1" +"@types/react-transition-group@^4.4.0": + version "4.4.12" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" + integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== + "@types/react-transition-group@^4.4.6": version "4.4.11" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.11.tgz#d963253a611d757de01ebb241143b1017d5d63d5" @@ -1367,6 +1586,98 @@ resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.3.tgz#d1884c8cc4a426d1ac117ca2611bf333834c6798" integrity sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q== +"@uiw/color-convert@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@uiw/color-convert/-/color-convert-2.7.1.tgz#2f491bb545a107410e3716bfcdec11fbbc7ac379" + integrity sha512-wLA8Uit9/IxK8WXCLjZVwTIB66EytK2SKDzGP4NWTfWAXJlQFV7ne1FfTlhKG2OU4zRdMkj2h4N9BG6I80vNUw== + +"@uiw/react-color-alpha@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@uiw/react-color-alpha/-/react-color-alpha-2.7.1.tgz#e135e6b28c87aa0b12b866f2f396cbdcc0006cdd" + integrity sha512-3gGKGOLrYX3FhFGn6lD/luov1ur4+fM1psa4+azuSb5oQRUgBogbcEDK/0A+AoNXPFayAWqS8ZoqKEZKDX+geQ== + dependencies: + "@uiw/color-convert" "2.7.1" + "@uiw/react-drag-event-interactive" "2.7.1" + +"@uiw/react-color-chrome@^2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@uiw/react-color-chrome/-/react-color-chrome-2.7.1.tgz#9d52852e9604449a537c08660976a0fac740bceb" + integrity sha512-DKqsyXvlf0Yp24uwngWIsDIr1ifK5tHwBewZ9zfAaqg8YM4bcjLv6uJhZp+sRPIPjhyANdaGKeUXL0ZclCNEkw== + dependencies: + "@uiw/color-convert" "2.7.1" + "@uiw/react-color-alpha" "2.7.1" + "@uiw/react-color-editable-input" "2.7.1" + "@uiw/react-color-editable-input-hsla" "2.7.1" + "@uiw/react-color-editable-input-rgba" "2.7.1" + "@uiw/react-color-github" "2.7.1" + "@uiw/react-color-hue" "2.7.1" + "@uiw/react-color-saturation" "2.7.1" + +"@uiw/react-color-editable-input-hsla@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@uiw/react-color-editable-input-hsla/-/react-color-editable-input-hsla-2.7.1.tgz#2e72f75c8064c8b312422aec415b0f63629118f7" + integrity sha512-75YHFSsfuI8BygDHFVgKttos6uVWsVDPZYcrH+DzMt/QU2+FfNiHJjoQHJjJvvMF172uI0ir0Q9MNQIUHWjmMg== + dependencies: + "@uiw/color-convert" "2.7.1" + "@uiw/react-color-editable-input-rgba" "2.7.1" + +"@uiw/react-color-editable-input-rgba@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@uiw/react-color-editable-input-rgba/-/react-color-editable-input-rgba-2.7.1.tgz#587fd1648b287015e6a1e6bf1193d6eb063dc5fc" + integrity sha512-Qou4mVGrT+ndd4b5H1+sqoxTG3raiiGtI4MdmQKCQ/8RwO7XnOV6tWn9aJIuOfQ4woZZeoswiZWNSw6u0exUmA== + dependencies: + "@uiw/color-convert" "2.7.1" + "@uiw/react-color-editable-input" "2.7.1" + +"@uiw/react-color-editable-input@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@uiw/react-color-editable-input/-/react-color-editable-input-2.7.1.tgz#535208e8b04883484988ea2bd728448f7708f6ea" + integrity sha512-kaPf7dkCzyTadQ1ocF+5DSyh4PuSgrBRnAdnw/u+HFLZtVMfz23uHnNAgXkVZSmfaBahTCAJnUd7hq1ZzJOkgg== + +"@uiw/react-color-github@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@uiw/react-color-github/-/react-color-github-2.7.1.tgz#2635cd836a1427e2232951bcb4263e4fd16900b2" + integrity sha512-RKhppBwvFA0K6w/sFUWbjJN2NoEEggSKepV8j129xngK33NIqjvoAyZswdKaXohB1nLIXueTv/7kn4HLED6I5A== + dependencies: + "@uiw/color-convert" "2.7.1" + "@uiw/react-color-swatch" "2.7.1" + +"@uiw/react-color-hue@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@uiw/react-color-hue/-/react-color-hue-2.7.1.tgz#716640000851c289c2ab017411aa46c913b482d7" + integrity sha512-3ZH2cOjatVm67RPklFXKqxcXDuoKd3j6H9o4cIhIxL4Dc5EET9wB9WcrNcz7QSO1V084Q8hIwFbay6zCAAhBhA== + dependencies: + "@uiw/color-convert" "2.7.1" + "@uiw/react-color-alpha" "2.7.1" + +"@uiw/react-color-saturation@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@uiw/react-color-saturation/-/react-color-saturation-2.7.1.tgz#4d793410ae41fdd20e66b019c6a939eb989ca12f" + integrity sha512-CO3IYWpYQliEwkKEDviB26idVR5KZaW0nUSySRw5Y93N0ZHUILVR3dfYvYRP7GURoRAmXpf0Et5+WQbDTdXfMA== + dependencies: + "@uiw/color-convert" "2.7.1" + "@uiw/react-drag-event-interactive" "2.7.1" + +"@uiw/react-color-swatch@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@uiw/react-color-swatch/-/react-color-swatch-2.7.1.tgz#423aab3f1baba9bd12b2c63b1d70d2d682bdf130" + integrity sha512-1uS+S+JQw1+5RlM3yRfvuuHHldGqxTt88/IZEd3hwcMI60Q6WVS4noWZTXrma83yQoJnL5RkbLPzAcrpZ9ddIg== + dependencies: + "@uiw/color-convert" "2.7.1" + +"@uiw/react-color-wheel@^2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@uiw/react-color-wheel/-/react-color-wheel-2.7.1.tgz#1b731f2377e0ad05886f9e4a3aa1b86c689fc426" + integrity sha512-WKgEaOYYAxGPOwe4H+XaTfbJ9xIhRY9wXHo+gmOjIJmr8M1aXhzRwcRcSpFfY2S1/d9XhKz6nFbqURDeWjtEqQ== + dependencies: + "@uiw/color-convert" "2.7.1" + "@uiw/react-drag-event-interactive" "2.7.1" + +"@uiw/react-drag-event-interactive@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@uiw/react-drag-event-interactive/-/react-drag-event-interactive-2.7.1.tgz#7e989ace87a39e64846f34b4e74e9ae6afa2a648" + integrity sha512-7qT0e6aNc4enYofwseC7uYzWKemijf+DWxfCjZUOEzP2FDv/j5c7wQwnsLvfrOq5VDpR6FW4wIopPpxaz8a/Kw== + "@vitejs/plugin-react@^4.6.0": version "4.6.0" resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz#2707b485f44806d42d41c63921883cff9c54dfaa" @@ -1439,6 +1750,15 @@ axios@^1.10.0, axios@^1.9.0: form-data "^4.0.0" proxy-from-env "^1.1.0" +babel-plugin-macros@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" + integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== + dependencies: + "@babel/runtime" "^7.12.5" + cosmiconfig "^7.0.0" + resolve "^1.19.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -1491,6 +1811,11 @@ btoa@^1.2.1: resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + caniuse-lite@^1.0.30001669: version "1.0.30001684" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz#0eca437bab7d5f03452ff0ef9de8299be6b08e16" @@ -1577,7 +1902,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -convert-source-map@^1.7.0: +convert-source-map@^1.5.0, convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== @@ -1599,6 +1924,17 @@ core-js-pure@^3.30.2: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.39.0.tgz#aa0d54d70a15bdc13e7c853db87c10abc30d68f3" integrity sha512-7fEcWwKI4rJinnK+wLTezeg2smbFFdSBP6E2kQZNbnzM2s1rpKQ6aaRteZSSg7FLU3P0HGGVo/gbpfanU36urg== +cosmiconfig@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + cross-fetch@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" @@ -1700,6 +2036,13 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + esbuild@^0.25.0: version "0.25.1" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.1.tgz#a16b8d070b6ad4871935277bda6ccfe852e3fa2f" @@ -1741,6 +2084,11 @@ escape-html@^1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + ev-emitter@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/ev-emitter/-/ev-emitter-1.1.1.tgz#8f18b0ce5c76a5d18017f71c0a795c65b9138f2a" @@ -1782,6 +2130,11 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + fizzy-ui-utils@^2.0.0: version "2.0.7" resolved "https://registry.yarnpkg.com/fizzy-ui-utils/-/fizzy-ui-utils-2.0.7.tgz#7df45dcc4eb374a08b65d39bb9a4beedf7330505" @@ -1818,6 +2171,11 @@ fsevents@~2.3.2, fsevents@~2.3.3: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -1862,6 +2220,13 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hast-util-parse-selector@^2.0.0: version "2.2.5" resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a" @@ -1893,6 +2258,13 @@ highlightjs-vue@^1.0.0: resolved "https://registry.yarnpkg.com/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz#fdfe97fbea6354e70ee44e3a955875e114db086d" integrity sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA== +hoist-non-react-statics@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + html-parse-stringify@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" @@ -1943,6 +2315,14 @@ immutable@^5.0.2: resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.0.3.tgz#aa037e2313ea7b5d400cd9298fa14e404c933db1" integrity sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw== +import-fresh@^3.2.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + indent-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" @@ -1981,6 +2361,18 @@ is-alphanumerical@^1.0.0: is-alphabetical "^1.0.0" is-decimal "^1.0.0" +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + is-decimal@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" @@ -2057,11 +2449,21 @@ jsesc@^3.0.2: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + lodash.debounce@^4: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -2102,6 +2504,11 @@ masonry-layout@^4.2.2: get-size "^2.0.2" outlayer "^2.1.0" +memoize-one@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" + integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== + micromatch@^4.0.5: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" @@ -2280,6 +2687,13 @@ outlayer@^2.1.0: fizzy-ui-utils "^2.0.0" get-size "^2.0.2" +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + parse-entities@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" @@ -2292,11 +2706,31 @@ parse-entities@^2.0.0: is-decimal "^1.0.0" is-hexadecimal "^1.0.0" +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" @@ -2349,7 +2783,7 @@ prop-types-extra@^1.1.0: react-is "^16.3.2" warning "^4.0.0" -prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -2476,7 +2910,7 @@ react-inspector@^6.0.1: resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-6.0.2.tgz#aa3028803550cb6dbd7344816d5c80bf39d07e9d" integrity sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ== -react-is@^16.13.1, react-is@^16.3.2: +react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -2519,6 +2953,21 @@ react-router@6.28.0: dependencies: "@remix-run/router" "1.21.0" +react-select@^5.10.2: + version "5.10.2" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.10.2.tgz#8dffc69dfd7d74684d9613e6eb27204e3b99e127" + integrity sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ== + dependencies: + "@babel/runtime" "^7.12.0" + "@emotion/cache" "^11.4.0" + "@emotion/react" "^11.8.1" + "@floating-ui/dom" "^1.0.1" + "@types/react-transition-group" "^4.4.0" + memoize-one "^6.0.0" + prop-types "^15.6.0" + react-transition-group "^4.3.0" + use-isomorphic-layout-effect "^1.2.0" + react-syntax-highlighter@^15.6.1: version "15.6.1" resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz#fa567cb0a9f96be7bbccf2c13a3c4b5657d9543e" @@ -2531,7 +2980,7 @@ react-syntax-highlighter@^15.6.1: prismjs "^1.27.0" refractor "^3.6.0" -react-transition-group@^4.4.5: +react-transition-group@^4.3.0, react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== @@ -2608,6 +3057,20 @@ reselect@^5.1.1: resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e" integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.19.0: + version "1.22.10" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + dependencies: + is-core-module "^2.16.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + ret@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c" @@ -2720,6 +3183,11 @@ source-map-explorer@^2.5.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== +source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + source-map@^0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" @@ -2758,6 +3226,11 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== + supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -2765,6 +3238,11 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + swagger-client@^3.35.5: version "3.35.5" resolved "https://registry.yarnpkg.com/swagger-client/-/swagger-client-3.35.5.tgz#5660aa5fbe42007b30601ed57a4219e4c0bd7634" @@ -2950,6 +3428,11 @@ url-parse@^1.5.10: querystringify "^2.1.1" requires-port "^1.0.0" +use-isomorphic-layout-effect@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz#2f11a525628f56424521c748feabc2ffcc962fce" + integrity sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA== + use-sync-external-store@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz#adbc795d8eeb47029963016cefdf89dc799fcebc" @@ -3050,6 +3533,11 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"