2905/remove old web files (#3225)

* Removed Plugin.Use_Legacy_Frontend feature flag
* Remove version modal html generation in Java
* Remove contributor list html generation in Java
* Remove more html generation in Java
* Delete old web file bundle
* Removed locale html/js translation from backend
* Move Html#swapColorsToSpan to the React side
* Reimplement datatables and backend for preferences
* Load preferences from backend
* Usability fixes to the players table
* Delete WebAssetVersionCheckTask
* Add new locale to locale files
* Export defaultPreferences as json on react export

Affects issues:
- Close #2905
This commit is contained in:
Aurora Lahtela 2023-09-23 22:24:34 +03:00 committed by GitHub
parent 2fc4ee7ac1
commit 01d904c7d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
188 changed files with 6223 additions and 33768 deletions

View File

@ -16,6 +16,8 @@
*/
package com.djrapitops.plan.extension.icon;
import java.util.Objects;
/**
* Object that represents an icon on the website.
* <p>
@ -72,6 +74,19 @@ public class Icon {
return this;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Icon icon = (Icon) o;
return type == icon.type && Objects.equals(getName(), icon.getName()) && getColor() == icon.getColor();
}
@Override
public int hashCode() {
return Objects.hash(id, type, getName(), getColor());
}
@Override
public String toString() {
return "Icon{" + type.name() + ", '" + name + '\'' + ", " + color.name() + '}';

View File

@ -21,10 +21,8 @@ import com.djrapitops.plan.extension.icon.Color;
import com.djrapitops.plan.extension.icon.Icon;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;
/**
* Object for giving Plan table data.
@ -100,6 +98,52 @@ public final class Table {
return tableColumnFormats;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Table table = (Table) o;
return Arrays.equals(getColumns(), table.getColumns())
&& Arrays.equals(getIcons(), table.getIcons())
&& Arrays.equals(getTableColumnFormats(), table.getTableColumnFormats())
&& valuesEqual(table);
}
private boolean valuesEqual(Table other) {
List<Object[]> rows1 = getRows();
List<Object[]> rows2 = other.getRows();
if (rows1.size() != rows2.size()) return false;
for (int i = 0; i < rows1.size(); i++) {
Object[] values1 = rows1.get(i);
Object[] values2 = rows2.get(i);
for (int j = 0; j < getMaxColumnSize(); j++) {
if (!Objects.equals(Objects.toString(values1[0]), Objects.toString(values2[0]))) {
return false;
}
}
}
return true;
}
@Override
public int hashCode() {
int result = Objects.hash(getRows());
result = 31 * result + Arrays.hashCode(getColumns());
result = 31 * result + Arrays.hashCode(getIcons());
result = 31 * result + Arrays.hashCode(getTableColumnFormats());
return result;
}
@Override
public String toString() {
return "Table{" +
"columns=" + Arrays.toString(columns) +
", icons=" + Arrays.toString(icons) +
", tableColumnFormats=" + Arrays.toString(tableColumnFormats) +
", rows=" + rows.stream().map(Arrays::toString).collect(Collectors.toList()) +
'}';
}
/**
* Factory for creating new {@link Table} objects.
*/

View File

@ -19,7 +19,6 @@ package com.djrapitops.plan.modules.bukkit;
import com.djrapitops.plan.TaskSystem;
import com.djrapitops.plan.addons.placeholderapi.PlaceholderCacheRefreshTask;
import com.djrapitops.plan.delivery.web.ResourceWriteTask;
import com.djrapitops.plan.delivery.web.WebAssetVersionCheckTask;
import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieExpiryCleanupTask;
import com.djrapitops.plan.delivery.webserver.cache.JSONFileStorage;
import com.djrapitops.plan.delivery.webserver.configuration.AddressAllowList;
@ -94,10 +93,6 @@ public interface BukkitTaskModule {
@IntoSet
TaskSystem.Task bindResourceWriteTask(ResourceWriteTask resourceWriteTask);
@Binds
@IntoSet
TaskSystem.Task bindWebAssetVersionCheckTask(WebAssetVersionCheckTask webAssetVersionCheckTask);
@Binds
@IntoSet
TaskSystem.Task bindActiveCookieStoreExpiryTask(ActiveCookieExpiryCleanupTask activeCookieExpiryCleanupTask);

View File

@ -18,7 +18,6 @@ package com.djrapitops.plan.modules.bungee;
import com.djrapitops.plan.TaskSystem;
import com.djrapitops.plan.delivery.web.ResourceWriteTask;
import com.djrapitops.plan.delivery.web.WebAssetVersionCheckTask;
import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieExpiryCleanupTask;
import com.djrapitops.plan.delivery.webserver.cache.JSONFileStorage;
import com.djrapitops.plan.delivery.webserver.configuration.AddressAllowList;
@ -81,10 +80,6 @@ public interface BungeeTaskModule {
@IntoSet
TaskSystem.Task bindResourceWriteTask(ResourceWriteTask resourceWriteTask);
@Binds
@IntoSet
TaskSystem.Task bindWebAssetVersionCheckTask(WebAssetVersionCheckTask webAssetVersionCheckTask);
@Binds
@IntoSet
TaskSystem.Task bindActiveCookieStoreExpiryTask(ActiveCookieExpiryCleanupTask activeCookieExpiryCleanupTask);

View File

@ -136,6 +136,7 @@ task yarnStart(type: YarnTask) {
task copyYarnBuildResults {
inputs.files(fileTree("$rootDir/react/dashboard/build"))
outputs.dir("$rootDir/common/build/resources/main/assets/plan/web")
outputs.dir("$rootDir/common/build/resources/test/assets/plan/web")
dependsOn yarnBundle
doLast {
@ -144,6 +145,10 @@ task copyYarnBuildResults {
from "$rootDir/react/dashboard/build"
into "$rootDir/common/build/resources/main/assets/plan/web"
}
copy {
into "$rootDir/common/build/resources/main/assets/plan/web"
into "$rootDir/common/build/resources/test/assets/plan/web"
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.domain.datatransfer;
import com.djrapitops.plan.delivery.domain.datatransfer.extension.ExtensionDescriptionDto;
import java.util.List;
/**
* @author AuroraLS3
*/
public class PlayerListDto {
private final List<TablePlayerDto> players;
private final List<ExtensionDescriptionDto> extensionDescriptors;
public PlayerListDto(List<TablePlayerDto> players, List<ExtensionDescriptionDto> extensionDescriptors) {
this.players = players;
this.extensionDescriptors = extensionDescriptors;
}
public List<TablePlayerDto> getPlayers() {
return players;
}
public List<ExtensionDescriptionDto> getExtensionDescriptors() {
return extensionDescriptors;
}
@Override
public String toString() {
return "PlayerListDto{" +
"players=" + players +
", extensionDescriptors=" + extensionDescriptors +
'}';
}
}

View File

@ -0,0 +1,174 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.domain.datatransfer;
import com.djrapitops.plan.delivery.domain.datatransfer.extension.ExtensionValueDataDto;
import java.util.Map;
import java.util.UUID;
/**
* Represents a row for players table.
*
* @author AuroraLS3
*/
public class TablePlayerDto {
private UUID playerUUID;
private String playerName;
private double activityIndex;
private Long playtimeActive;
private Long sessionCount;
private Long lastSeen;
private Long registered;
private String country;
private Map<String, ExtensionValueDataDto> extensionValues;
private TablePlayerDto() {
// Builder constructor
}
public static TablePlayerDtoBuilder builder() {
return new TablePlayerDtoBuilder();
}
public String getPlayerName() {
return playerName;
}
private void setPlayerName(String playerName) {
this.playerName = playerName;
}
public double getActivityIndex() {
return activityIndex;
}
private void setActivityIndex(double activityIndex) {
this.activityIndex = activityIndex;
}
public Long getPlaytimeActive() {
return playtimeActive;
}
private void setPlaytimeActive(Long playtimeActive) {
this.playtimeActive = playtimeActive;
}
public Long getSessionCount() {
return sessionCount;
}
private void setSessionCount(Long sessionCount) {
this.sessionCount = sessionCount;
}
public Long getLastSeen() {
return lastSeen;
}
private void setLastSeen(Long lastSeen) {
this.lastSeen = lastSeen;
}
public Long getRegistered() {
return registered;
}
private void setRegistered(Long registered) {
this.registered = registered;
}
public String getCountry() {
return country;
}
private void setCountry(String country) {
this.country = country;
}
public Map<String, ExtensionValueDataDto> getExtensionValues() {
return extensionValues;
}
private void setExtensionValues(Map<String, ExtensionValueDataDto> extensionValues) {
this.extensionValues = extensionValues;
}
public UUID getPlayerUUID() {
return playerUUID;
}
public void setPlayerUUID(UUID playerUUID) {
this.playerUUID = playerUUID;
}
public static final class TablePlayerDtoBuilder {
private final TablePlayerDto tablePlayerDto;
private TablePlayerDtoBuilder() {tablePlayerDto = new TablePlayerDto();}
public TablePlayerDtoBuilder withUuid(UUID playerUUID) {
tablePlayerDto.setPlayerUUID(playerUUID);
return this;
}
public TablePlayerDtoBuilder withName(String name) {
tablePlayerDto.setPlayerName(name);
return this;
}
public TablePlayerDtoBuilder withActivityIndex(double activityIndex) {
tablePlayerDto.setActivityIndex(activityIndex);
return this;
}
public TablePlayerDtoBuilder withPlaytimeActive(Long playtimeActive) {
tablePlayerDto.setPlaytimeActive(playtimeActive);
return this;
}
public TablePlayerDtoBuilder withSessionCount(Long sessionCount) {
tablePlayerDto.setSessionCount(sessionCount);
return this;
}
public TablePlayerDtoBuilder withLastSeen(Long lastSeen) {
tablePlayerDto.setLastSeen(lastSeen);
return this;
}
public TablePlayerDtoBuilder withRegistered(Long registered) {
tablePlayerDto.setRegistered(registered);
return this;
}
public TablePlayerDtoBuilder withCountry(String country) {
tablePlayerDto.setCountry(country);
return this;
}
public TablePlayerDtoBuilder withExtensionValues(Map<String, ExtensionValueDataDto> extensionValues) {
tablePlayerDto.setExtensionValues(extensionValues);
return this;
}
public TablePlayerDto build() {return tablePlayerDto;}
}
}

View File

@ -16,12 +16,14 @@
*/
package com.djrapitops.plan.delivery.domain.datatransfer.extension;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.extension.FormatType;
import com.djrapitops.plan.extension.implementation.results.ExtensionTabData;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
public class ExtensionTabDataDto {
@ -36,25 +38,29 @@ public class ExtensionTabDataDto {
tableData = extensionTabData.getTableData().stream().map(ExtensionTableDataDto::new).collect(Collectors.toList());
}
private List<ExtensionValueDataDto> constructValues(List<String> order, ExtensionTabData tabData) {
public static Optional<ExtensionValueDataDto> mapToValue(ExtensionTabData tabData, String key) {
Formatters formatters = Formatters.getInstance();
Formatter<Double> decimalFormatter = formatters.decimals();
Formatter<Double> percentageFormatter = formatters.percentage();
Map<FormatType, Formatter<Long>> numberFormatters = new EnumMap<>(FormatType.class);
numberFormatters.put(FormatType.DATE_SECOND, formatters.secondLong());
numberFormatters.put(FormatType.DATE_YEAR, formatters.yearLong());
numberFormatters.put(FormatType.TIME_MILLISECONDS, formatters.timeAmount());
numberFormatters.put(FormatType.NONE, Object::toString);
Optional<ExtensionValueDataDto> booleanValue = tabData.getBoolean(key).map(data -> new ExtensionValueDataDto(data.getDescription(), "BOOLEAN", data.getFormattedValue()));
if (booleanValue.isPresent()) return booleanValue;
Optional<ExtensionValueDataDto> doubleValue = tabData.getDouble(key).map(data -> new ExtensionValueDataDto(data.getDescription(), "DOUBLE", data.getFormattedValue(formatters.decimals())));
if (doubleValue.isPresent()) return doubleValue;
Optional<ExtensionValueDataDto> percentage = tabData.getPercentage(key).map(data -> new ExtensionValueDataDto(data.getDescription(), "PERCENTAGE", data.getFormattedValue(formatters.percentage())));
if (percentage.isPresent()) return percentage;
Optional<ExtensionValueDataDto> number = tabData.getNumber(key).map(data -> new ExtensionValueDataDto(data.getDescription(), data.getFormatType() == FormatType.NONE ? "NUMBER" : data.getFormatType().name(), data.getFormattedValue(formatters.getNumberFormatter(data.getFormatType()))));
if (number.isPresent()) return number;
Optional<ExtensionValueDataDto> string = tabData.getString(key).map(data -> new ExtensionValueDataDto(data.getDescription(), data.isPlayerName() ? "LINK" : "STRING", data.getFormattedValue()));
if (string.isPresent()) return string;
// If component is not found either return empty Optional.
return tabData.getComponent(key).map(data -> new ExtensionValueDataDto(data.getDescription(), "COMPONENT", data.getFormattedValue()));
}
private List<ExtensionValueDataDto> constructValues(List<String> order, ExtensionTabData tabData) {
List<ExtensionValueDataDto> extensionValues = new ArrayList<>();
for (String key : order) {
tabData.getBoolean(key).ifPresent(data -> extensionValues.add(new ExtensionValueDataDto(data.getDescription(), "BOOLEAN", data.getFormattedValue())));
tabData.getDouble(key).ifPresent(data -> extensionValues.add(new ExtensionValueDataDto(data.getDescription(), "DOUBLE", data.getFormattedValue(decimalFormatter))));
tabData.getPercentage(key).ifPresent(data -> extensionValues.add(new ExtensionValueDataDto(data.getDescription(), "PERCENTAGE", data.getFormattedValue(percentageFormatter))));
tabData.getNumber(key).ifPresent(data -> extensionValues.add(new ExtensionValueDataDto(data.getDescription(), data.getFormatType() == FormatType.NONE ? "NUMBER" : data.getFormatType().name(), data.getFormattedValue(numberFormatters.get(data.getFormatType())))));
tabData.getString(key).ifPresent(data -> extensionValues.add(new ExtensionValueDataDto(data.getDescription(), data.isPlayerName() ? "HTML" : "STRING", data.getFormattedValue())));
tabData.getComponent(key).ifPresent(data -> extensionValues.add(new ExtensionValueDataDto(data.getDescription(), "COMPONENT", data.getFormattedValue())));
mapToValue(tabData, key).ifPresent(extensionValues::add);
}
return extensionValues;
}

View File

@ -16,8 +16,10 @@
*/
package com.djrapitops.plan.delivery.domain.datatransfer.extension;
import com.djrapitops.plan.delivery.rendering.html.structure.HtmlTable;
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 java.util.ArrayList;
import java.util.Arrays;
@ -41,11 +43,48 @@ public class TableDto {
.map(IconDto::new)
.collect(Collectors.toList());
rows = HtmlTable.mapToRows(table.getRows(), table.getTableColumnFormats()).stream()
rows = mapToRows(table.getRows(), table.getTableColumnFormats()).stream()
.map(row -> constructRow(columns, row))
.collect(Collectors.toList());
}
public static List<TableCellDto[]> mapToRows(List<Object[]> rows, TableColumnFormat[] tableColumnFormats) {
return rows.stream()
.map(row -> {
List<TableCellDto> mapped = new ArrayList<>(row.length);
for (int i = 0; i < row.length; i++) {
Object value = row[i];
if (value == null) {
mapped.add(null);
} else {
TableColumnFormat format = tableColumnFormats[i];
mapped.add(new TableCellDto(applyFormat(format, value), value));
}
}
return mapped.toArray(new TableCellDto[0]);
})
.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()));
default:
return value.toString();
}
} catch (Exception e) {
return Objects.toString(value);
}
}
private List<TableCellDto> constructRow(List<String> columns, TableCellDto[] row) {
List<TableCellDto> constructedRow = new ArrayList<>();

View File

@ -0,0 +1,70 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.domain.datatransfer.preferences;
import java.util.Objects;
/**
* @author AuroraLS3
*/
public class GraphThresholds {
private double highThreshold;
private double mediumThreshold;
public GraphThresholds(double highThreshold, double mediumThreshold) {
this.highThreshold = highThreshold;
this.mediumThreshold = mediumThreshold;
}
public double getHighThreshold() {
return highThreshold;
}
public void setHighThreshold(int highThreshold) {
this.highThreshold = highThreshold;
}
public double getMediumThreshold() {
return mediumThreshold;
}
public void setMediumThreshold(int mediumThreshold) {
this.mediumThreshold = mediumThreshold;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GraphThresholds that = (GraphThresholds) o;
return getHighThreshold() == that.getHighThreshold() && getMediumThreshold() == that.getMediumThreshold();
}
@Override
public int hashCode() {
return Objects.hash(getHighThreshold(), getMediumThreshold());
}
@Override
public String toString() {
return "GraphThresholds{" +
"highThreshold=" + highThreshold +
", mediumThreshold=" + mediumThreshold +
'}';
}
}

View File

@ -0,0 +1,212 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.domain.datatransfer.preferences;
import java.util.Objects;
/**
* @author AuroraLS3
*/
public class Preferences {
private boolean recentDaysInDateFormat;
private String dateFormatFull;
private String dateFormatNoSeconds;
private String dateFormatClock;
private TimeFormat timeFormat;
private String decimalFormat;
private int firstDay;
private String playerHeadImageUrl;
private GraphThresholds tpsThresholds;
private GraphThresholds diskThresholds;
private Preferences() {
}
public static Builder builder() {
return new Preferences.Builder();
}
public boolean isRecentDaysInDateFormat() {
return recentDaysInDateFormat;
}
private void setRecentDaysInDateFormat(boolean recentDaysInDateFormat) {
this.recentDaysInDateFormat = recentDaysInDateFormat;
}
public String getDateFormatFull() {
return dateFormatFull;
}
private void setDateFormatFull(String dateFormatFull) {
this.dateFormatFull = dateFormatFull;
}
public String getDateFormatNoSeconds() {
return dateFormatNoSeconds;
}
private void setDateFormatNoSeconds(String dateFormatNoSeconds) {
this.dateFormatNoSeconds = dateFormatNoSeconds;
}
public String getDateFormatClock() {
return dateFormatClock;
}
private void setDateFormatClock(String dateFormatClock) {
this.dateFormatClock = dateFormatClock;
}
public TimeFormat getTimeFormat() {
return timeFormat;
}
private void setTimeFormat(TimeFormat timeFormat) {
this.timeFormat = timeFormat;
}
public String getDecimalFormat() {
return decimalFormat;
}
private void setDecimalFormat(String decimalFormat) {
this.decimalFormat = decimalFormat;
}
public int getFirstDay() {
return firstDay;
}
private void setFirstDay(int firstDay) {
this.firstDay = firstDay;
}
public String getPlayerHeadImageUrl() {
return playerHeadImageUrl;
}
private void setPlayerHeadImageUrl(String playerHeadImageUrl) {
this.playerHeadImageUrl = playerHeadImageUrl;
}
public GraphThresholds getTpsThresholds() {
return tpsThresholds;
}
private void setTpsThresholds(GraphThresholds tpsThresholds) {
this.tpsThresholds = tpsThresholds;
}
public GraphThresholds getDiskThresholds() {
return diskThresholds;
}
private void setDiskThresholds(GraphThresholds diskThresholds) {
this.diskThresholds = diskThresholds;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Preferences that = (Preferences) o;
return isRecentDaysInDateFormat() == that.isRecentDaysInDateFormat() && getFirstDay() == that.getFirstDay() && Objects.equals(getDateFormatFull(), that.getDateFormatFull()) && Objects.equals(getDateFormatNoSeconds(), that.getDateFormatNoSeconds()) && Objects.equals(getDateFormatClock(), that.getDateFormatClock()) && Objects.equals(getTimeFormat(), that.getTimeFormat()) && Objects.equals(getDecimalFormat(), that.getDecimalFormat()) && Objects.equals(getPlayerHeadImageUrl(), that.getPlayerHeadImageUrl()) && Objects.equals(getTpsThresholds(), that.getTpsThresholds()) && Objects.equals(getDiskThresholds(), that.getDiskThresholds());
}
@Override
public int hashCode() {
return Objects.hash(isRecentDaysInDateFormat(), getDateFormatFull(), getDateFormatNoSeconds(), getDateFormatClock(), getTimeFormat(), getDecimalFormat(), getFirstDay(), getPlayerHeadImageUrl(), getTpsThresholds(), getDiskThresholds());
}
@Override
public String toString() {
return "Preferences{" +
"recentDaysInDateFormat=" + recentDaysInDateFormat +
", dateFormatFull='" + dateFormatFull + '\'' +
", dateFormatNoSeconds='" + dateFormatNoSeconds + '\'' +
", dateFormatClock='" + dateFormatClock + '\'' +
", timeFormat=" + timeFormat +
", decimalFormat='" + decimalFormat + '\'' +
", firstDay=" + firstDay +
", playerHeadImageUrl='" + playerHeadImageUrl + '\'' +
", tpsThresholds=" + tpsThresholds +
", diskThresholds=" + diskThresholds +
'}';
}
public static final class Builder {
private final Preferences preferences;
private Builder() {preferences = new Preferences();}
public Builder withRecentDaysInDateFormat(boolean recentDaysInDateFormat) {
preferences.setRecentDaysInDateFormat(recentDaysInDateFormat);
return this;
}
public Builder withDateFormatFull(String dateFormatFull) {
preferences.setDateFormatFull(dateFormatFull);
return this;
}
public Builder withDateFormatNoSeconds(String dateFormatNoSeconds) {
preferences.setDateFormatNoSeconds(dateFormatNoSeconds);
return this;
}
public Builder withDateFormatClock(String dateFormatClock) {
preferences.setDateFormatClock(dateFormatClock);
return this;
}
public Builder withTimeFormat(TimeFormat timeFormat) {
preferences.setTimeFormat(timeFormat);
return this;
}
public Builder withDecimalFormat(String decimalFormat) {
preferences.setDecimalFormat(decimalFormat);
return this;
}
public Builder withFirstDay(int firstDay) {
preferences.setFirstDay(firstDay);
return this;
}
public Builder withPlayerHeadImageUrl(String playerHeadImageUrl) {
preferences.setPlayerHeadImageUrl(playerHeadImageUrl);
return this;
}
public Builder withTpsThresholds(GraphThresholds tpsThresholds) {
preferences.setTpsThresholds(tpsThresholds);
return this;
}
public Builder withDiskThresholds(GraphThresholds diskThresholds) {
preferences.setDiskThresholds(diskThresholds);
return this;
}
public Preferences build() {return preferences;}
}
}

View File

@ -0,0 +1,210 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.domain.datatransfer.preferences;
import java.util.Objects;
/**
* @author AuroraLS3
*/
public class TimeFormat {
private String year;
private String years;
private String month;
private String months;
private String day;
private String days;
private String hours;
private String minutes;
private String seconds;
private String zero;
private TimeFormat() {
// Constructor is private for builder
}
public static Builder builder() {
return new Builder();
}
public String getYear() {
return year;
}
private void setYear(String year) {
this.year = year;
}
public String getYears() {
return years;
}
private void setYears(String years) {
this.years = years;
}
public String getMonth() {
return month;
}
private void setMonth(String month) {
this.month = month;
}
public String getMonths() {
return months;
}
private void setMonths(String months) {
this.months = months;
}
public String getDay() {
return day;
}
private void setDay(String day) {
this.day = day;
}
public String getDays() {
return days;
}
private void setDays(String days) {
this.days = days;
}
public String getHours() {
return hours;
}
private void setHours(String hours) {
this.hours = hours;
}
public String getMinutes() {
return minutes;
}
private void setMinutes(String minutes) {
this.minutes = minutes;
}
public String getSeconds() {
return seconds;
}
private void setSeconds(String seconds) {
this.seconds = seconds;
}
public String getZero() {
return zero;
}
private void setZero(String zero) {
this.zero = zero;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TimeFormat that = (TimeFormat) o;
return Objects.equals(getYear(), that.getYear()) && Objects.equals(getYears(), that.getYears()) && Objects.equals(getMonth(), that.getMonth()) && Objects.equals(getMonths(), that.getMonths()) && Objects.equals(getDay(), that.getDay()) && Objects.equals(getDays(), that.getDays()) && Objects.equals(getHours(), that.getHours()) && Objects.equals(getMinutes(), that.getMinutes()) && Objects.equals(getSeconds(), that.getSeconds()) && Objects.equals(getZero(), that.getZero());
}
@Override
public int hashCode() {
return Objects.hash(getYear(), getYears(), getMonth(), getMonths(), getDay(), getDays(), getHours(), getMinutes(), getSeconds(), getZero());
}
@Override
public String toString() {
return "TimeFormat{" +
"year='" + year + '\'' +
", years='" + years + '\'' +
", month='" + month + '\'' +
", months='" + months + '\'' +
", day='" + day + '\'' +
", days='" + days + '\'' +
", hours='" + hours + '\'' +
", minutes='" + minutes + '\'' +
", seconds='" + seconds + '\'' +
", zero='" + zero + '\'' +
'}';
}
public static final class Builder {
private final TimeFormat timeFormat;
private Builder() {timeFormat = new TimeFormat();}
public Builder withYear(String year) {
timeFormat.setYear(year);
return this;
}
public Builder withYears(String years) {
timeFormat.setYears(years);
return this;
}
public Builder withMonth(String month) {
timeFormat.setMonth(month);
return this;
}
public Builder withMonths(String months) {
timeFormat.setMonths(months);
return this;
}
public Builder withDay(String day) {
timeFormat.setDay(day);
return this;
}
public Builder withDays(String days) {
timeFormat.setDays(days);
return this;
}
public Builder withHours(String hours) {
timeFormat.setHours(hours);
return this;
}
public Builder withMinutes(String minutes) {
timeFormat.setMinutes(minutes);
return this;
}
public Builder withSeconds(String seconds) {
timeFormat.setSeconds(seconds);
return this;
}
public Builder withZero(String zero) {
timeFormat.setZero(zero);
return this;
}
public TimeFormat build() {return timeFormat;}
}
}

View File

@ -20,7 +20,6 @@ import com.djrapitops.plan.identification.Server;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.ExportSettings;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
@ -86,11 +85,6 @@ public class ExportScheduler extends PluginRunnable {
}
private void scheduleReactExport() {
if (config.isTrue(PluginSettings.LEGACY_FRONTEND) ||
config.isFalse(ExportSettings.SERVER_PAGE) &&
config.isFalse(ExportSettings.PLAYER_PAGES) &&
config.isFalse(ExportSettings.PLAYERS_PAGE)) {return;}
runnableFactory.create(
new ExportTask(exporter, Exporter::exportReact, errorLogger)
).runTaskLaterAsynchronously(TimeAmount.toTicks(5, TimeUnit.SECONDS));

View File

@ -16,23 +16,16 @@
*/
package com.djrapitops.plan.delivery.export;
import com.djrapitops.plan.delivery.rendering.pages.Page;
import com.djrapitops.plan.delivery.rendering.pages.PageFactory;
import com.djrapitops.plan.delivery.web.ResourceService;
import com.djrapitops.plan.delivery.web.resolver.Response;
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.delivery.webserver.resolver.json.RootJSONResolver;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.identification.Server;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
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.file.PlanFiles;
import com.djrapitops.plan.storage.file.Resource;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
@ -54,25 +47,19 @@ public class NetworkPageExporter extends FileExporter {
private final PlanFiles files;
private final PlanConfig config;
private final DBSystem dbSystem;
private final PageFactory pageFactory;
private final RootJSONResolver jsonHandler;
private final Theme theme;
@Inject
public NetworkPageExporter(
PlanFiles files,
PlanConfig config,
DBSystem dbSystem,
PageFactory pageFactory,
RootJSONResolver jsonHandler,
Theme theme
RootJSONResolver jsonHandler
) {
this.files = files;
this.config = config;
this.dbSystem = dbSystem;
this.pageFactory = pageFactory;
this.jsonHandler = jsonHandler;
this.theme = theme;
}
/**
@ -89,35 +76,10 @@ public class NetworkPageExporter extends FileExporter {
ExportPaths exportPaths = new ExportPaths();
exportPaths.put("./players", toRelativePathFromRoot("players"));
exportRequiredResources(exportPaths, toDirectory);
exportJSON(exportPaths, toDirectory, server);
exportHtml(exportPaths, toDirectory);
exportReactRedirects(toDirectory);
}
private void exportHtml(ExportPaths exportPaths, Path toDirectory) throws IOException {
if (config.isFalse(PluginSettings.LEGACY_FRONTEND)) return;
Path to = toDirectory
.resolve("network")
.resolve("index.html");
Page page = pageFactory.networkPage();
// Fixes refreshingJsonRequest ignoring old data of export
String html = StringUtils.replaceEach(page.toHtml(),
new String[]{"loadPlayersOnlineGraph, 'network-overview', true);",
"&middot; Performance",
"<head>"
},
new String[]{"loadPlayersOnlineGraph, 'network-overview');",
"&middot; Performance (Unavailable with Export)",
"<head><style>.refresh-element {display: none;}</style>"
});
export(to, exportPaths.resolveExportPaths(html));
}
public static String[] getRedirections() {
return new String[]{
"network",
@ -134,8 +96,6 @@ public class NetworkPageExporter extends FileExporter {
}
private void exportReactRedirects(Path toDirectory) throws IOException {
if (config.isTrue(PluginSettings.LEGACY_FRONTEND)) return;
exportReactRedirects(toDirectory, files, config, getRedirections());
}
@ -170,7 +130,8 @@ public class NetworkPageExporter extends FileExporter {
"sessions",
"extensionData?server=" + serverUUID,
"retention",
"joinAddresses"
"joinAddresses",
"playersTable"
);
}
@ -210,75 +171,6 @@ public class NetworkPageExporter extends FileExporter {
}
}
private void exportRequiredResources(ExportPaths exportPaths, Path toDirectory) throws IOException {
if (config.isFalse(PluginSettings.LEGACY_FRONTEND)) return;
exportResources(exportPaths, toDirectory,
"./img/Flaticon_circle.png",
"./css/sb-admin-2.css",
"./css/style.css",
"./css/noauth.css",
"./vendor/datatables/datatables.min.js",
"./vendor/datatables/datatables.min.css",
"./vendor/highcharts/modules/map.js",
"./vendor/highcharts/mapdata/world.js",
"./vendor/highcharts/modules/drilldown.js",
"./vendor/highcharts/highcharts.js",
"./vendor/highcharts/modules/no-data-to-display.js",
"./vendor/masonry/masonry.pkgd.min.js",
"./vendor/fontawesome-free/css/all.min.css",
"./vendor/fontawesome-free/webfonts/fa-brands-400.eot",
"./vendor/fontawesome-free/webfonts/fa-brands-400.ttf",
"./vendor/fontawesome-free/webfonts/fa-brands-400.woff",
"./vendor/fontawesome-free/webfonts/fa-brands-400.woff2",
"./vendor/fontawesome-free/webfonts/fa-regular-400.eot",
"./vendor/fontawesome-free/webfonts/fa-regular-400.ttf",
"./vendor/fontawesome-free/webfonts/fa-regular-400.woff",
"./vendor/fontawesome-free/webfonts/fa-regular-400.woff2",
"./vendor/fontawesome-free/webfonts/fa-solid-900.eot",
"./vendor/fontawesome-free/webfonts/fa-solid-900.ttf",
"./vendor/fontawesome-free/webfonts/fa-solid-900.woff",
"./vendor/fontawesome-free/webfonts/fa-solid-900.woff2",
"./js/domUtils.js",
"./js/sb-admin-2.js",
"./js/xmlhttprequests.js",
"./js/color-selector.js",
"./js/sessionAccordion.js",
"./js/pingTable.js",
"./js/graphs.js",
"./js/network-values.js"
);
}
private void exportResources(ExportPaths exportPaths, Path toDirectory, String... resourceNames) throws IOException {
for (String resourceName : resourceNames) {
String nonRelativePath = toNonRelativePath(resourceName);
exportResource(toDirectory, nonRelativePath);
exportPaths.put(resourceName, toRelativePathFromRoot(nonRelativePath));
}
}
private void exportResource(Path toDirectory, String resourceName) throws IOException {
WebResource resource = ResourceService.getInstance().getResource("Plan", resourceName,
() -> files.getResourceFromJar("web/" + resourceName).asWebResource());
Path to = toDirectory.resolve(resourceName);
if (resourceName.endsWith(".css") || resourceName.endsWith("color-selector.js")) {
export(to, theme.replaceThemeColors(resource.asString()));
} else if ("js/network-values.js".equalsIgnoreCase(resourceName) || "js/sessionAccordion.js".equalsIgnoreCase(resourceName)) {
String relativePlayerLink = toRelativePathFromRoot("player");
String relativeServerLink = toRelativePathFromRoot("server/");
export(to, StringUtils.replaceEach(resource.asString(),
new String[]{"../player", "./player", "./server/", "server/"},
new String[]{relativePlayerLink, relativePlayerLink, relativeServerLink, relativeServerLink}
));
} else if (Resource.isTextResource(resourceName)) {
export(to, resource.asString());
} else {
export(to, resource);
}
}
private String toRelativePathFromRoot(String resourceName) {
// Network html is exported at /network//index.html or /server/index.html
return "../" + toNonRelativePath(resourceName);

View File

@ -16,25 +16,16 @@
*/
package com.djrapitops.plan.delivery.export;
import com.djrapitops.plan.delivery.domain.container.PlayerContainer;
import com.djrapitops.plan.delivery.rendering.pages.Page;
import com.djrapitops.plan.delivery.rendering.pages.PageFactory;
import com.djrapitops.plan.delivery.web.ResourceService;
import com.djrapitops.plan.delivery.web.resolver.Response;
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.delivery.webserver.resolver.json.RootJSONResolver;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
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.PlayerFetchQueries;
import com.djrapitops.plan.storage.database.queries.containers.ContainerFetchQueries;
import com.djrapitops.plan.storage.file.PlanFiles;
import com.djrapitops.plan.storage.file.Resource;
import org.apache.commons.lang3.StringUtils;
import javax.inject.Inject;
@ -56,25 +47,19 @@ public class PlayerPageExporter extends FileExporter {
private final PlanFiles files;
private final PlanConfig config;
private final DBSystem dbSystem;
private final PageFactory pageFactory;
private final RootJSONResolver jsonHandler;
private final Theme theme;
@Inject
public PlayerPageExporter(
PlanFiles files,
PlanConfig config,
DBSystem dbSystem,
PageFactory pageFactory,
RootJSONResolver jsonHandler,
Theme theme
RootJSONResolver jsonHandler
) {
this.files = files;
this.config = config;
this.dbSystem = dbSystem;
this.pageFactory = pageFactory;
this.jsonHandler = jsonHandler;
this.theme = theme;
}
public static String[] getRedirections(UUID playerUUID) {
@ -106,33 +91,14 @@ public class PlayerPageExporter extends FileExporter {
ExportPaths exportPaths = new ExportPaths();
exportPaths.put("../network", toRelativePathFromRoot("network"));
exportPaths.put("../server/", toRelativePathFromRoot("server"));
exportRequiredResources(exportPaths, toDirectory);
Path playerDirectory = toDirectory.resolve("player/" + toFileName(playerUUID.toString()));
exportJSON(exportPaths, playerDirectory, playerUUID);
exportHtml(exportPaths, playerDirectory, playerUUID);
exportReactRedirects(toDirectory, playerUUID);
exportPaths.clear();
}
private void exportHtml(ExportPaths exportPaths, Path playerDirectory, UUID playerUUID) throws IOException {
if (config.isFalse(PluginSettings.LEGACY_FRONTEND)) return;
Path to = playerDirectory.resolve("index.html");
try {
Database db = dbSystem.getDatabase();
PlayerContainer player = db.query(ContainerFetchQueries.fetchPlayerContainer(playerUUID));
Page page = pageFactory.playerPage(player);
export(to, exportPaths.resolveExportPaths(page.toHtml()));
} catch (IllegalStateException notFound) {
throw new NotFoundException(notFound.getMessage());
}
}
private void exportReactRedirects(Path toDirectory, UUID playerUUID) throws IOException {
if (config.isTrue(PluginSettings.LEGACY_FRONTEND)) return;
exportReactRedirects(toDirectory, files, config, getRedirections(playerUUID));
}
@ -163,70 +129,6 @@ public class PlayerPageExporter extends FileExporter {
}
}
private void exportRequiredResources(ExportPaths exportPaths, Path toDirectory) throws IOException {
if (config.isFalse(PluginSettings.LEGACY_FRONTEND)) return;
// Style
exportResources(exportPaths, toDirectory,
"../img/Flaticon_circle.png",
"../css/sb-admin-2.css",
"../css/style.css",
"../css/noauth.css",
"../vendor/datatables/datatables.min.js",
"../vendor/datatables/datatables.min.css",
"../vendor/highcharts/modules/map.js",
"../vendor/highcharts/mapdata/world.js",
"../vendor/highcharts/modules/drilldown.js",
"../vendor/highcharts/highcharts.js",
"../vendor/highcharts/modules/no-data-to-display.js",
"../vendor/fullcalendar/fullcalendar.min.css",
"../vendor/momentjs/moment.js",
"../vendor/masonry/masonry.pkgd.min.js",
"../vendor/fullcalendar/fullcalendar.min.js",
"../vendor/fontawesome-free/css/all.min.css",
"../vendor/fontawesome-free/webfonts/fa-brands-400.eot",
"../vendor/fontawesome-free/webfonts/fa-brands-400.ttf",
"../vendor/fontawesome-free/webfonts/fa-brands-400.woff",
"../vendor/fontawesome-free/webfonts/fa-brands-400.woff2",
"../vendor/fontawesome-free/webfonts/fa-regular-400.eot",
"../vendor/fontawesome-free/webfonts/fa-regular-400.ttf",
"../vendor/fontawesome-free/webfonts/fa-regular-400.woff",
"../vendor/fontawesome-free/webfonts/fa-regular-400.woff2",
"../vendor/fontawesome-free/webfonts/fa-solid-900.eot",
"../vendor/fontawesome-free/webfonts/fa-solid-900.ttf",
"../vendor/fontawesome-free/webfonts/fa-solid-900.woff",
"../vendor/fontawesome-free/webfonts/fa-solid-900.woff2",
"../js/sb-admin-2.js",
"../js/xmlhttprequests.js",
"../js/color-selector.js",
"../js/sessionAccordion.js",
"../js/graphs.js",
"../js/player-values.js"
);
}
private void exportResources(ExportPaths exportPaths, Path toDirectory, String... resourceNames) throws IOException {
for (String resourceName : resourceNames) {
String nonRelativePath = toNonRelativePath(resourceName);
exportResource(toDirectory, nonRelativePath);
exportPaths.put(resourceName, toRelativePathFromRoot(nonRelativePath));
}
}
private void exportResource(Path toDirectory, String resourceName) throws IOException {
WebResource resource = ResourceService.getInstance().getResource("Plan", resourceName,
() -> files.getResourceFromJar("web/" + resourceName).asWebResource());
Path to = toDirectory.resolve(resourceName);
if (resourceName.endsWith(".css") || resourceName.endsWith("color-selector.js")) {
export(to, theme.replaceThemeColors(resource.asString()));
} else if (Resource.isTextResource(resourceName)) {
export(to, resource.asString());
} else {
export(to, resource);
}
}
private String toRelativePathFromRoot(String resourceName) {
// Player html is exported at /player/<uuid>/index.html
return "../../" + toNonRelativePath(resourceName);

View File

@ -16,23 +16,16 @@
*/
package com.djrapitops.plan.delivery.export;
import com.djrapitops.plan.delivery.rendering.pages.Page;
import com.djrapitops.plan.delivery.rendering.pages.PageFactory;
import com.djrapitops.plan.delivery.web.ResourceService;
import com.djrapitops.plan.delivery.web.resolver.Response;
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.delivery.webserver.resolver.json.RootJSONResolver;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
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.file.PlanFiles;
import com.djrapitops.plan.storage.file.Resource;
import org.apache.commons.lang3.StringUtils;
import javax.inject.Inject;
@ -53,28 +46,23 @@ public class PlayersPageExporter extends FileExporter {
private final PlanFiles files;
private final PlanConfig config;
private final DBSystem dbSystem;
private final PageFactory pageFactory;
private final RootJSONResolver jsonHandler;
private final Theme theme;
private final ServerInfo serverInfo;
private final ExportPaths exportPaths;
private static final String PLAYERS_TABLE = "playersTable";
@Inject
public PlayersPageExporter(
PlanFiles files,
PlanConfig config, DBSystem dbSystem,
PageFactory pageFactory,
RootJSONResolver jsonHandler,
Theme theme,
ServerInfo serverInfo
) {
this.files = files;
this.config = config;
this.dbSystem = dbSystem;
this.pageFactory = pageFactory;
this.jsonHandler = jsonHandler;
this.theme = theme;
this.serverInfo = serverInfo;
exportPaths = new ExportPaths();
@ -85,121 +73,42 @@ public class PlayersPageExporter extends FileExporter {
if (dbState == Database.State.CLOSED || dbState == Database.State.CLOSING) return;
exportPaths.put("href=\"/\"", "href=\"" + toRelativePathFromRoot(serverInfo.getServer().isProxy() ? "network" : "server") + '"');
exportRequiredResources(toDirectory);
exportJSON(toDirectory);
exportHtml(toDirectory);
exportReactRedirects(toDirectory);
exportPaths.clear();
}
private void exportHtml(Path toDirectory) throws IOException {
if (config.isFalse(PluginSettings.LEGACY_FRONTEND)) return;
Path to = toDirectory
.resolve("players")
.resolve("index.html");
Page page = pageFactory.playersPage();
// Fixes refreshingJsonRequest ignoring old data of export
String html = StringUtils.replaceEach(page.toHtml(),
new String[]{
"}, 'playerlist', true);",
"<head>"
},
new String[]{
"}, 'playerlist');",
"<head><style>.refresh-element {display: none;}</style>"
});
export(to, exportPaths.resolveExportPaths(html));
}
private void exportReactRedirects(Path toDirectory) throws IOException {
if (config.isTrue(PluginSettings.LEGACY_FRONTEND)) return;
String[] redirections = {"players"};
exportReactRedirects(toDirectory, files, config, redirections);
}
private void exportJSON(Path toDirectory) throws IOException {
Response response = getJSONResponse("players")
Response response = getJSONResponse()
.orElseThrow(() -> new NotFoundException("players page was not properly exported: not found"));
String jsonResourceName = toFileName(toJSONResourceName("players")) + ".json";
String jsonResourceName = toFileName(toJSONResourceName()) + ".json";
export(toDirectory.resolve("data").resolve(jsonResourceName),
// Replace ../player in urls to fix player page links
StringUtils.replace(response.getAsString(), "../player", toRelativePathFromRoot("player"))
);
exportPaths.put("./v1/players", toRelativePathFromRoot("data/" + jsonResourceName));
exportPaths.put("./v1/" + PLAYERS_TABLE, toRelativePathFromRoot("data/" + jsonResourceName));
}
private String toJSONResourceName(String resource) {
return StringUtils.replaceEach(resource, new String[]{"?", "&", "type=", "server="}, new String[]{"-", "_", "", ""});
private String toJSONResourceName() {
return StringUtils.replaceEach(PLAYERS_TABLE, new String[]{"?", "&", "type=", "server="}, new String[]{"-", "_", "", ""});
}
private Optional<Response> getJSONResponse(String resource) {
private Optional<Response> getJSONResponse() {
try {
return jsonHandler.getResolver().resolve(new Request("GET", "/v1/" + resource, null, Collections.emptyMap()));
return jsonHandler.getResolver().resolve(new Request("GET", "/v1/" + PLAYERS_TABLE, null, Collections.emptyMap()));
} catch (WebUserAuthException e) {
// The rest of the exceptions should not be thrown
throw new IllegalStateException("Unexpected exception thrown: " + e.toString(), e);
}
}
private void exportRequiredResources(Path toDirectory) throws IOException {
if (config.isFalse(PluginSettings.LEGACY_FRONTEND)) return;
// Style
exportResources(toDirectory,
"img/Flaticon_circle.png",
"css/sb-admin-2.css",
"css/style.css",
"css/noauth.css",
"vendor/datatables/datatables.min.js",
"vendor/datatables/datatables.min.css",
"vendor/fontawesome-free/css/all.min.css",
"vendor/fontawesome-free/webfonts/fa-brands-400.eot",
"vendor/fontawesome-free/webfonts/fa-brands-400.ttf",
"vendor/fontawesome-free/webfonts/fa-brands-400.woff",
"vendor/fontawesome-free/webfonts/fa-brands-400.woff2",
"vendor/fontawesome-free/webfonts/fa-regular-400.eot",
"vendor/fontawesome-free/webfonts/fa-regular-400.ttf",
"vendor/fontawesome-free/webfonts/fa-regular-400.woff",
"vendor/fontawesome-free/webfonts/fa-regular-400.woff2",
"vendor/fontawesome-free/webfonts/fa-solid-900.eot",
"vendor/fontawesome-free/webfonts/fa-solid-900.ttf",
"vendor/fontawesome-free/webfonts/fa-solid-900.woff",
"vendor/fontawesome-free/webfonts/fa-solid-900.woff2",
"js/sb-admin-2.js",
"js/xmlhttprequests.js",
"js/color-selector.js"
);
}
private void exportResources(Path toDirectory, String... resourceNames) throws IOException {
for (String resourceName : resourceNames) {
exportResource(toDirectory, resourceName);
}
}
private void exportResource(Path toDirectory, String resourceName) throws IOException {
WebResource resource = ResourceService.getInstance().getResource("Plan", resourceName,
() -> files.getResourceFromJar("web/" + resourceName).asWebResource());
Path to = toDirectory.resolve(resourceName);
if (resourceName.endsWith(".css") || resourceName.endsWith("color-selector.js")) {
export(to, theme.replaceThemeColors(resource.asString()));
} else if (Resource.isTextResource(resourceName)) {
export(to, resource.asString());
} else {
export(to, resource);
}
exportPaths.put(resourceName, toRelativePathFromRoot(resourceName));
}
private String toRelativePathFromRoot(String resourceName) {
// Players html is exported at /players/index.html or /server/index.html
return "../" + toNonRelativePath(resourceName);

View File

@ -84,6 +84,7 @@ public class ReactExporter extends FileExporter {
exportJson(toDirectory, "metadata");
exportJson(toDirectory, "version");
exportJson(toDirectory, "networkMetadata");
exportJson(toDirectory, "preferences");
}
private void exportLocaleJson(Path toDirectory) throws IOException {

View File

@ -16,25 +16,18 @@
*/
package com.djrapitops.plan.delivery.export;
import com.djrapitops.plan.delivery.rendering.pages.Page;
import com.djrapitops.plan.delivery.rendering.pages.PageFactory;
import com.djrapitops.plan.delivery.web.ResourceService;
import com.djrapitops.plan.delivery.web.resolver.Response;
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.delivery.webserver.resolver.json.RootJSONResolver;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.identification.Server;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
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.file.PlanFiles;
import com.djrapitops.plan.storage.file.Resource;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
@ -55,10 +48,8 @@ public class ServerPageExporter extends FileExporter {
private final PlanFiles files;
private final PlanConfig config;
private final PageFactory pageFactory;
private final DBSystem dbSystem;
private final RootJSONResolver jsonHandler;
private final Theme theme;
private final ServerInfo serverInfo;
private final ExportPaths exportPaths;
@ -67,18 +58,14 @@ public class ServerPageExporter extends FileExporter {
public ServerPageExporter(
PlanFiles files,
PlanConfig config,
PageFactory pageFactory,
DBSystem dbSystem,
RootJSONResolver jsonHandler,
Theme theme,
ServerInfo serverInfo // To know if current server is a Proxy
) {
this.files = files;
this.config = config;
this.pageFactory = pageFactory;
this.dbSystem = dbSystem;
this.jsonHandler = jsonHandler;
this.theme = theme;
this.serverInfo = serverInfo;
exportPaths = new ExportPaths();
@ -97,41 +84,11 @@ public class ServerPageExporter extends FileExporter {
if (dbState == Database.State.CLOSED || dbState == Database.State.CLOSING) return;
exportPaths.put("../network", toRelativePathFromRoot("network"));
exportRequiredResources(toDirectory);
exportJSON(toDirectory, server);
exportHtml(toDirectory, server);
exportReactRedirects(toDirectory, server.getUuid());
exportPaths.clear();
}
private void exportHtml(Path toDirectory, Server server) throws IOException {
if (config.isFalse(PluginSettings.LEGACY_FRONTEND)) return;
ServerUUID serverUUID = server.getUuid();
Path to = toDirectory
.resolve(serverInfo.getServer().isProxy() ? "server/" + toFileName(server.getName()) : "server")
.resolve("index.html");
Page page = pageFactory.serverPage(serverUUID);
// Fixes refreshingJsonRequest ignoring old data of export
String html = StringUtils.replaceEach(page.toHtml(),
new String[]{
"loadOptimizedPerformanceGraph, 'performance', true);",
"loadserverCalendar, 'online-activity-overview', true);",
"}, 'playerlist', true);",
"<head>"
},
new String[]{
"loadOptimizedPerformanceGraph, 'performance');",
"loadserverCalendar, 'online-activity-overview');",
"}, 'playerlist');",
"<head><style>.refresh-element {display: none;}</style>"
});
export(to, exportPaths.resolveExportPaths(html));
}
public static String[] getRedirections(ServerUUID serverUUID) {
String server = "server/";
return new String[]{
@ -151,8 +108,6 @@ public class ServerPageExporter extends FileExporter {
}
private void exportReactRedirects(Path toDirectory, ServerUUID serverUUID) throws IOException {
if (config.isTrue(PluginSettings.LEGACY_FRONTEND)) return;
exportReactRedirects(toDirectory, files, config, getRedirections(serverUUID));
}
@ -186,7 +141,7 @@ public class ServerPageExporter extends FileExporter {
"graph?type=joinAddressByDay&server=" + serverUUID,
"graph?type=serverCalendar&server=" + serverUUID,
"graph?type=punchCard&server=" + serverUUID,
"players?server=" + serverUUID,
"playersTable?server=" + serverUUID,
"kills?server=" + serverUUID,
"pingTable?server=" + serverUUID,
"sessions?server=" + serverUUID,
@ -233,71 +188,6 @@ public class ServerPageExporter extends FileExporter {
}
}
private void exportRequiredResources(Path toDirectory) throws IOException {
if (config.isFalse(PluginSettings.LEGACY_FRONTEND)) return;
// Style
exportResources(toDirectory,
"../img/Flaticon_circle.png",
"../css/sb-admin-2.css",
"../css/style.css",
"../vendor/datatables/datatables.min.js",
"../vendor/datatables/datatables.min.css",
"../vendor/highcharts/modules/map.js",
"../vendor/highcharts/mapdata/world.js",
"../vendor/highcharts/modules/drilldown.js",
"../vendor/highcharts/highcharts.js",
"../vendor/highcharts/modules/no-data-to-display.js",
"../vendor/fullcalendar/fullcalendar.min.css",
"../vendor/momentjs/moment.js",
"../vendor/masonry/masonry.pkgd.min.js",
"../vendor/fullcalendar/fullcalendar.min.js",
"../vendor/fontawesome-free/css/all.min.css",
"../vendor/fontawesome-free/webfonts/fa-brands-400.eot",
"../vendor/fontawesome-free/webfonts/fa-brands-400.ttf",
"../vendor/fontawesome-free/webfonts/fa-brands-400.woff",
"../vendor/fontawesome-free/webfonts/fa-brands-400.woff2",
"../vendor/fontawesome-free/webfonts/fa-regular-400.eot",
"../vendor/fontawesome-free/webfonts/fa-regular-400.ttf",
"../vendor/fontawesome-free/webfonts/fa-regular-400.woff",
"../vendor/fontawesome-free/webfonts/fa-regular-400.woff2",
"../vendor/fontawesome-free/webfonts/fa-solid-900.eot",
"../vendor/fontawesome-free/webfonts/fa-solid-900.ttf",
"../vendor/fontawesome-free/webfonts/fa-solid-900.woff",
"../vendor/fontawesome-free/webfonts/fa-solid-900.woff2",
"../js/domUtils.js",
"../js/sb-admin-2.js",
"../js/xmlhttprequests.js",
"../js/color-selector.js",
"../js/sessionAccordion.js",
"../js/pingTable.js",
"../js/graphs.js",
"../js/server-values.js"
);
}
private void exportResources(Path toDirectory, String... resourceNames) throws IOException {
for (String resourceName : resourceNames) {
String nonRelativePath = toNonRelativePath(resourceName);
exportResource(toDirectory, nonRelativePath);
exportPaths.put(resourceName, toRelativePathFromRoot(nonRelativePath));
}
}
private void exportResource(Path toDirectory, String resourceName) throws IOException {
WebResource resource = ResourceService.getInstance().getResource("Plan", resourceName,
() -> files.getResourceFromJar("web/" + resourceName).asWebResource());
Path to = toDirectory.resolve(resourceName);
if (resourceName.endsWith(".css") || resourceName.endsWith("color-selector.js")) {
export(to, theme.replaceThemeColors(resource.asString()));
} else if (Resource.isTextResource(resourceName)) {
export(to, resource.asString());
} else {
export(to, resource);
}
}
private String toRelativePathFromRoot(String resourceName) {
// Server html is exported at /server/<name>/index.html or /server/index.html
return (serverInfo.getServer().isProxy() ? "../../" : "../") + toNonRelativePath(resourceName);

View File

@ -18,6 +18,7 @@ package com.djrapitops.plan.delivery.formatting;
import com.djrapitops.plan.delivery.domain.DateHolder;
import com.djrapitops.plan.delivery.formatting.time.*;
import com.djrapitops.plan.extension.FormatType;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.locale.Locale;
@ -160,6 +161,20 @@ public class Formatters {
return value -> byteSizeFormatter.apply((double) value);
}
public Formatter<Long> getNumberFormatter(FormatType type) {
switch (type) {
case DATE_SECOND:
return secondLong();
case DATE_YEAR:
return yearLong();
case TIME_MILLISECONDS:
return timeAmount();
case NONE:
default:
return Object::toString;
}
}
static class Holder {
static final AtomicReference<Formatters> formatters = new AtomicReference<>();

View File

@ -113,31 +113,12 @@ public class Contributors {
// Static method class
}
public static String generateContributorHtml() {
int estimatedLength = CONTRIBUTOR_ARRAY.length * 40 + 50;
StringBuilder html = new StringBuilder(estimatedLength);
Arrays.stream(CONTRIBUTOR_ARRAY).sorted()
.forEach(contributor -> contributor.appendHtml(html));
return html.toString();
}
public static List<Contributor> getContributors() {
return Arrays.stream(CONTRIBUTOR_ARRAY).sorted().collect(Collectors.toList());
}
enum For {
CODE("fa-code"),
LANG("fa-language");
private final String icon;
For(String icon) {
this.icon = icon;
}
String toHtml() {
return " <i class=\"fa fa-fw " + icon + "\"></i>";
}
CODE, LANG
}
private static class Contributor implements Comparable<Contributor> {
@ -149,15 +130,6 @@ public class Contributors {
this.contributed = contributed;
}
public void appendHtml(StringBuilder html) {
html.append("<li class=\"col-4\">")
.append(name);
for (For contribution : contributed) {
html.append(contribution.toHtml());
}
html.append("</li>");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -18,7 +18,6 @@ package com.djrapitops.plan.delivery.rendering.html;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringSubstitutor;
import org.apache.commons.text.TextStringBuilder;
import java.io.Serializable;
import java.net.URLEncoder;
@ -31,39 +30,8 @@ import java.util.Map;
*/
public enum Html {
COLOR_0("<span class=\"black\">"),
COLOR_1("<span class=\"darkblue\">"),
COLOR_2("<span class=\"darkgreen\">"),
COLOR_3("<span class=\"darkaqua\">"),
COLOR_4("<span class=\"darkred\">"),
COLOR_5("<span class=\"darkpurple\">"),
COLOR_6("<span class=\"gold\">"),
COLOR_7("<span class=\"gray\">"),
COLOR_8("<span class=\"darkgray\">"),
COLOR_9("<span class=\"blue\">"),
COLOR_A("<span class=\"green\">"),
COLOR_B("<span class=\"aqua\">"),
COLOR_C("<span class=\"red\">"),
COLOR_D("<span class=\"pink\">"),
COLOR_E("<span class=\"yellow\">"),
COLOR_F("<span class=\"white\">"),
SPAN("${0}</span>"),
LINK("<a class=\"link\" href=\"${0}\">${1}</a>"),
LINK_EXTERNAL("<a class=\"link\" rel=\"noopener noreferrer\" target=\"_blank\" href=\"${0}\">${1}</a>"),
BACK_BUTTON_NETWORK("<a class=\"btn bg-plan btn-icon-split\" href=\"../network\">" +
"<span class=\"icon text-white-50\">" +
"<i class=\"fas fa-fw fa-arrow-left\"></i><i class=\"fas fa-fw fa-cloud\"></i>" +
"</span>" +
"<span class=\"text\">Network page</span>" +
"</a>"),
BACK_BUTTON_SERVER("<a class=\"btn bg-plan btn-icon-split\" href=\"../server/\">" +
"<span class=\"icon text-white-50\">" +
"<i class=\"fas fa-fw fa-arrow-left\"></i><i class=\"fas fa-fw fa-server\"></i>" +
"</span>" +
"<span class=\"text\">Server page</span>" +
"</a>");
LINK_EXTERNAL("<a class=\"link\" rel=\"noopener noreferrer\" target=\"_blank\" href=\"${0}\">${1}</a>");
private final String html;
@ -71,105 +39,6 @@ public enum Html {
this.html = html;
}
/**
* Changes Minecraft color codes to HTML span elements with correct color class assignments.
*
* @param string String to replace Minecraft color codes from
* @return String with span elements.
*/
public static String swapColorCodesToSpan(String string) {
return swapColorCodesToSpan(string, string.contains("&sect;") ? "&sect;" : "§");
}
private static String swapColorCodesToSpan(String string, String splitWith) {
if (string == null) return null;
if (!string.contains(splitWith)) return string;
Html[] replacer = new Html[]{
Html.COLOR_0, Html.COLOR_1, Html.COLOR_2, Html.COLOR_3,
Html.COLOR_4, Html.COLOR_5, Html.COLOR_6, Html.COLOR_7,
Html.COLOR_8, Html.COLOR_9, Html.COLOR_A, Html.COLOR_B,
Html.COLOR_C, Html.COLOR_D, Html.COLOR_E, Html.COLOR_F
};
Map<Character, String> colorMap = new HashMap<>();
for (Html html : replacer) {
colorMap.put(Character.toLowerCase(html.name().charAt(6)), html.create());
colorMap.put('k', "");
colorMap.put('l', "");
colorMap.put('m', "");
colorMap.put('n', "");
colorMap.put('o', "");
}
StringBuilder result = new StringBuilder(string.length());
String[] split = string.split(splitWith);
// Skip first part if it does not start with §
boolean skipFirst = !string.startsWith(splitWith);
int placedSpans = 0;
int hexNumbersLeft = 0;
for (String part : split) {
if (part.isEmpty()) {
continue;
}
if (skipFirst) {
result.append(part);
skipFirst = false;
continue;
}
char colorChar = part.charAt(0);
// Deal with hex colors
if (hexNumbersLeft > 1) {
result.append(colorChar);
hexNumbersLeft--;
continue;
} else if (hexNumbersLeft == 1) {
result.append(colorChar).append(";\">").append(part.substring(1));
hexNumbersLeft--;
continue;
}
if (colorChar == 'r') {
appendEndTags(result, placedSpans);
placedSpans = 0; // Colors were reset
result.append(part.substring(1));
continue;
}
// Deal with hex colors
if (colorChar == 'x') {
result.append("<span style=\"color: #");
hexNumbersLeft = 6;
placedSpans++;
continue;
}
String replacement = colorMap.get(colorChar);
if (replacement != null) {
result.append(replacement).append(part.substring(1));
if (!replacement.isEmpty()) {
placedSpans++;
}
} else {
result.append(part);
}
}
appendEndTags(result, placedSpans);
return result.toString();
}
public static String separateWithDots(String... elements) {
TextStringBuilder builder = new TextStringBuilder();
builder.appendWithSeparators(elements, " &#x2022; ");
return builder.toString();
}
/**
* @return The HTML String
*/
@ -177,10 +46,6 @@ public enum Html {
return html;
}
private static void appendEndTags(StringBuilder result, int placedSpans) {
result.append("</span>".repeat(Math.max(0, placedSpans)));
}
/**
* @param replacements The replacement Strings
* @return The HTML String

View File

@ -1,115 +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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.rendering.html.structure;
import com.djrapitops.plan.extension.table.Table;
import org.apache.commons.lang3.math.NumberUtils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.List;
/**
* @deprecated Table html generation is to be done in frontend in the future.
*/
@Deprecated(since = "5.5")
public class DynamicHtmlTable implements HtmlTable {
private final Header[] headers;
private final List<Object[]> rows;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("MMM d yyyy, HH:mm");
public DynamicHtmlTable(Header[] headers, List<Object[]> rows) {
this.headers = headers;
this.rows = rows;
}
public DynamicHtmlTable(Table table) {
this(HtmlTable.mapToHeaders(table), table.getRows());
}
@Override
public String toHtml() {
return "<table class=\"table table-bordered table-striped table-hover player-plugin-table\" style=\"width: 100%\">" +
buildTableHeader() +
buildTableBody() +
"</table>";
}
private String buildTableHeader() {
StringBuilder builtHeader = new StringBuilder("<thead><tr>");
for (Header header : headers) {
builtHeader.append("<th>")
.append(header.getIcon().toHtml())
.append(' ')
.append(header.getText())
.append("</th>");
}
builtHeader.append("</tr></thead>");
return builtHeader.toString();
}
private String buildTableBody() {
StringBuilder builtBody = new StringBuilder();
builtBody.append("<tbody>");
if (rows.isEmpty()) {
appendRow(builtBody, "No Data");
}
for (Object[] row : rows) {
appendRow(builtBody, row);
}
return builtBody.append("</tbody>").toString();
}
private void appendRow(StringBuilder builtBody, Object... row) {
int headerLength = row.length - 1;
builtBody.append("<tr>");
for (int i = 0; i < headers.length; i++) {
try {
if (i > headerLength) {
builtBody.append("<td>-");
} else {
appendValue(builtBody, row[i]);
}
builtBody.append("</td>");
} catch (ClassCastException | ArrayIndexOutOfBoundsException e) {
throw new IllegalStateException("Invalid formatter given at index " + i + ": " + e.getMessage(), e);
}
}
builtBody.append("</tr>");
}
private void appendValue(StringBuilder builtBody, Object value) {
String valueString = value != null ? value.toString() : "-";
try {
long time = dateFormat.parse(valueString).getTime();
builtBody.append("<td data-order=\"").append(time).append("\">").append(valueString);
} catch (ParseException e) {
if (NumberUtils.isParsable(valueString)) {
builtBody.append("<td data-order=\"").append(valueString).append("\">").append(valueString);
} else {
// Removes non numbers from the value
String numbersInValue = valueString.replaceAll("\\D", "");
if (!numbersInValue.isEmpty()) {
builtBody.append("<td data-order=\"").append(numbersInValue).append("\">").append(valueString);
} else {
builtBody.append("<td>").append(valueString);
}
}
}
}
}

View File

@ -1,122 +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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.rendering.html.structure;
import com.djrapitops.plan.delivery.domain.datatransfer.extension.TableCellDto;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.html.Html;
import com.djrapitops.plan.delivery.rendering.html.icon.Color;
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
import com.djrapitops.plan.extension.table.Table;
import com.djrapitops.plan.extension.table.TableColumnFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @deprecated Table html generation is to be done in frontend in the future.
*/
@Deprecated(since = "5.5")
public interface HtmlTable {
static HtmlTable fromExtensionTable(Table table, com.djrapitops.plan.extension.icon.Color tableColor) {
return fromExtensionTable(table, Color.getByName(tableColor.name()).orElse(Color.NONE));
}
static HtmlTable fromExtensionTable(Table table, Color tableColor) {
if (table.getRows().size() > 10) {
return new DynamicHtmlTable(table);
} else {
return new HtmlTableWithColoredHeader(table, tableColor);
}
}
static Header[] mapToHeaders(Table table) {
ArrayList<Header> headers = new ArrayList<>();
com.djrapitops.plan.extension.icon.Icon[] icons = table.getIcons();
String[] columns = table.getColumns();
for (int i = 0; i < columns.length; i++) {
String column = columns[i];
if (column == null) {
break;
}
headers.add(new Header(Icon.fromExtensionIcon(icons[i]), column));
}
return headers.toArray(new Header[0]);
}
static List<TableCellDto[]> mapToRows(List<Object[]> rows, TableColumnFormat[] tableColumnFormats) {
return rows.stream()
.map(row -> {
List<TableCellDto> mapped = new ArrayList<>(row.length);
for (int i = 0; i < row.length; i++) {
Object value = row[i];
if (value == null) {
mapped.add(null);
} else {
TableColumnFormat format = tableColumnFormats[i];
mapped.add(new TableCellDto(applyFormat(format, value), value));
}
}
return mapped.toArray(new TableCellDto[0]);
})
.collect(Collectors.toList());
}
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(Html.swapColorCodesToSpan(value.toString())));
default:
return Html.swapColorCodesToSpan(value.toString());
}
} catch (Exception e) {
return Objects.toString(value);
}
}
String toHtml();
class Header {
private final Icon icon;
private final String text;
public Header(Icon icon, String text) {
this.icon = icon;
this.text = text;
}
public Icon getIcon() {
return icon;
}
public String getText() {
return text;
}
}
}

View File

@ -1,99 +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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.rendering.html.structure;
import com.djrapitops.plan.delivery.domain.datatransfer.extension.TableCellDto;
import com.djrapitops.plan.delivery.rendering.html.icon.Color;
import com.djrapitops.plan.extension.table.Table;
import java.util.List;
/**
* @deprecated Table html generation is to be done in frontend in the future.
*/
@Deprecated(since = "5.5")
public class HtmlTableWithColoredHeader implements HtmlTable {
private final Header[] headers;
private final Color headerColor;
private final List<TableCellDto[]> rows;
public HtmlTableWithColoredHeader(Header[] headers, Color headerColor, List<TableCellDto[]> rows) {
this.headers = headers;
this.headerColor = headerColor;
this.rows = rows;
}
public HtmlTableWithColoredHeader(Table table, Color headerColor) {
this(HtmlTable.mapToHeaders(table), headerColor, HtmlTable.mapToRows(table.getRows(), table.getTableColumnFormats()));
}
@Override
public String toHtml() {
return "<div class=\"scrollbar\">" +
"<table class=\"table table-striped\">" +
buildTableHeader() +
buildTableBody() +
"</table>" +
"</div>";
}
private String buildTableHeader() {
StringBuilder builtHeader = new StringBuilder("<thead class=\"" + headerColor.getBackgroundColorClass() + "\"><tr>");
for (Header header : headers) {
builtHeader.append("<th>")
.append(header.getIcon().toHtml())
.append(' ')
.append(header.getText())
.append("</th>");
}
builtHeader.append("</tr></thead>");
return builtHeader.toString();
}
private String buildTableBody() {
StringBuilder builtBody = new StringBuilder();
builtBody.append("<tbody>");
if (rows.isEmpty()) {
appendRow(builtBody, new TableCellDto("No Data"));
}
for (TableCellDto[] row : rows) {
appendRow(builtBody, row);
}
return builtBody.append("</tbody>").toString();
}
private void appendRow(StringBuilder builtBody, TableCellDto... row) {
int headerLength = row.length - 1;
builtBody.append("<tr>");
for (int i = 0; i < headers.length; i++) {
try {
if (i > headerLength) {
builtBody.append("<td>-");
} else {
builtBody.append("<td>");
TableCellDto cell = row[i];
builtBody.append(cell != null ? cell.getValue() : '-');
}
builtBody.append("</td>");
} catch (ClassCastException | ArrayIndexOutOfBoundsException e) {
throw new IllegalStateException("Invalid formatter given at index " + i + ": " + e.getMessage(), e);
}
}
builtBody.append("</tr>");
}
}

View File

@ -1,78 +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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.rendering.html.structure;
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
import org.apache.commons.lang3.StringUtils;
/**
* Html utility for creating navigation link html.
*
* @author AuroraLS3
*/
public class NavLink {
private final Icon icon;
private final String tabID;
private final String tabName;
private final boolean collapsed;
private NavLink(Icon icon, String tabID, String tabName, boolean collapsed) {
this.icon = icon;
this.tabID = tabID;
this.tabName = tabName;
this.collapsed = collapsed;
}
public static NavLink main(Icon icon, String tabName) {
return new NavLink(icon, null, tabName, false);
}
public static NavLink main(Icon icon, String tabID, String tabName) {
return new NavLink(icon, tabID, tabName, false);
}
public static NavLink collapsed(Icon icon, String tabName) {
return new NavLink(icon, null, tabName, true);
}
public static NavLink collapsed(Icon icon, String tabID, String tabName) {
return new NavLink(icon, tabID, tabName, true);
}
public static String format(String id) {
return StringUtils.replaceChars(StringUtils.lowerCase(id), ' ', '-');
}
public String toHtml() {
String usedId = getUsedTabId();
if (collapsed) {
return "<a class=\"collapse-item nav-button\" href=\"#tab-" + usedId + "\">" +
icon.toHtml() + ' ' +
tabName + "</a>";
}
return "<li class=\"nav-item nav-button\">" +
"<a class=\"nav-link\" href=\"#tab-" + usedId + "\">" +
icon.toHtml() +
"<span>" + tabName + "</span></a>" +
"</li>";
}
private String getUsedTabId() {
return format(tabID != null ? tabID : tabName);
}
}

View File

@ -1,92 +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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.rendering.html.structure;
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
import org.apache.commons.lang3.RegExUtils;
/**
* Represents a structural HTML element that has Tabs on the top.
*
* @author AuroraLS3
*/
public class TabsElement {
private final int pluginId;
private final Tab[] tabs;
public TabsElement(int pluginId, Tab... tabs) {
this.pluginId = pluginId;
this.tabs = tabs;
}
public String toHtmlFull() {
String[] navAndContent = toHtml();
return navAndContent[0] + navAndContent[1];
}
public String[] toHtml() {
StringBuilder nav = new StringBuilder();
StringBuilder content = new StringBuilder();
nav.append("<ul class=\"nav nav-tabs tab-nav-right\" role=\"tablist\">");
content.append("<div class=\"tab-content\">");
boolean first = true;
for (Tab tab : tabs) {
String id = tab.getId(pluginId);
String navHtml = tab.getNavHtml();
String contentHtml = tab.getContentHtml();
nav.append("<li role=\"presentation\" class=\"nav-item col-black\"")
.append("><a href=\"#").append(id).append("\" class=\"nav-link col-black").append(first ? " active" : "").append('"').append(" data-bs-toggle=\"tab\">")
.append(navHtml).append("</a></li>");
content.append("<div role=\"tabpanel\" class=\"tab-pane fade").append(first ? " in active show" : "")
.append("\" id=\"").append(id).append("\">")
.append(contentHtml).append("</div>");
first = false;
}
content.append("</div>");
nav.append("</ul>");
return new String[]{nav.toString(), content.toString()};
}
public static class Tab {
private final Icon icon;
private final String title;
private final String contentHtml;
public Tab(Icon icon, String title, String contentHtml) {
this.icon = icon;
this.title = title;
this.contentHtml = contentHtml;
}
public String getNavHtml() {
return icon.toHtml() + ' ' + title;
}
public String getContentHtml() {
return contentHtml;
}
public String getId(int pluginId) {
return "tab_" + pluginId + "_" + RegExUtils.removeAll(title, "[^a-zA-Z0-9]*").toLowerCase();
}
}
}

View File

@ -96,7 +96,7 @@ public class JSONFactory {
this.formatters = formatters;
}
public Map<String, Object> serverPlayersTableJSON(ServerUUID serverUUID) {
public PlayersTableJSONCreator serverPlayersTableJSON(ServerUUID serverUUID) {
Integer xMostRecentPlayers = config.get(DisplaySettings.PLAYERS_PER_SERVER_PAGE);
Long playtimeThreshold = config.get(TimeSettings.ACTIVE_PLAY_THRESHOLD);
boolean openPlayerLinksInNewTab = config.isTrue(DisplaySettings.OPEN_PLAYER_LINKS_IN_NEW_TAB);
@ -108,10 +108,10 @@ public class JSONFactory {
database.query(new ExtensionServerTableDataQuery(serverUUID, xMostRecentPlayers)),
openPlayerLinksInNewTab,
formatters, locale
).toJSONMap();
);
}
public Map<String, Object> networkPlayersTableJSON() {
public PlayersTableJSONCreator networkPlayersTableJSON() {
Integer xMostRecentPlayers = config.get(DisplaySettings.PLAYERS_PER_PLAYERS_PAGE);
Long playtimeThreshold = config.get(TimeSettings.ACTIVE_PLAY_THRESHOLD);
boolean openPlayerLinksInNewTab = config.isTrue(DisplaySettings.OPEN_PLAYER_LINKS_IN_NEW_TAB);
@ -147,7 +147,7 @@ public class JSONFactory {
openPlayerLinksInNewTab,
formatters, locale,
true // players page
).toJSONMap();
);
}
public List<RetentionData> playerRetentionAsJSONMap(ServerUUID serverUUID) {

View File

@ -23,7 +23,6 @@ import com.djrapitops.plan.delivery.domain.keys.PlayerKeys;
import com.djrapitops.plan.delivery.domain.mutators.*;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.html.Html;
import com.djrapitops.plan.delivery.rendering.json.graphs.Graphs;
import com.djrapitops.plan.delivery.rendering.json.graphs.line.PingGraph;
import com.djrapitops.plan.delivery.rendering.json.graphs.pie.WorldPie;
@ -51,7 +50,6 @@ import com.djrapitops.plan.storage.database.queries.objects.SessionQueries;
import com.djrapitops.plan.utilities.comparators.DateHolderRecentComparator;
import com.djrapitops.plan.utilities.java.Lists;
import com.djrapitops.plan.utilities.java.Maps;
import org.apache.commons.text.StringEscapeUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -372,7 +370,7 @@ public class PlayerJSONCreator {
List<Nickname> mapped = new ArrayList<>();
for (com.djrapitops.plan.delivery.domain.Nickname nickname : nicknames) {
mapped.add(new Nickname(
Html.swapColorCodesToSpan(StringEscapeUtils.escapeHtml4(nickname.getName())),
nickname.getName(),
serverNames.getOrDefault(nickname.getServerUUID(), nickname.getServerUUID().toString()),
dateFormatter.apply(nickname.getDate())
));

View File

@ -17,6 +17,11 @@
package com.djrapitops.plan.delivery.rendering.json;
import com.djrapitops.plan.delivery.domain.TablePlayer;
import com.djrapitops.plan.delivery.domain.datatransfer.PlayerListDto;
import com.djrapitops.plan.delivery.domain.datatransfer.TablePlayerDto;
import com.djrapitops.plan.delivery.domain.datatransfer.extension.ExtensionDescriptionDto;
import com.djrapitops.plan.delivery.domain.datatransfer.extension.ExtensionTabDataDto;
import com.djrapitops.plan.delivery.domain.datatransfer.extension.ExtensionValueDataDto;
import com.djrapitops.plan.delivery.domain.mutators.ActivityIndex;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
@ -33,6 +38,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import java.util.*;
import java.util.stream.Collectors;
/**
* Utility for creating jQuery Datatables JSON for a Players Table.
@ -109,6 +115,7 @@ public class PlayersTableJSONCreator {
}
}
@Deprecated
public Map<String, Object> toJSONMap() {
return Maps.builder(String.class, Object.class)
.put("columns", createColumnHeaders())
@ -116,6 +123,41 @@ public class PlayersTableJSONCreator {
.build();
}
public PlayerListDto toPlayerList() {
return new PlayerListDto(toPlayers(), getExtensionDescriptors());
}
private List<TablePlayerDto> toPlayers() {
return players.stream()
.map(player -> TablePlayerDto.builder()
.withUuid(player.getPlayerUUID())
.withName(player.getName().orElseGet(() -> player.getPlayerUUID().toString()))
.withSessionCount((long) player.getSessionCount().orElse(0))
.withPlaytimeActive(player.getActivePlaytime().orElse(null))
.withLastSeen(player.getLastSeen().orElse(null))
.withRegistered(player.getRegistered().orElse(null))
.withCountry(player.getGeolocation().orElse(null))
.withExtensionValues(mapToExtensionValues(extensionData.get(player.getPlayerUUID())))
.build()
).collect(Collectors.toList());
}
private List<ExtensionDescriptionDto> getExtensionDescriptors() {
return extensionDescriptions.stream().map(ExtensionDescriptionDto::new).collect(Collectors.toList());
}
private Map<String, ExtensionValueDataDto> mapToExtensionValues(ExtensionTabData extensionTabData) {
if (extensionTabData == null) return Collections.emptyMap();
Map<String, ExtensionValueDataDto> values = new HashMap<>();
List<ExtensionDescription> descriptions = extensionTabData.getDescriptions();
for (ExtensionDescription description : descriptions) {
String name = description.getName();
ExtensionTabDataDto.mapToValue(extensionTabData, name).ifPresent(value -> values.put(name, value));
}
return values;
}
private List<Map<String, Object>> createData() {
List<Map<String, Object>> dataJson = new ArrayList<>();
@ -196,7 +238,7 @@ public class PlayersTableJSONCreator {
}
// If it's a String add a String, otherwise the player has no value for this extension provider.
String stringValue = tabData.getString(key).map(ExtensionStringData::getFormattedValue).orElse("-");
String stringValue = tabData.getString(key).map(ExtensionStringData::getValue).orElse("-");
putDataEntry(dataJson, stringValue, stringValue, key);
}

View File

@ -17,7 +17,6 @@
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.delivery.formatting.PlaceholderReplacer;
import com.djrapitops.plan.delivery.rendering.html.Contributors;
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
import com.djrapitops.plan.settings.theme.Theme;
import com.djrapitops.plan.utilities.java.UnaryChain;
@ -63,10 +62,7 @@ public class ErrorMessagePage implements Page {
placeholders.put("title", icon.toHtml() + " " + errorTitle);
placeholders.put("titleText", errorTitle);
placeholders.put("paragraph", errorMsg);
placeholders.put("versionButton", versionChecker.getUpdateButton().orElse(versionChecker.getCurrentVersionButton()));
placeholders.put("version", versionChecker.getCurrentVersion());
placeholders.put("updateModal", versionChecker.getUpdateModal());
placeholders.put("contributors", Contributors.generateContributorHtml());
return UnaryChain.of(template)
.chain(theme::replaceThemeColors)
.chain(placeholders::apply)

View File

@ -17,7 +17,6 @@
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.delivery.formatting.PlaceholderReplacer;
import com.djrapitops.plan.delivery.rendering.html.Contributors;
import com.djrapitops.plan.delivery.rendering.html.Html;
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
import com.djrapitops.plan.exceptions.ExceptionWithContext;
@ -58,10 +57,7 @@ public class InternalErrorPage implements Page {
placeholders.put("title", Icon.called("bug") + " 500 Internal Error occurred");
placeholders.put("titleText", "500 Internal Error occurred");
placeholders.put("paragraph", createContent());
placeholders.put("versionButton", versionChecker.getUpdateButton().orElse(versionChecker.getCurrentVersionButton()));
placeholders.put("version", versionChecker.getCurrentVersion());
placeholders.put("updateModal", versionChecker.getUpdateModal());
placeholders.put("contributors", Contributors.generateContributorHtml());
return placeholders.apply(template);
}

View File

@ -1,76 +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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.delivery.formatting.PlaceholderReplacer;
import com.djrapitops.plan.delivery.web.resource.WebResource;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.theme.Theme;
import com.djrapitops.plan.utilities.java.UnaryChain;
import com.djrapitops.plan.version.VersionChecker;
/**
* Html String generator for /login and /register page.
*
* @author AuroraLS3
*/
public class LoginPage implements Page {
private final WebResource template;
private final ServerInfo serverInfo;
private final Locale locale;
private final Theme theme;
private final VersionChecker versionChecker;
LoginPage(
WebResource htmlTemplate,
ServerInfo serverInfo,
Locale locale,
Theme theme,
VersionChecker versionChecker
) {
this.template = htmlTemplate;
this.serverInfo = serverInfo;
this.locale = locale;
this.theme = theme;
this.versionChecker = versionChecker;
}
@Override
public long lastModified() {
return template.getLastModified().orElseGet(System::currentTimeMillis);
}
@Override
public String toHtml() {
PlaceholderReplacer placeholders = new PlaceholderReplacer();
placeholders.put("command", getCommand());
placeholders.put("version", versionChecker.getCurrentVersion());
return UnaryChain.of(template.asString())
.chain(theme::replaceThemeColors)
.chain(placeholders::apply)
.chain(locale::replaceLanguageInHtml)
.apply();
}
private String getCommand() {
if (serverInfo.getServer().isProxy()) return "planproxy";
return "plan";
}
}

View File

@ -1,141 +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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.component.ComponentSvc;
import com.djrapitops.plan.delivery.domain.container.CachingSupplier;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.formatting.PlaceholderReplacer;
import com.djrapitops.plan.delivery.rendering.html.Contributors;
import com.djrapitops.plan.delivery.webserver.cache.DataID;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
import com.djrapitops.plan.extension.implementation.results.ExtensionData;
import com.djrapitops.plan.extension.implementation.storage.queries.ExtensionServerDataQuery;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.ProxySettings;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import com.djrapitops.plan.settings.locale.Locale;
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.utilities.java.UnaryChain;
import com.djrapitops.plan.version.VersionChecker;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
/**
* Html String generator for /network page.
*
* @author AuroraLS3
*/
public class NetworkPage implements Page {
private final String templateHtml;
private final DBSystem dbSystem;
private final VersionChecker versionChecker;
private final PlanConfig config;
private final Theme theme;
private final ServerInfo serverInfo;
private final JSONStorage jsonStorage;
private final Formatters formatters;
private final Locale locale;
private final ComponentSvc componentSvc;
NetworkPage(
String templateHtml,
DBSystem dbSystem,
VersionChecker versionChecker,
PlanConfig config,
Theme theme,
ServerInfo serverInfo,
JSONStorage jsonStorage,
Formatters formatters,
Locale locale,
ComponentSvc componentSvc
) {
this.templateHtml = templateHtml;
this.dbSystem = dbSystem;
this.versionChecker = versionChecker;
this.config = config;
this.theme = theme;
this.serverInfo = serverInfo;
this.jsonStorage = jsonStorage;
this.formatters = formatters;
this.locale = locale;
this.componentSvc = componentSvc;
}
@Override
public String toHtml() {
PlaceholderReplacer placeholders = new PlaceholderReplacer();
ServerUUID serverUUID = serverInfo.getServerUUID();
placeholders.put("networkDisplayName", config.get(ProxySettings.NETWORK_NAME));
placeholders.put("serverName", config.get(ProxySettings.NETWORK_NAME));
placeholders.put("serverUUID", serverUUID.toString());
placeholders.put("refreshBarrier", config.get(WebserverSettings.REDUCED_REFRESH_BARRIER));
placeholders.put("gmPieColors", theme.getValue(ThemeVal.GRAPH_GM_PIE));
placeholders.put("playersGraphColor", theme.getValue(ThemeVal.GRAPH_PLAYERS_ONLINE));
placeholders.put("worldMapColLow", theme.getValue(ThemeVal.WORLD_MAP_LOW));
placeholders.put("worldMapColHigh", theme.getValue(ThemeVal.WORLD_MAP_HIGH));
placeholders.put("maxPingColor", theme.getValue(ThemeVal.GRAPH_MAX_PING));
placeholders.put("minPingColor", theme.getValue(ThemeVal.GRAPH_MIN_PING));
placeholders.put("avgPingColor", theme.getValue(ThemeVal.GRAPH_AVG_PING));
placeholders.put("timeZone", config.getTimeZoneOffsetHours());
placeholders.put("versionButton", versionChecker.getUpdateButton().orElse(versionChecker.getCurrentVersionButton()));
placeholders.put("version", versionChecker.getCurrentVersion());
placeholders.put("updateModal", versionChecker.getUpdateModal());
placeholders.put("contributors", Contributors.generateContributorHtml());
CachingSupplier<ServerPluginTabs> pluginTabs = new CachingSupplier<>(() -> {
List<ExtensionData> extensionData = dbSystem.getDatabase().query(new ExtensionServerDataQuery(serverUUID));
return new ServerPluginTabs(extensionData, formatters, componentSvc);
});
long after = System.currentTimeMillis() - config.get(WebserverSettings.REDUCED_REFRESH_BARRIER);
String navIdentifier = DataID.EXTENSION_NAV.of(serverUUID);
String tabIdentifier = DataID.EXTENSION_TABS.of(serverUUID);
String nav = jsonStorage.fetchJsonMadeAfter(navIdentifier, after).orElseGet(() -> {
jsonStorage.invalidateOlder(navIdentifier, after);
return jsonStorage.storeJson(navIdentifier, pluginTabs.get().getNav());
}).json;
String tabs = jsonStorage.fetchJsonMadeAfter(tabIdentifier, after).orElseGet(() -> {
jsonStorage.invalidateOlder(tabIdentifier, after);
return jsonStorage.storeJson(tabIdentifier, pluginTabs.get().getTabs());
}).json;
PlaceholderReplacer pluginPlaceholders = new PlaceholderReplacer();
pluginPlaceholders.put("networkDisplayName", config.get(ProxySettings.NETWORK_NAME));
pluginPlaceholders.put("serverName", config.get(ProxySettings.NETWORK_NAME));
pluginPlaceholders.put("serverUUID", serverUUID.toString());
pluginPlaceholders.put("navPluginsTabs", nav);
pluginPlaceholders.put("tabsPlugins", StringUtils.remove(tabs, "${backButton}"));
return UnaryChain.of(templateHtml)
.chain(theme::replaceThemeColors)
.chain(placeholders::apply)
.chain(pluginPlaceholders::apply)
.chain(locale::replaceLanguageInHtml)
.apply();
}
}

View File

@ -16,26 +16,15 @@
*/
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.component.ComponentSvc;
import com.djrapitops.plan.delivery.domain.container.PlayerContainer;
import com.djrapitops.plan.delivery.formatting.Formatters;
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.delivery.webserver.Addresses;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
import com.djrapitops.plan.extension.implementation.results.ExtensionData;
import com.djrapitops.plan.extension.implementation.storage.queries.ExtensionPlayerDataQuery;
import com.djrapitops.plan.identification.Server;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.settings.locale.Locale;
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.objects.ServerQueries;
import com.djrapitops.plan.storage.file.PlanFiles;
import com.djrapitops.plan.storage.file.PublicHtmlFiles;
@ -47,7 +36,6 @@ import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.*;
/**
* Factory for creating different {@link Page} objects.
@ -60,51 +48,31 @@ public class PageFactory {
private final Lazy<VersionChecker> versionChecker;
private final Lazy<PlanFiles> files;
private final Lazy<PublicHtmlFiles> publicHtmlFiles;
private final Lazy<PlanConfig> config;
private final Lazy<Theme> theme;
private final Lazy<DBSystem> dbSystem;
private final Lazy<ServerInfo> serverInfo;
private final Lazy<JSONStorage> jsonStorage;
private final Lazy<Formatters> formatters;
private final Lazy<Locale> locale;
private final Lazy<ComponentSvc> componentService;
private final Lazy<Addresses> addresses;
private static final String ERROR_HTML_FILE = "error.html";
@Inject
public PageFactory(
Lazy<VersionChecker> versionChecker,
Lazy<PlanFiles> files,
Lazy<PublicHtmlFiles> publicHtmlFiles, Lazy<PlanConfig> config,
Lazy<PublicHtmlFiles> publicHtmlFiles,
Lazy<Theme> theme,
Lazy<DBSystem> dbSystem,
Lazy<ServerInfo> serverInfo,
Lazy<JSONStorage> jsonStorage,
Lazy<Formatters> formatters,
Lazy<Locale> locale,
Lazy<ComponentSvc> componentService,
Lazy<Addresses> addresses
) {
this.versionChecker = versionChecker;
this.files = files;
this.publicHtmlFiles = publicHtmlFiles;
this.config = config;
this.theme = theme;
this.dbSystem = dbSystem;
this.serverInfo = serverInfo;
this.jsonStorage = jsonStorage;
this.formatters = formatters;
this.locale = locale;
this.componentService = componentService;
this.addresses = addresses;
}
public Page playersPage() throws IOException {
if (config.get().isFalse(PluginSettings.LEGACY_FRONTEND)) {
return reactPage();
}
return new PlayersPage(getResourceAsString("players.html"), versionChecker.get(),
config.get(), theme.get(), serverInfo.get());
return reactPage();
}
public Page reactPage() throws IOException {
@ -134,100 +102,24 @@ public class PageFactory {
* @throws IOException If the template files can not be read.
*/
public Page serverPage(ServerUUID serverUUID) throws IOException {
Server server = dbSystem.get().getDatabase().query(ServerQueries.fetchServerMatchingIdentifier(serverUUID))
.orElseThrow(() -> new NotFoundException("Server not found in the database"));
if (config.get().isFalse(PluginSettings.LEGACY_FRONTEND)) {
return reactPage();
if (dbSystem.get().getDatabase().query(ServerQueries.fetchServerMatchingIdentifier(serverUUID)).isEmpty()) {
throw new NotFoundException("Server not found in the database");
}
return new ServerPage(
getResourceAsString("server.html"),
server,
config.get(),
theme.get(),
versionChecker.get(),
dbSystem.get(),
serverInfo.get(),
jsonStorage.get(),
formatters.get(),
locale.get(),
componentService.get()
);
return reactPage();
}
public Page playerPage(PlayerContainer player) throws IOException {
if (config.get().isFalse(PluginSettings.LEGACY_FRONTEND)) {
return reactPage();
}
return new PlayerPage(
getResourceAsString("player.html"), player,
versionChecker.get(),
config.get(),
this,
theme.get(),
formatters.get(),
serverInfo.get(),
locale.get()
);
}
public PlayerPluginTab inspectPluginTabs(UUID playerUUID) {
Database database = dbSystem.get().getDatabase();
Map<ServerUUID, List<ExtensionData>> extensionPlayerData = database.query(new ExtensionPlayerDataQuery(playerUUID));
if (extensionPlayerData.isEmpty()) {
return new PlayerPluginTab("", Collections.emptyList(), formatters.get(), componentService.get());
}
List<PlayerPluginTab> playerPluginTabs = new ArrayList<>();
for (Map.Entry<ServerUUID, Server> entry : database.query(ServerQueries.fetchPlanServerInformation()).entrySet()) {
ServerUUID serverUUID = entry.getKey();
String serverName = entry.getValue().getIdentifiableName();
List<ExtensionData> ofServer = extensionPlayerData.get(serverUUID);
if (ofServer == null) {
continue;
}
playerPluginTabs.add(new PlayerPluginTab(serverName, ofServer, formatters.get(), componentService.get()));
}
StringBuilder navs = new StringBuilder();
StringBuilder tabs = new StringBuilder();
playerPluginTabs.stream().sorted().forEach(tab -> {
navs.append(tab.getNav());
tabs.append(tab.getTab());
});
return new PlayerPluginTab(navs.toString(), tabs.toString(), componentService.get());
public Page playerPage() throws IOException {
return reactPage();
}
public Page networkPage() throws IOException {
if (config.get().isFalse(PluginSettings.LEGACY_FRONTEND)) {
return reactPage();
}
return new NetworkPage(getResourceAsString("network.html"),
dbSystem.get(),
versionChecker.get(),
config.get(),
theme.get(),
serverInfo.get(),
jsonStorage.get(),
formatters.get(),
locale.get(),
componentService.get()
);
return reactPage();
}
public Page internalErrorPage(String message, @Untrusted Throwable error) {
try {
return new InternalErrorPage(
getResourceAsString("error.html"), message, error,
getResourceAsString(ERROR_HTML_FILE), message, error,
versionChecker.get());
} catch (IOException noParse) {
return () -> "Error occurred: " + error.toString() +
@ -238,13 +130,13 @@ public class PageFactory {
public Page errorPage(String title, String error) throws IOException {
return new ErrorMessagePage(
getResourceAsString("error.html"), title, error,
getResourceAsString(ERROR_HTML_FILE), title, error,
versionChecker.get(), theme.get());
}
public Page errorPage(Icon icon, String title, String error) throws IOException {
return new ErrorMessagePage(
getResourceAsString("error.html"), icon, title, error, theme.get(), versionChecker.get());
getResourceAsString(ERROR_HTML_FILE), icon, title, error, theme.get(), versionChecker.get());
}
public String getResourceAsString(String name) throws IOException {
@ -268,29 +160,15 @@ public class PageFactory {
}
public Page loginPage() throws IOException {
if (config.get().isFalse(PluginSettings.LEGACY_FRONTEND)) {
return reactPage();
}
return new LoginPage(getResource("login.html"), serverInfo.get(), locale.get(), theme.get(), versionChecker.get());
return reactPage();
}
public Page registerPage() throws IOException {
if (config.get().isFalse(PluginSettings.LEGACY_FRONTEND)) {
return reactPage();
}
return new LoginPage(getResource("register.html"), serverInfo.get(), locale.get(), theme.get(), versionChecker.get());
return reactPage();
}
public Page queryPage() throws IOException {
if (config.get().isFalse(PluginSettings.LEGACY_FRONTEND)) {
return reactPage();
}
return new QueryPage(
getResourceAsString("query.html"),
locale.get(), theme.get(), versionChecker.get()
);
return reactPage();
}
public Page errorsPage() throws IOException {

View File

@ -1,127 +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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.delivery.domain.container.PlayerContainer;
import com.djrapitops.plan.delivery.domain.keys.PlayerKeys;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.formatting.PlaceholderReplacer;
import com.djrapitops.plan.delivery.rendering.html.Contributors;
import com.djrapitops.plan.delivery.rendering.html.Html;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.DisplaySettings;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.theme.Theme;
import com.djrapitops.plan.settings.theme.ThemeVal;
import com.djrapitops.plan.utilities.java.UnaryChain;
import com.djrapitops.plan.version.VersionChecker;
import java.util.UUID;
/**
* Html String generator for /player page.
*
* @author AuroraLS3
*/
public class PlayerPage implements Page {
private final String templateHtml;
private final PlayerContainer player;
private final VersionChecker versionChecker;
private final PlanConfig config;
private final PageFactory pageFactory;
private final Theme theme;
private final ServerInfo serverInfo;
private final Formatter<Long> clockLongFormatter;
private final Formatter<Long> secondLongFormatter;
private final Locale locale;
PlayerPage(
String templateHtml,
PlayerContainer player,
VersionChecker versionChecker,
PlanConfig config,
PageFactory pageFactory,
Theme theme,
Formatters formatters,
ServerInfo serverInfo,
Locale locale
) {
this.templateHtml = templateHtml;
this.player = player;
this.versionChecker = versionChecker;
this.config = config;
this.pageFactory = pageFactory;
this.theme = theme;
this.serverInfo = serverInfo;
clockLongFormatter = formatters.clockLong();
secondLongFormatter = formatters.secondLong();
this.locale = locale;
}
@Override
public String toHtml() {
if (player.getValue(PlayerKeys.REGISTERED).isEmpty()) {
throw new IllegalStateException("Player is not registered");
}
return createFor(player);
}
public String createFor(PlayerContainer player) {
long now = System.currentTimeMillis();
UUID playerUUID = player.getUnsafe(PlayerKeys.UUID);
PlaceholderReplacer placeholders = new PlaceholderReplacer();
placeholders.put("refresh", clockLongFormatter.apply(now));
placeholders.put("refreshFull", secondLongFormatter.apply(now));
placeholders.put("versionButton", versionChecker.getUpdateButton().orElse(versionChecker.getCurrentVersionButton()));
placeholders.put("version", versionChecker.getCurrentVersion());
placeholders.put("updateModal", versionChecker.getUpdateModal());
String playerName = player.getValue(PlayerKeys.NAME).orElse(playerUUID.toString());
placeholders.put("playerName", playerName);
placeholders.put("playerUUID", playerUUID);
placeholders.put("playerHeadUrl", config.get(DisplaySettings.PLAYER_HEAD_IMG_URL));
placeholders.put("timeZone", config.getTimeZoneOffsetHours());
placeholders.put("gmPieColors", theme.getValue(ThemeVal.GRAPH_GM_PIE));
placeholders.put("contributors", Contributors.generateContributorHtml());
PlaceholderReplacer pluginPlaceholders = new PlaceholderReplacer();
PlayerPluginTab pluginTabs = pageFactory.inspectPluginTabs(playerUUID);
pluginPlaceholders.put("playerName", playerName);
pluginPlaceholders.put("backButton", (serverInfo.getServer().isProxy() ? Html.BACK_BUTTON_NETWORK : Html.BACK_BUTTON_SERVER).create());
pluginPlaceholders.put("navPluginsTabs", pluginTabs.getNav());
pluginPlaceholders.put("pluginsTabs", pluginTabs.getTab());
return UnaryChain.of(templateHtml)
.chain(theme::replaceThemeColors)
.chain(placeholders::apply)
.chain(pluginPlaceholders::apply)
.chain(locale::replaceLanguageInHtml)
.apply();
}
}

View File

@ -1,252 +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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.component.ComponentSvc;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
import com.djrapitops.plan.delivery.rendering.html.structure.NavLink;
import com.djrapitops.plan.delivery.rendering.html.structure.TabsElement;
import com.djrapitops.plan.extension.ElementOrder;
import com.djrapitops.plan.extension.FormatType;
import com.djrapitops.plan.extension.implementation.TabInformation;
import com.djrapitops.plan.extension.implementation.results.*;
import java.util.*;
/**
* Responsible for generating /player page plugin tabs based on DataExtension API data.
*
* @author AuroraLS3
*/
public class PlayerPluginTab implements Comparable<PlayerPluginTab> {
private String serverName;
private List<ExtensionData> playerData;
private Map<FormatType, Formatter<Long>> numberFormatters;
private Formatter<Double> decimalFormatter;
private Formatter<Double> percentageFormatter;
private String nav;
private String tab;
private boolean hasWideTable;
private final ComponentSvc componentSvc;
public PlayerPluginTab(String nav, String tab, ComponentSvc componentSvc) {
this.nav = nav;
this.tab = tab;
this.componentSvc = componentSvc;
}
public PlayerPluginTab(
String serverName,
List<ExtensionData> playerData,
Formatters formatters,
ComponentSvc componentSvc
) {
this.serverName = serverName;
this.playerData = playerData;
this.componentSvc = componentSvc;
numberFormatters = new EnumMap<>(FormatType.class);
numberFormatters.put(FormatType.DATE_SECOND, formatters.secondLong());
numberFormatters.put(FormatType.DATE_YEAR, formatters.yearLong());
numberFormatters.put(FormatType.TIME_MILLISECONDS, formatters.timeAmount());
numberFormatters.put(FormatType.NONE, Object::toString);
this.decimalFormatter = formatters.decimals();
this.percentageFormatter = formatters.percentage();
hasWideTable = false;
generate();
}
public String getNav() {
return nav;
}
public String getTab() {
return tab;
}
private void generate() {
if (playerData.isEmpty()) {
nav = NavLink.collapsed(Icon.called("cubes").build(), "plugins-" + serverName + " (No Data)", serverName + " (No Data)").toHtml();
tab = wrapInWideTab(
serverName + " (No Data)",
"<div class=\"card\"><div class=\"card-body\"><p>No Extension Data</p></div></div>"
);
} else {
nav = NavLink.collapsed(Icon.called("cubes").build(), "plugins-" + serverName, serverName).toHtml();
tab = generatePageTab();
}
}
private String generatePageTab() {
Collections.sort(playerData);
StringBuilder tabBuilder = new StringBuilder();
for (ExtensionData datum : playerData) {
ExtensionInformation extensionInformation = datum.getExtensionInformation();
boolean onlyGeneric = datum.hasOnlyGenericTab();
String tabsElement;
if (onlyGeneric) {
ExtensionTabData genericTabData = datum.getTabs().get(0);
tabsElement = buildContentHtml(genericTabData);
} else {
tabsElement = new TabsElement(
datum.getPluginID(),
datum.getTabs().stream().map(this::wrapToTabElementTab).toArray(TabsElement.Tab[]::new)
).toHtmlFull();
}
tabBuilder.append(wrapInContainer(extensionInformation, tabsElement));
}
return wrapInCardColumnsTab(serverName, tabBuilder.toString());
}
private String wrapInWideTab(String serverName, String content) {
return "<div class=\"tab\" id=\"" + NavLink.format("plugins-" + serverName) + "\"><div class=\"container-fluid mt-4\">" +
// Page heading
"<div class=\"d-sm-flex align-items-center justify-content-between mb-4\">" +
"<h1 class=\"h3 mb-0 text-gray-800\"><i class=\"sidebar-toggler fa fa-fw fa-bars\"></i>" + serverName + " &middot; Plugins Overview</h1>${backButton}" +
"</div>" +
// End Page heading
"<div class=\"row\"><div class=\"col-md-12\">" + content + "</div></div></div></div>";
}
private String wrapInCardColumnsTab(String serverName, String content) {
return "<div class=\"tab\" id=\"" + NavLink.format("plugins-" + serverName) + "\"><div class=\"container-fluid mt-4\">" +
// Page heading
"<div class=\"d-sm-flex align-items-center justify-content-between mb-4\">" +
"<h1 class=\"h3 mb-0 text-gray-800\"><i class=\"sidebar-toggler fa fa-fw fa-bars\"></i>" + serverName + " &middot; Plugins Overview</h1>${backButton}" +
"</div>" +
// End Page heading
"<div class=\"row\" data-masonry='{\"percentPosition\": true}'>" + content + "</div></div></div>";
}
private TabsElement.Tab wrapToTabElementTab(ExtensionTabData tabData) {
TabInformation tabInformation = tabData.getTabInformation();
String tabContentHtml = buildContentHtml(tabData);
String tabName = tabInformation.getTabName();
return tabName.isEmpty() ? new TabsElement.Tab(Icon.called("info-circle").build(), "General", tabContentHtml)
: new TabsElement.Tab(Icon.fromExtensionIcon(tabInformation.getTabIcon()), tabName, tabContentHtml);
}
private String buildContentHtml(ExtensionTabData tabData) {
TabInformation tabInformation = tabData.getTabInformation();
List<ElementOrder> order = tabInformation.getTabElementOrder();
String values = buildValuesHtml(tabData);
String valuesHtml = values.isEmpty() ? "" : "<div class=\"card-body\">" + values + "</div>";
String tablesHtml = buildTablesHtml(tabData);
StringBuilder builder = new StringBuilder();
for (ElementOrder ordering : order) {
switch (ordering) {
case VALUES:
builder.append(valuesHtml);
break;
case TABLE:
builder.append(tablesHtml);
break;
default:
break;
}
}
return builder.toString();
}
private String buildTablesHtml(ExtensionTabData tabData) {
StringBuilder builder = new StringBuilder();
for (ExtensionTableData tableData : tabData.getTableData()) {
if (tableData.isWideTable()) {
hasWideTable = true;
}
builder.append(tableData.getHtmlTable().toHtml());
}
return builder.toString();
}
private String buildValuesHtml(ExtensionTabData tabData) {
StringBuilder builder = new StringBuilder();
for (String key : tabData.getValueOrder()) {
tabData.getBoolean(key).ifPresent(data -> append(builder, data.getDescription(), data.getFormattedValue()));
tabData.getDouble(key).ifPresent(data -> append(builder, data.getDescription(), data.getFormattedValue(decimalFormatter)));
tabData.getPercentage(key).ifPresent(data -> append(builder, data.getDescription(), data.getFormattedValue(percentageFormatter)));
tabData.getNumber(key).ifPresent(data -> append(builder, data.getDescription(), data.getFormattedValue(numberFormatters.get(data.getFormatType()))));
tabData.getString(key).ifPresent(data -> append(builder, data.getDescription(), data.getFormattedValue()));
tabData.getComponent(key).ifPresent(data -> append(builder, data.getDescription(), data.getHtmlValue(componentSvc)));
}
return builder.toString();
}
private void append(StringBuilder builder, ExtensionDescription description, String formattedValue) {
Optional<String> textDescription = description.getDescription();
if (textDescription.isPresent()) {
builder.append("<p title=\"").append(textDescription.get()).append("\">");
} else {
builder.append("<p>");
}
builder.append(Icon.fromExtensionIcon(description.getIcon()))
.append(' ').append(description.getText()).append("<span class=\"float-end\"><b>").append(formattedValue).append("</b></span></p>");
}
private String wrapInContainer(ExtensionInformation information, String tabsElement) {
String colWidth = hasWideTable ? "col-md-8 col-lg-8" : "col-md-4 col-lg-4";
// TODO move large tables to their own tabs
return "<div class=\"col-lg-6 col-xxl-4\">" +
"<div class=\"card shadow mb-4\">" +
"<div class=\"card-header py-3\">" +
"<h6 class=\"m-0 fw-bold col-black\">" + Icon.fromExtensionIcon(information.getIcon()) + ' ' + information.getPluginName() + "</h6>" +
"</div>" +
tabsElement +
"</div>" +
"</div>";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PlayerPluginTab)) return false;
PlayerPluginTab that = (PlayerPluginTab) o;
return Objects.equals(serverName, that.serverName) &&
Objects.equals(nav, that.nav);
}
@Override
public int hashCode() {
return Objects.hash(serverName, nav);
}
@Override
public int compareTo(PlayerPluginTab other) {
return String.CASE_INSENSITIVE_ORDER.compare(this.serverName, other.serverName);
}
}

View File

@ -1,73 +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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.delivery.formatting.PlaceholderReplacer;
import com.djrapitops.plan.delivery.rendering.html.Contributors;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.settings.config.paths.ProxySettings;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import com.djrapitops.plan.settings.theme.Theme;
import com.djrapitops.plan.version.VersionChecker;
/**
* Html String generator for /players page.
*
* @author AuroraLS3
*/
public class PlayersPage implements Page {
private final String templateHtml;
private final VersionChecker versionChecker;
private final PlanConfig config;
private final Theme theme;
private final ServerInfo serverInfo;
PlayersPage(
String templateHtml,
VersionChecker versionChecker,
PlanConfig config,
Theme theme,
ServerInfo serverInfo
) {
this.templateHtml = templateHtml;
this.versionChecker = versionChecker;
this.config = config;
this.theme = theme;
this.serverInfo = serverInfo;
}
@Override
public String toHtml() {
PlaceholderReplacer placeholders = new PlaceholderReplacer();
placeholders.put("refreshBarrier", config.get(WebserverSettings.REDUCED_REFRESH_BARRIER));
placeholders.put("versionButton", versionChecker.getUpdateButton().orElse(versionChecker.getCurrentVersionButton()));
placeholders.put("version", versionChecker.getCurrentVersion());
placeholders.put("updateModal", versionChecker.getUpdateModal());
placeholders.put("contributors", Contributors.generateContributorHtml());
if (serverInfo.getServer().isProxy()) {
placeholders.put("networkName", config.get(ProxySettings.NETWORK_NAME));
} else {
placeholders.put("networkName", config.get(PluginSettings.SERVER_NAME));
}
return placeholders.apply(theme.replaceThemeColors(templateHtml));
}
}

View File

@ -1,64 +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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.delivery.formatting.PlaceholderReplacer;
import com.djrapitops.plan.delivery.rendering.html.Contributors;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.theme.Theme;
import com.djrapitops.plan.utilities.java.UnaryChain;
import com.djrapitops.plan.version.VersionChecker;
/**
* Page to display error stacktrace.
*
* @author AuroraLS3
*/
public class QueryPage implements Page {
private final String template;
private final Locale locale;
private final Theme theme;
private final VersionChecker versionChecker;
public QueryPage(
String template,
Locale locale,
Theme theme,
VersionChecker versionChecker
) {
this.template = template;
this.locale = locale;
this.theme = theme;
this.versionChecker = versionChecker;
}
@Override
public String toHtml() {
PlaceholderReplacer placeholders = new PlaceholderReplacer();
placeholders.put("versionButton", versionChecker.getUpdateButton().orElse(versionChecker.getCurrentVersionButton()));
placeholders.put("version", versionChecker.getCurrentVersion());
placeholders.put("updateModal", versionChecker.getUpdateModal());
placeholders.put("contributors", Contributors.generateContributorHtml());
return UnaryChain.of(template)
.chain(theme::replaceThemeColors)
.chain(placeholders::apply)
.chain(locale::replaceLanguageInHtml)
.apply();
}
}

View File

@ -1,137 +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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.component.ComponentSvc;
import com.djrapitops.plan.delivery.domain.container.CachingSupplier;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.formatting.PlaceholderReplacer;
import com.djrapitops.plan.delivery.rendering.html.Contributors;
import com.djrapitops.plan.delivery.rendering.html.Html;
import com.djrapitops.plan.delivery.webserver.cache.DataID;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
import com.djrapitops.plan.extension.implementation.results.ExtensionData;
import com.djrapitops.plan.extension.implementation.storage.queries.ExtensionServerDataQuery;
import com.djrapitops.plan.identification.Server;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import com.djrapitops.plan.settings.locale.Locale;
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.utilities.java.UnaryChain;
import com.djrapitops.plan.version.VersionChecker;
import java.util.List;
/**
* Html String generator for /server page.
*
* @author AuroraLS3
*/
public class ServerPage implements Page {
private final String templateHtml;
private final Server server;
private final PlanConfig config;
private final Theme theme;
private final VersionChecker versionChecker;
private final DBSystem dbSystem;
private final ServerInfo serverInfo;
private final JSONStorage jsonStorage;
private final Formatters formatters;
private final Locale locale;
private final ComponentSvc componentSvc;
ServerPage(
String templateHtml, Server server,
PlanConfig config,
Theme theme,
VersionChecker versionChecker,
DBSystem dbSystem,
ServerInfo serverInfo,
JSONStorage jsonStorage,
Formatters formatters,
Locale locale,
ComponentSvc componentSvc
) {
this.templateHtml = templateHtml;
this.server = server;
this.config = config;
this.theme = theme;
this.versionChecker = versionChecker;
this.dbSystem = dbSystem;
this.serverInfo = serverInfo;
this.jsonStorage = jsonStorage;
this.formatters = formatters;
this.locale = locale;
this.componentSvc = componentSvc;
}
@Override
public String toHtml() {
PlaceholderReplacer placeholders = new PlaceholderReplacer();
ServerUUID serverUUID = server.getUuid();
placeholders.put("serverUUID", serverUUID.toString());
placeholders.put("serverName", server.getIdentifiableName());
placeholders.put("serverDisplayName", server.getName());
placeholders.put("refreshBarrier", config.get(WebserverSettings.REDUCED_REFRESH_BARRIER));
placeholders.put("timeZone", config.getTimeZoneOffsetHours());
placeholders.put("gmPieColors", theme.getValue(ThemeVal.GRAPH_GM_PIE));
placeholders.put("contributors", Contributors.generateContributorHtml());
placeholders.put("versionButton", versionChecker.getUpdateButton().orElse(versionChecker.getCurrentVersionButton()));
placeholders.put("version", versionChecker.getCurrentVersion());
placeholders.put("updateModal", versionChecker.getUpdateModal());
CachingSupplier<ServerPluginTabs> pluginTabs = new CachingSupplier<>(() -> {
List<ExtensionData> extensionData = dbSystem.getDatabase().query(new ExtensionServerDataQuery(serverUUID));
return new ServerPluginTabs(extensionData, formatters, componentSvc);
});
long after = System.currentTimeMillis() - config.get(WebserverSettings.REDUCED_REFRESH_BARRIER);
String navIdentifier = DataID.EXTENSION_NAV.of(serverUUID);
String tabIdentifier = DataID.EXTENSION_TABS.of(serverUUID);
String nav = jsonStorage.fetchJsonMadeAfter(navIdentifier, after).orElseGet(() -> {
jsonStorage.invalidateOlder(navIdentifier, after);
return jsonStorage.storeJson(navIdentifier, pluginTabs.get().getNav());
}).json;
String tabs = jsonStorage.fetchJsonMadeAfter(tabIdentifier, after).orElseGet(() -> {
jsonStorage.invalidateOlder(tabIdentifier, after);
return jsonStorage.storeJson(tabIdentifier, pluginTabs.get().getTabs());
}).json;
PlaceholderReplacer pluginPlaceholders = new PlaceholderReplacer();
pluginPlaceholders.put("serverUUID", serverUUID.toString());
pluginPlaceholders.put("serverName", server.getIdentifiableName());
pluginPlaceholders.put("serverDisplayName", server.getName());
pluginPlaceholders.put("backButton", serverInfo.getServer().isProxy() ? Html.BACK_BUTTON_NETWORK.create() : "");
pluginPlaceholders.put("navPluginsTabs", nav);
pluginPlaceholders.put("tabsPlugins", tabs);
return UnaryChain.of(templateHtml)
.chain(theme::replaceThemeColors)
.chain(placeholders::apply)
.chain(pluginPlaceholders::apply)
.chain(locale::replaceLanguageInHtml)
.apply();
}
}

View File

@ -1,263 +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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.component.ComponentSvc;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
import com.djrapitops.plan.delivery.rendering.html.structure.NavLink;
import com.djrapitops.plan.delivery.rendering.html.structure.TabsElement;
import com.djrapitops.plan.extension.ElementOrder;
import com.djrapitops.plan.extension.FormatType;
import com.djrapitops.plan.extension.implementation.TabInformation;
import com.djrapitops.plan.extension.implementation.results.*;
import com.djrapitops.plan.utilities.java.Lists;
import java.util.*;
/**
* Responsible for generating /server page plugin tabs based on DataExtension API data.
* <p>
* Currently very similar to {@link PlayerPluginTab}.
* This will become more complex once tables are added, since some big tables will be moved to their own tabs.
*
* @author AuroraLS3
*/
public class ServerPluginTabs {
private List<ExtensionData> serverData;
private List<ExtensionData> extraTabServerData;
private Map<FormatType, Formatter<Long>> numberFormatters;
private Formatter<Double> decimalFormatter;
private Formatter<Double> percentageFormatter;
private StringBuilder nav;
private String tab;
private final ComponentSvc componentSvc;
public ServerPluginTabs(String nav, String tab, ComponentSvc componentSvc) {
this.nav = new StringBuilder(nav);
this.tab = tab;
this.componentSvc = componentSvc;
}
public ServerPluginTabs(
List<ExtensionData> serverData,
Formatters formatters,
ComponentSvc componentSvc
) {
this.serverData = serverData;
Collections.sort(serverData);
this.extraTabServerData = Lists.filter(serverData, ExtensionData::doesNeedWiderSpace);
this.serverData.removeAll(extraTabServerData);
this.componentSvc = componentSvc;
numberFormatters = new EnumMap<>(FormatType.class);
numberFormatters.put(FormatType.DATE_SECOND, formatters.secondLong());
numberFormatters.put(FormatType.DATE_YEAR, formatters.yearLong());
numberFormatters.put(FormatType.TIME_MILLISECONDS, formatters.timeAmount());
numberFormatters.put(FormatType.NONE, Object::toString);
this.decimalFormatter = formatters.decimals();
this.percentageFormatter = formatters.percentage();
generate();
}
public String getNav() {
return nav.toString();
}
public String getTabs() {
return tab;
}
private void generate() {
String tabID = "plugins-overview";
if (serverData.isEmpty()) {
nav = new StringBuilder(NavLink.main(Icon.called("cubes").build(), tabID, "Overview (No Data)").toHtml());
tab = wrapInWideColumnTab(
"Overview", "<div class=\"card\"><div class=\"card-body\"><p>No Extension Data</p></div></div>"
);
} else {
nav = new StringBuilder(NavLink.main(Icon.called("cubes").build(), tabID, "Overview").toHtml());
tab = generateOverviewTab();
}
tab += generateExtraTabs();
}
private String generateExtraTabs() {
StringBuilder tabBuilder = new StringBuilder();
for (ExtensionData datum : extraTabServerData) {
ExtensionInformation extensionInformation = datum.getExtensionInformation();
boolean onlyGeneric = datum.hasOnlyGenericTab();
String tabsElement;
if (onlyGeneric) {
ExtensionTabData genericTabData = datum.getTabs().get(0);
tabsElement = buildContentHtml(genericTabData);
} else {
tabsElement = new TabsElement(
datum.getPluginID(),
datum.getTabs().stream().map(this::wrapToTabElementTab).toArray(TabsElement.Tab[]::new)
).toHtmlFull();
}
String tabName = extensionInformation.getPluginName();
tabBuilder.append(wrapInWideColumnTab(tabName, wrapInWideContainer(extensionInformation, tabsElement)));
nav.append(NavLink.main(Icon.fromExtensionIcon(extensionInformation.getIcon()), "plugins-" + tabName, tabName).toHtml());
}
return tabBuilder.toString();
}
private String generateOverviewTab() {
StringBuilder contentBuilder = new StringBuilder();
for (ExtensionData datum : serverData) {
ExtensionInformation extensionInformation = datum.getExtensionInformation();
boolean onlyGeneric = datum.hasOnlyGenericTab();
String tabsElement;
if (onlyGeneric) {
ExtensionTabData genericTabData = datum.getTabs().get(0);
tabsElement = buildContentHtml(genericTabData);
} else {
tabsElement = new TabsElement(
datum.getPluginID(),
datum.getTabs().stream().map(this::wrapToTabElementTab).toArray(TabsElement.Tab[]::new)
).toHtmlFull();
}
contentBuilder.append(wrapInContainer(extensionInformation, tabsElement));
}
return wrapInOverviewTab(contentBuilder.toString());
}
private String wrapInWideColumnTab(String tabName, String content) {
return "<div class=\"tab\" id=\"" + NavLink.format("plugins-" + tabName) + "\"><div class=\"container-fluid mt-4\">" +
// Page heading
"<div class=\"d-sm-flex align-items-center justify-content-between mb-4\">" +
"<h1 class=\"h3 mb-0 text-gray-800\"><i class=\"sidebar-toggler fa fa-fw fa-bars\"></i>${serverName} &middot; " + tabName + "</h1>${backButton}" +
"</div>" +
// End Page heading
"<div class=\"row\"><div class=\"col-md-12\">" + content + "</div></div></div></div>";
}
private String wrapInOverviewTab(String content) {
return "<div class=\"tab\" id=\"" + NavLink.format("plugins-overview") + "\"><div class=\"container-fluid mt-4\">" +
// Page heading
"<div class=\"d-sm-flex align-items-center justify-content-between mb-4\">" +
"<h1 class=\"h3 mb-0 text-gray-800\"><i class=\"sidebar-toggler fa fa-fw fa-bars\"></i>${serverName} &middot; Plugins Overview</h1>${backButton}" +
"</div>" +
// End Page heading
"<div class=\"row\" data-masonry='{\"percentPosition\": true}'>" + content + "</div></div></div>";
}
private TabsElement.Tab wrapToTabElementTab(ExtensionTabData tabData) {
TabInformation tabInformation = tabData.getTabInformation();
String tabContentHtml = buildContentHtml(tabData);
String tabName = tabInformation.getTabName();
return tabName.isEmpty() ? new TabsElement.Tab(Icon.called("info-circle").build(), "General", tabContentHtml)
: new TabsElement.Tab(Icon.fromExtensionIcon(tabInformation.getTabIcon()), tabName, tabContentHtml);
}
private String buildContentHtml(ExtensionTabData tabData) {
TabInformation tabInformation = tabData.getTabInformation();
List<ElementOrder> order = tabInformation.getTabElementOrder();
String values = buildValuesHtml(tabData);
String valuesHtml = values.isEmpty() ? "" : "<div class=\"card-body\">" + values + "</div>";
String tablesHtml = buildTablesHtml(tabData);
StringBuilder builder = new StringBuilder();
for (ElementOrder ordering : order) {
switch (ordering) {
case VALUES:
builder.append(valuesHtml);
break;
case TABLE:
builder.append(tablesHtml);
break;
default:
break;
}
}
return builder.toString();
}
private String buildTablesHtml(ExtensionTabData tabData) {
StringBuilder builder = new StringBuilder();
for (ExtensionTableData tableData : tabData.getTableData()) {
builder.append(tableData.getHtmlTable().toHtml());
}
return builder.toString();
}
private String buildValuesHtml(ExtensionTabData tabData) {
StringBuilder builder = new StringBuilder();
for (String key : tabData.getValueOrder()) {
tabData.getBoolean(key).ifPresent(data -> append(builder, data.getDescription(), data.getFormattedValue()));
tabData.getDouble(key).ifPresent(data -> append(builder, data.getDescription(), data.getFormattedValue(decimalFormatter)));
tabData.getPercentage(key).ifPresent(data -> append(builder, data.getDescription(), data.getFormattedValue(percentageFormatter)));
tabData.getNumber(key).ifPresent(data -> append(builder, data.getDescription(), data.getFormattedValue(numberFormatters.get(data.getFormatType()))));
tabData.getString(key).ifPresent(data -> append(builder, data.getDescription(), data.getFormattedValue()));
tabData.getComponent(key).ifPresent(data -> append(builder, data.getDescription(), data.getHtmlValue(componentSvc)));
}
return builder.toString();
}
private void append(StringBuilder builder, ExtensionDescription description, String formattedValue) {
Optional<String> textDescription = description.getDescription();
if (textDescription.isPresent()) {
builder.append("<p title=\"").append(textDescription.get()).append("\">");
} else {
builder.append("<p>");
}
builder.append(Icon.fromExtensionIcon(description.getIcon()))
.append(' ').append(description.getText()).append("<span class=\"float-end\"><b>").append(formattedValue).append("</b></span></p>");
}
private String wrapInContainer(ExtensionInformation information, String tabsElement) {
return "<div class=\"col-lg-6 col-xxl-4\">" +
"<div class=\"card shadow mb-4\">" +
"<div class=\"card-header py-3\">" +
"<h6 class=\"m-0 fw-bold col-black\">" + Icon.fromExtensionIcon(information.getIcon()) + ' ' + information.getPluginName() + "</h6>" +
"</div>" +
tabsElement +
"</div>" +
"</div>";
}
private String wrapInWideContainer(ExtensionInformation information, String tabsElement) {
return "<div class=\"card shadow mb-4\">" +
"<div class=\"card-header py-3\">" +
"<h6 class=\"m-0 fw-bold col-black\">" + Icon.fromExtensionIcon(information.getIcon()) + ' ' + information.getPluginName() + "</h6>" +
"</div>" +
tabsElement +
"</div>";
}
}

View File

@ -1,145 +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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.web;
import com.djrapitops.plan.TaskSystem;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.settings.config.ConfigNode;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.storage.file.PlanFiles;
import net.playeranalytics.plugin.scheduling.RunnableFactory;
import net.playeranalytics.plugin.server.PluginLogger;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* Task in charge of checking html customized files on enable to see if they are outdated.
*
* @deprecated Html customization system will be overhauled for React version of frontend.
*/
@Singleton
@Deprecated(forRemoval = true, since = "#2260") // TODO Remove after Frontend BETA
public class WebAssetVersionCheckTask extends TaskSystem.Task {
private final PlanConfig config;
private final PlanFiles files;
private final PluginLogger logger;
private final AssetVersions assetVersions;
private final Formatters formatters;
@Inject
public WebAssetVersionCheckTask(
PlanConfig config,
PlanFiles files,
PluginLogger logger,
AssetVersions assetVersions,
Formatters formatters
) {
this.config = config;
this.files = files;
this.logger = logger;
this.assetVersions = assetVersions;
this.formatters = formatters;
}
@Override
public void register(RunnableFactory runnableFactory) {
runnableFactory.create(this).runTaskLaterAsynchronously(3, TimeUnit.SECONDS);
}
@Override
public void run() {
try {
runTask();
} finally {
cancel();
}
}
private void runTask() {
Optional<ConfigNode> planCustomizationNode = getPlanCustomizationNode();
if (planCustomizationNode.isPresent()) {
try {
assetVersions.prepare();
} catch (IOException e) {
logger.warn(String.format("Could not read web asset versions, %s", e.toString()));
logger.warn("Web asset version check will be skipped!");
return;
}
List<AssetInfo> outdated = new ArrayList<>();
for (ConfigNode child : planCustomizationNode.get().getChildren()) {
if (child.getBoolean()) {
String resource = child.getKey(false).replace(',', '.');
findOutdatedResource(resource).ifPresent(outdated::add);
}
}
if (!outdated.isEmpty()) {
logger.warn("You have customized files which are out of date due to recent updates!");
logger.warn("Plan may not work properly until these files are updated to include the new modifications.");
logger.warn("See https://github.com/plan-player-analytics/Plan/commits/html to compare changes");
}
for (AssetInfo asset : outdated) {
logger.warn(String.format("- %s was modified %s, but the plugin contains a version from %s",
asset.filename,
formatters.secondLong().apply(asset.modifiedAt),
formatters.secondLong().apply(asset.expectedModifiedAt)
));
}
}
}
private Optional<AssetInfo> findOutdatedResource(String resource) {
Path dir = config.getResourceSettings().getCustomizationDirectory();
Optional<File> resourceFile = files.attemptToFind(dir, resource);
Optional<Long> webAssetVersion = assetVersions.getAssetVersion(resource);
if (resourceFile.isPresent() && webAssetVersion.isPresent() && webAssetVersion.get() > resourceFile.get().lastModified()) {
return Optional.of(new AssetInfo(
resource,
resourceFile.get().lastModified(),
webAssetVersion.get()
));
}
return Optional.empty();
}
private Optional<ConfigNode> getPlanCustomizationNode() {
return config.getResourceSettings().getCustomizationConfigNode().getNode("Plan");
}
private static class AssetInfo {
private final String filename;
private final long modifiedAt;
private final long expectedModifiedAt;
public AssetInfo(String filename, long modifiedAt, long expectedModifiedAt) {
this.filename = filename;
this.modifiedAt = modifiedAt;
this.expectedModifiedAt = expectedModifiedAt;
}
}
}

View File

@ -24,7 +24,6 @@ import com.djrapitops.plan.delivery.rendering.html.icon.Family;
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
import com.djrapitops.plan.delivery.rendering.pages.Page;
import com.djrapitops.plan.delivery.rendering.pages.PageFactory;
import com.djrapitops.plan.delivery.web.ResourceService;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.ResponseBuilder;
@ -34,7 +33,6 @@ import com.djrapitops.plan.delivery.web.resource.WebResource;
import com.djrapitops.plan.identification.Identifiers;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.ErrorPageLang;
import com.djrapitops.plan.settings.theme.Theme;
@ -71,7 +69,6 @@ public class ResponseFactory {
private static final String STATIC_BUNDLE_FOLDER = "static";
private final PlanFiles files;
private final PlanConfig config;
private final PublicHtmlFiles publicHtmlFiles;
private final PageFactory pageFactory;
private final Locale locale;
@ -92,7 +89,6 @@ public class ResponseFactory {
Lazy<Addresses> addresses
) {
this.files = files;
this.config = config;
this.publicHtmlFiles = publicHtmlFiles;
this.pageFactory = pageFactory;
this.locale = locale;
@ -103,14 +99,6 @@ public class ResponseFactory {
httpLastModifiedFormatter = formatters.httpLastModifiedLong();
}
/**
* @throws UncheckedIOException If reading the resource fails
*/
public WebResource getResource(@Untrusted String resourceName) {
return ResourceService.getInstance().getResource("Plan", resourceName,
() -> files.getResourceFromJar("web/" + resourceName).asWebResource());
}
/**
* @throws UncheckedIOException If reading the resource fails
*/
@ -191,7 +179,7 @@ public class ResponseFactory {
}
private Response getCachedOrNew(long modified, String fileName, Function<String, Response> newResponseFunction) {
WebResource resource = config.isFalse(PluginSettings.LEGACY_FRONTEND) ? getPublicOrJarResource(fileName) : getResource(fileName);
WebResource resource = getPublicOrJarResource(fileName);
Optional<Long> lastModified = resource.getLastModified();
if (lastModified.isPresent() && modified == lastModified.get()) {
return browserCachedNotChangedResponse();
@ -240,16 +228,10 @@ public class ResponseFactory {
public Response javaScriptResponse(@Untrusted String fileName) {
try {
WebResource resource = config.isFalse(PluginSettings.LEGACY_FRONTEND) ? getPublicOrJarResource(fileName) : getResource(fileName);
WebResource resource = getPublicOrJarResource(fileName);
String content = UnaryChain.of(resource.asString())
.chain(this::replaceMainAddressPlaceholder)
.chain(theme::replaceThemeColors)
.chain(contents -> {
if (fileName.contains(STATIC_BUNDLE_FOLDER) || fileName.startsWith("vendor/") || fileName.startsWith("/vendor/")) {
return contents;
}
return locale.replaceLanguageInJavascript(contents);
})
.chain(contents -> StringUtils.replace(contents,
".p=\"/\"",
".p=\"" + getBasePath() + "/\""))
@ -290,7 +272,7 @@ public class ResponseFactory {
public Response cssResponse(@Untrusted String fileName) {
try {
WebResource resource = config.isFalse(PluginSettings.LEGACY_FRONTEND) ? getPublicOrJarResource(fileName) : getResource(fileName);
WebResource resource = getPublicOrJarResource(fileName);
String content = UnaryChain.of(resource.asString())
.chain(theme::replaceThemeColors)
.chain(contents -> StringUtils.replace(contents, "/static", getBasePath() + "/static"))
@ -320,7 +302,7 @@ public class ResponseFactory {
public Response imageResponse(@Untrusted String fileName) {
try {
WebResource resource = config.isFalse(PluginSettings.LEGACY_FRONTEND) ? getPublicOrJarResource(fileName) : getResource(fileName);
WebResource resource = getPublicOrJarResource(fileName);
ResponseBuilder responseBuilder = Response.builder()
.setMimeType(MimeType.IMAGE)
.setContent(resource)
@ -356,7 +338,7 @@ public class ResponseFactory {
type = MimeType.FONT_BYTESTREAM;
}
try {
WebResource resource = config.isFalse(PluginSettings.LEGACY_FRONTEND) ? getPublicOrJarResource(fileName) : getResource(fileName);
WebResource resource = getPublicOrJarResource(fileName);
ResponseBuilder responseBuilder = Response.builder()
.setMimeType(type)
.setContent(resource);
@ -422,7 +404,7 @@ public class ResponseFactory {
try {
return Response.builder()
.setMimeType(MimeType.FAVICON)
.setContent(getResource("favicon.ico"))
.setContent(getPublicOrJarResource("favicon.ico"))
.build();
} catch (UncheckedIOException e) {
return forInternalError(e, "Could not read favicon");
@ -431,7 +413,7 @@ public class ResponseFactory {
public Response robotsResponse() {
try {
WebResource resource = getResource("robots.txt");
WebResource resource = getPublicOrJarResource("robots.txt");
Long lastModified = resource.getLastModified().orElseGet(System::currentTimeMillis);
return Response.builder()
.setMimeType("text/plain")
@ -523,13 +505,9 @@ public class ResponseFactory {
Database db = dbSystem.getDatabase();
PlayerContainer player = db.query(ContainerFetchQueries.fetchPlayerContainer(playerUUID));
if (player.getValue(PlayerKeys.REGISTERED).isPresent()) {
return forPage(request, pageFactory.playerPage(player));
return forPage(request, pageFactory.playerPage());
} else {
if (config.isTrue(PluginSettings.LEGACY_FRONTEND)) {
return playerNotFound404();
} else {
return forPage(request, pageFactory.reactPage(), 404);
}
return forPage(request, pageFactory.reactPage(), 404);
}
} catch (IllegalStateException notFoundLegacy) {
return playerNotFound404();
@ -574,7 +552,7 @@ public class ResponseFactory {
try {
return Response.builder()
.setMimeType(MimeType.JSON)
.setContent(getResource(file))
.setContent(getPublicOrJarResource(file))
.build();
} catch (UncheckedIOException e) {
return forInternalError(e, "Could not read " + file);

View File

@ -17,11 +17,8 @@
package com.djrapitops.plan.delivery.webserver;
import com.djrapitops.plan.SubSystem;
import com.djrapitops.plan.delivery.web.ResourceService;
import com.djrapitops.plan.delivery.webserver.auth.ActiveCookieStore;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.storage.file.PublicHtmlFiles;
import net.playeranalytics.plugin.server.PluginLogger;
@ -36,7 +33,6 @@ import javax.inject.Singleton;
@Singleton
public class WebServerSystem implements SubSystem {
private final PlanConfig config;
private final Addresses addresses;
private final ActiveCookieStore activeCookieStore;
private final PublicHtmlFiles publicHtmlFiles;
@ -45,13 +41,11 @@ public class WebServerSystem implements SubSystem {
@Inject
public WebServerSystem(
PlanConfig config,
Addresses addresses,
ActiveCookieStore activeCookieStore,
PublicHtmlFiles publicHtmlFiles,
WebServer webServer,
PluginLogger logger) {
this.config = config;
this.addresses = addresses;
this.activeCookieStore = activeCookieStore;
this.publicHtmlFiles = publicHtmlFiles;
@ -63,24 +57,9 @@ public class WebServerSystem implements SubSystem {
public void enable() {
activeCookieStore.enable();
webServer.enable();
if (config.isTrue(PluginSettings.LEGACY_FRONTEND)) {
if (!webServer.isAuthRequired()) {
ResourceService.getInstance().addStylesToResource("Plan", "error.html", ResourceService.Position.PRE_CONTENT, "./css/noauth.css");
ResourceService.getInstance().addStylesToResource("Plan", "server.html", ResourceService.Position.PRE_CONTENT, "../css/noauth.css");
ResourceService.getInstance().addStylesToResource("Plan", "player.html", ResourceService.Position.PRE_CONTENT, "../css/noauth.css");
ResourceService.getInstance().addStylesToResource("Plan", "players.html", ResourceService.Position.PRE_CONTENT, "./css/noauth.css");
ResourceService.getInstance().addStylesToResource("Plan", "network.html", ResourceService.Position.PRE_CONTENT, "./css/noauth.css");
ResourceService.getInstance().addStylesToResource("Plan", "query.html", ResourceService.Position.PRE_CONTENT, "./css/noauth.css");
}
if (webServer.isEnabled()) {
ResourceService.getInstance().addStylesToResource("Plan", "server.html", ResourceService.Position.PRE_CONTENT, "../css/querybutton.css");
ResourceService.getInstance().addStylesToResource("Plan", "players.html", ResourceService.Position.PRE_CONTENT, "./css/querybutton.css");
ResourceService.getInstance().addStylesToResource("Plan", "network.html", ResourceService.Position.PRE_CONTENT, "./css/querybutton.css");
}
} else {
if (publicHtmlFiles.findPublicHtmlResource("index.html").isPresent()) {
logger.info("Found index.html in public_html, using a custom React bundle!");
}
if (publicHtmlFiles.findPublicHtmlResource("index.html").isPresent()) {
logger.info("Found index.html in public_html, using a custom React bundle!");
}
}

View File

@ -25,6 +25,7 @@ import com.djrapitops.plan.identification.ServerUUID;
*/
public enum DataID {
PLAYERS,
PLAYERS_V2,
SESSIONS,
SERVERS,
KILLS,

View File

@ -17,7 +17,6 @@
package com.djrapitops.plan.delivery.webserver.resolver;
import com.djrapitops.plan.delivery.domain.auth.WebPermission;
import com.djrapitops.plan.delivery.rendering.html.Html;
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;
@ -26,7 +25,6 @@ import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.ResponseFactory;
import com.djrapitops.plan.identification.UUIDUtility;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.utilities.dev.Untrusted;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
@ -44,7 +42,6 @@ import java.util.UUID;
@Singleton
public class PlayerPageResolver implements Resolver {
private final PlanConfig config;
private final ResponseFactory responseFactory;
private final UUIDUtility uuidUtility;
@ -54,7 +51,6 @@ public class PlayerPageResolver implements Resolver {
ResponseFactory responseFactory,
UUIDUtility uuidUtility
) {
this.config = config;
this.responseFactory = responseFactory;
this.uuidUtility = uuidUtility;
}
@ -99,10 +95,6 @@ public class PlayerPageResolver implements Resolver {
return responseFactory.rawPlayerPageResponse(playerUUID);
}
if (path.getPart(2).isPresent() && config.isTrue(PluginSettings.LEGACY_FRONTEND)) {
// Redirect /player/{uuid/name}/ to /player/{uuid}
return responseFactory.redirectResponse("../" + Html.encodeToURL(playerUUID.toString()));
}
return responseFactory.playerPageResponse(request, playerUUID);
}
}

View File

@ -0,0 +1,122 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.domain.auth.WebPermission;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.rendering.json.JSONFactory;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.cache.AsyncJSONResolverService;
import com.djrapitops.plan.delivery.webserver.cache.DataID;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
import com.djrapitops.plan.identification.Identifiers;
import com.djrapitops.plan.identification.ServerUUID;
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.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
/**
* Resolves /v1/players JSON requests.
* <p>
* Deprecated.
*
* @author AuroraLS3
*/
@Singleton
@Path("/v1/players")
public class PlayersJSONResolver extends JSONResolver {
private final Identifiers identifiers;
private final AsyncJSONResolverService jsonResolverService;
private final JSONFactory jsonFactory;
@Inject
public PlayersJSONResolver(
Identifiers identifiers,
AsyncJSONResolverService jsonResolverService,
JSONFactory jsonFactory
) {
this.identifiers = identifiers;
this.jsonResolverService = jsonResolverService;
this.jsonFactory = jsonFactory;
}
@Override
public Formatter<Long> getHttpLastModifiedFormatter() {return jsonResolverService.getHttpLastModifiedFormatter();}
@Override
public boolean canAccess(Request request) {
WebUser user = request.getUser().orElse(new WebUser(""));
if (request.getQuery().get("server").isPresent()) {
return user.hasPermission(WebPermission.PAGE_SERVER_PLAYERS);
}
// Assume players page
return user.hasPermission(WebPermission.ACCESS_PLAYERS)
|| user.hasPermission(WebPermission.ACCESS_NETWORK) && user.hasPermission(WebPermission.PAGE_NETWORK_PLAYERS);
}
@GET
@Operation(
description = "Get player table data for /players page or a server. Deprecated, use /v1/playersTable instead.",
responses = {
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON)),
},
parameters = @Parameter(in = ParameterIn.QUERY, name = "server", description = "Server identifier to get data for (optional)", examples = {
@ExampleObject("Server 1"),
@ExampleObject("1"),
@ExampleObject("1fb39d2a-eb82-4868-b245-1fad17d823b3"),
}),
requestBody = @RequestBody(content = @Content(examples = @ExampleObject())),
deprecated = true
)
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(getResponse(request));
}
private Response getResponse(Request request) {
JSONStorage.StoredJSON storedJSON = getStoredJSON(request);
return getCachedOrNewResponse(request, storedJSON);
}
private JSONStorage.StoredJSON getStoredJSON(@Untrusted Request request) {
Optional<Long> timestamp = Identifiers.getTimestamp(request);
JSONStorage.StoredJSON storedJSON;
if (request.getQuery().get("server").isPresent()) {
ServerUUID serverUUID = identifiers.getServerUUID(request); // Can throw BadRequestException
storedJSON = jsonResolverService.resolve(timestamp, DataID.PLAYERS, serverUUID, uuid -> jsonFactory.serverPlayersTableJSON(uuid).toJSONMap());
} else {
// Assume players page
storedJSON = jsonResolverService.resolve(timestamp, DataID.PLAYERS, () -> jsonFactory.networkPlayersTableJSON().toJSONMap());
}
return storedJSON;
}
}

View File

@ -17,6 +17,7 @@
package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.domain.auth.WebPermission;
import com.djrapitops.plan.delivery.domain.datatransfer.PlayerListDto;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.rendering.json.JSONFactory;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
@ -34,6 +35,7 @@ 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;
@ -44,12 +46,12 @@ import javax.inject.Singleton;
import java.util.Optional;
/**
* Resolves /v1/players JSON requests.
* Resolves /v1/playersTable JSON requests.
*
* @author AuroraLS3
*/
@Singleton
@Path("/v1/players")
@Path("/v1/playersTable")
public class PlayersTableJSONResolver extends JSONResolver {
private final Identifiers identifiers;
@ -92,7 +94,7 @@ public class PlayersTableJSONResolver extends JSONResolver {
@ExampleObject("1"),
@ExampleObject("1fb39d2a-eb82-4868-b245-1fad17d823b3"),
}),
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = PlayerListDto.class)))
)
@Override
public Optional<Response> resolve(Request request) {
@ -109,10 +111,10 @@ public class PlayersTableJSONResolver extends JSONResolver {
JSONStorage.StoredJSON storedJSON;
if (request.getQuery().get("server").isPresent()) {
ServerUUID serverUUID = identifiers.getServerUUID(request); // Can throw BadRequestException
storedJSON = jsonResolverService.resolve(timestamp, DataID.PLAYERS, serverUUID, jsonFactory::serverPlayersTableJSON);
storedJSON = jsonResolverService.resolve(timestamp, DataID.PLAYERS_V2, serverUUID, uuid -> jsonFactory.serverPlayersTableJSON(uuid).toPlayerList());
} else {
// Assume players page
storedJSON = jsonResolverService.resolve(timestamp, DataID.PLAYERS, jsonFactory::networkPlayersTableJSON);
storedJSON = jsonResolverService.resolve(timestamp, DataID.PLAYERS_V2, () -> jsonFactory.networkPlayersTableJSON().toPlayerList());
}
return storedJSON;
}

View File

@ -20,6 +20,7 @@ import com.djrapitops.plan.delivery.domain.DateMap;
import com.djrapitops.plan.delivery.domain.auth.WebPermission;
import com.djrapitops.plan.delivery.domain.datatransfer.InputFilterDto;
import com.djrapitops.plan.delivery.domain.datatransfer.InputQueryDto;
import com.djrapitops.plan.delivery.domain.datatransfer.PlayerListDto;
import com.djrapitops.plan.delivery.domain.datatransfer.ViewDto;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
@ -255,13 +256,13 @@ public class QueryJSONResolver implements Resolver {
return graphJSONCreator.createActivityGraphJSON(activityData);
}
private Map<String, Object> getPlayersTableData(Set<Integer> userIds, List<ServerUUID> serverUUIDs, long after, long before) {
private PlayerListDto getPlayersTableData(Set<Integer> userIds, List<ServerUUID> serverUUIDs, long after, long before) {
Database database = dbSystem.getDatabase();
return new PlayersTableJSONCreator(
database.query(new QueryTablePlayersQuery(userIds, serverUUIDs, after, before, config.get(TimeSettings.ACTIVE_PLAY_THRESHOLD))),
database.query(new ExtensionQueryResultTableDataQuery(serverInfo.getServerUUID(), userIds)),
config.get(DisplaySettings.OPEN_PLAYER_LINKS_IN_NEW_TAB),
formatters, locale
).toJSONMap();
).toPlayerList();
}
}

View File

@ -22,6 +22,8 @@ import com.djrapitops.plan.delivery.web.resolver.CompositeResolver;
import com.djrapitops.plan.delivery.webserver.cache.AsyncJSONResolverService;
import com.djrapitops.plan.delivery.webserver.cache.DataID;
import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.delivery.webserver.resolver.json.metadata.PreferencesJSONResolver;
import com.djrapitops.plan.delivery.webserver.resolver.json.metadata.StorePreferencesJSONResolver;
import com.djrapitops.plan.identification.Identifiers;
import dagger.Lazy;
@ -46,6 +48,7 @@ public class RootJSONResolver {
private final WebGroupDeleteJSONResolver webGroupDeleteJSONResolver;
private final CompositeResolver.Builder readOnlyResourcesBuilder;
private final StorePreferencesJSONResolver storePreferencesJSONResolver;
private CompositeResolver resolver;
@Inject
@ -57,6 +60,7 @@ public class RootJSONResolver {
GraphsJSONResolver graphsJSONResolver,
SessionsJSONResolver sessionsJSONResolver,
PlayersJSONResolver playersJSONResolver,
PlayersTableJSONResolver playersTableJSONResolver,
ServerOverviewJSONCreator serverOverviewJSONCreator,
OnlineActivityOverviewJSONCreator onlineActivityOverviewJSONCreator,
@ -81,6 +85,8 @@ public class RootJSONResolver {
RetentionJSONResolver retentionJSONResolver,
PlayerJoinAddressJSONResolver playerJoinAddressJSONResolver,
PreferencesJSONResolver preferencesJSONResolver,
StorePreferencesJSONResolver storePreferencesJSONResolver,
WebGroupJSONResolver webGroupJSONResolver,
WebGroupPermissionJSONResolver webGroupPermissionJSONResolver,
WebPermissionJSONResolver webPermissionJSONResolver,
@ -91,7 +97,8 @@ public class RootJSONResolver {
this.asyncJSONResolverService = asyncJSONResolverService;
readOnlyResourcesBuilder = CompositeResolver.builder()
.add("players", playersTableJSONResolver)
.add("players", playersJSONResolver)
.add("playersTable", playersTableJSONResolver)
.add("sessions", sessionsJSONResolver)
.add("kills", playerKillsJSONResolver)
.add("graph", graphsJSONResolver)
@ -115,7 +122,8 @@ public class RootJSONResolver {
.add("whoami", whoAmIJSONResolver)
.add("extensionData", extensionJSONResolver)
.add("retention", retentionJSONResolver)
.add("joinAddresses", playerJoinAddressJSONResolver);
.add("joinAddresses", playerJoinAddressJSONResolver)
.add("preferences", preferencesJSONResolver);
this.webServer = webServer;
// These endpoints require authentication to be enabled.
@ -124,6 +132,7 @@ public class RootJSONResolver {
this.webPermissionJSONResolver = webPermissionJSONResolver;
this.webGroupSaveJSONResolver = webGroupSaveJSONResolver;
this.webGroupDeleteJSONResolver = webGroupDeleteJSONResolver;
this.storePreferencesJSONResolver = storePreferencesJSONResolver;
}
private <T> ServerTabJSONResolver<T> forJSON(DataID dataID, ServerTabJSONCreator<T> tabJSONCreator, WebPermission permission) {
@ -139,6 +148,7 @@ public class RootJSONResolver {
.add("permissions", webPermissionJSONResolver)
.add("saveGroupPermissions", webGroupSaveJSONResolver)
.add("deleteGroup", webGroupDeleteJSONResolver)
.add("storePreferences", storePreferencesJSONResolver)
.build();
} else {
resolver = readOnlyResourcesBuilder.build();

View File

@ -0,0 +1,86 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.resolver.json.metadata;
import com.djrapitops.plan.delivery.domain.datatransfer.preferences.Preferences;
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.request.Request;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.objects.WebUserQueries;
import com.djrapitops.plan.utilities.dev.Untrusted;
import com.djrapitops.plan.utilities.java.Maps;
import io.swagger.v3.oas.annotations.Operation;
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.GET;
import jakarta.ws.rs.Path;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
/**
* Represents /v1/preferences endpoint.
*
* @author AuroraLS3
*/
@Singleton
@Path("/v1/preferences")
public class PreferencesJSONResolver implements NoAuthResolver {
private final PlanConfig config;
private final DBSystem dbSystem;
@Inject
public PreferencesJSONResolver(PlanConfig config, DBSystem dbSystem) {
this.config = config;
this.dbSystem = dbSystem;
}
@GET
@Operation(
description = "Get user preferences (if they exist) and default preferences",
responses = {
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON))
},
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
)
@Override
public Optional<Response> resolve(@Untrusted Request request) {
Preferences defaultPreferences = config.getDefaultPreferences();
return Optional.of(Response.builder()
.setJSONContent(Maps.builder(String.class, Preferences.class)
.put("defaultPreferences", defaultPreferences)
.put("preferences", getUserPreferences(request))
.build()
).build());
}
private Preferences getUserPreferences(Request request) {
// No user -> no preferences
// No stored preferences -> no preferences
return request.getUser()
.flatMap(user -> dbSystem.getDatabase().query(WebUserQueries.fetchPreferences(user)))
.orElse(null);
}
}

View File

@ -0,0 +1,87 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.resolver.json.metadata;
import com.djrapitops.plan.delivery.domain.datatransfer.preferences.Preferences;
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.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.transactions.StoreWebUserPreferencesTransaction;
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.media.Content;
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 javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
/**
* @author AuroraLS3
*/
@Singleton
@Path("/v1/storePreferences")
public class StorePreferencesJSONResolver implements Resolver {
private final DBSystem dbSystem;
@Inject
public StorePreferencesJSONResolver(DBSystem dbSystem) {this.dbSystem = dbSystem;}
@Override
public boolean canAccess(Request request) {
return request.getUser().isPresent();
}
@POST
@Operation(
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"),
},
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = Preferences.class)))
)
@Override
public Optional<Response> resolve(Request request) {
if (!request.getMethod().equals("POST")) {
throw new BadRequestException("This endpoint only accepts POST requests.");
}
WebUser user = request.getUser()
.orElseThrow(() -> new BadRequestException("This endpoint only accepts requests if logged in."));
String preferencesBody = new String(request.getRequestBody(), StandardCharsets.UTF_8);
try {
Gson gson = new Gson();
@Untrusted String syntaxSanitized = gson.toJson(gson.fromJson(preferencesBody, Preferences.class));
dbSystem.getDatabase().executeTransaction(new StoreWebUserPreferencesTransaction(syntaxSanitized, user));
} catch (JsonSyntaxException invalidSyntax) {
throw new BadRequestException("Request body does not match json format of preferences");
}
return Optional.empty();
}
}

View File

@ -16,11 +16,6 @@
*/
package com.djrapitops.plan.extension.implementation.results;
import com.djrapitops.plan.component.Component;
import com.djrapitops.plan.component.ComponentOperation;
import com.djrapitops.plan.component.ComponentSvc;
import com.djrapitops.plan.delivery.rendering.html.Html;
/**
* Represents component data returned by a ComponentProvider method.
*
@ -44,8 +39,4 @@ public class ExtensionComponentData implements DescribedExtensionData {
return value;
}
public String getHtmlValue(ComponentSvc service) {
String legacy = service.convert(service.fromJson(value), ComponentOperation.LEGACY, Component.SECTION);
return Html.swapColorCodesToSpan(legacy);
}
}

View File

@ -18,6 +18,8 @@ package com.djrapitops.plan.extension.implementation.results;
import com.djrapitops.plan.delivery.rendering.html.Html;
import java.util.Map;
/**
* Represents double data returned by a DoubleProvider or PercentageProvider method.
*
@ -51,9 +53,19 @@ public class ExtensionStringData implements DescribedExtensionData {
return playerName;
}
public String getFormattedValue() {
String withColors = Html.swapColorCodesToSpan(value);
return !playerName ? withColors : Html.LINK.create("../player/" + Html.encodeToURL(value), withColors);
public String getValue() {
return value;
}
public Object getFormattedValue() {
if (playerName) {
return Map.of(
"link", "/player/" + Html.encodeToURL(value),
"text", value
);
} else {
return value;
}
}
ExtensionStringData concatenate(ExtensionStringData other) {

View File

@ -16,7 +16,6 @@
*/
package com.djrapitops.plan.extension.implementation.results;
import com.djrapitops.plan.delivery.rendering.html.structure.HtmlTable;
import com.djrapitops.plan.extension.icon.Color;
import com.djrapitops.plan.extension.table.Table;
@ -39,10 +38,6 @@ public class ExtensionTableData implements Comparable<ExtensionTableData> {
this.tableColor = tableColor;
}
public HtmlTable getHtmlTable() {
return HtmlTable.fromExtensionTable(table, tableColor);
}
public String getProviderName() {
return providerName;
}

View File

@ -16,6 +16,10 @@
*/
package com.djrapitops.plan.settings.config;
import com.djrapitops.plan.delivery.domain.datatransfer.preferences.GraphThresholds;
import com.djrapitops.plan.delivery.domain.datatransfer.preferences.Preferences;
import com.djrapitops.plan.delivery.domain.datatransfer.preferences.TimeFormat;
import com.djrapitops.plan.settings.config.paths.DisplaySettings;
import com.djrapitops.plan.settings.config.paths.ExportSettings;
import com.djrapitops.plan.settings.config.paths.FormatSettings;
import com.djrapitops.plan.settings.config.paths.key.Setting;
@ -154,6 +158,36 @@ public class PlanConfig extends Config {
return worldAliasSettings;
}
public Preferences getDefaultPreferences() {
return Preferences.builder()
.withDateFormatFull(get(FormatSettings.DATE_FULL))
.withDateFormatNoSeconds(get(FormatSettings.DATE_NO_SECONDS))
.withDateFormatClock(get(FormatSettings.DATE_CLOCK))
.withRecentDaysInDateFormat(isTrue(FormatSettings.DATE_RECENT_DAYS))
.withDecimalFormat(get(FormatSettings.DECIMALS))
.withFirstDay(1) // 1 is Monday
.withTimeFormat(TimeFormat.builder()
.withYear(get(FormatSettings.YEAR))
.withYears(get(FormatSettings.YEARS))
.withMonth(get(FormatSettings.MONTH))
.withMonths(get(FormatSettings.MONTHS))
.withDay(get(FormatSettings.DAY))
.withDays(get(FormatSettings.DAYS))
.withHours(get(FormatSettings.HOURS))
.withMinutes(get(FormatSettings.MINUTES))
.withSeconds(get(FormatSettings.SECONDS))
.withZero(get(FormatSettings.ZERO_SECONDS))
.build())
.withPlayerHeadImageUrl(get(DisplaySettings.PLAYER_HEAD_IMG_URL))
.withTpsThresholds(new GraphThresholds(
get(DisplaySettings.GRAPH_TPS_THRESHOLD_HIGH),
get(DisplaySettings.GRAPH_TPS_THRESHOLD_MED)))
.withDiskThresholds(new GraphThresholds(
get(DisplaySettings.GRAPH_DISK_THRESHOLD_HIGH),
get(DisplaySettings.GRAPH_DISK_THRESHOLD_MED)))
.build();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -17,7 +17,6 @@
package com.djrapitops.plan.settings.config;
import com.djrapitops.plan.settings.config.paths.CustomizedFileSettings;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import com.djrapitops.plan.storage.file.PlanFiles;
import com.djrapitops.plan.utilities.dev.Untrusted;
@ -42,10 +41,6 @@ public class ResourceSettings {
}
public boolean shouldBeCustomized(String plugin, @Untrusted String fileName) {
if (config.isTrue(CustomizedFileSettings.WEB_DEV_MODE) && config.isTrue(PluginSettings.LEGACY_FRONTEND)) {
return true;
}
ConfigNode fileCustomization = getCustomizationConfigNode();
fileCustomization.setComment(Collections.singletonList("The files are placed in /Plan/web/ if the setting is 'true' when accessed."));

View File

@ -174,6 +174,9 @@ public class ConfigUpdater {
new ConfigChange.MovedValue("Time.Thresholds.Activity_index.Playtime_threshold", "Time.Thresholds.Activity_index.Playtime_threshold.Time"),
new ConfigChange.Removed("Plugin.Frontend_BETA"),
new ConfigChange.Removed("Plugin.Use_Legacy_Frontend"),
new ConfigChange.Removed("Customized_files.Enable_web_dev_mode"),
new ConfigChange.Removed("Customized_files.Plan"),
};
}

View File

@ -16,12 +16,10 @@
*/
package com.djrapitops.plan.settings.config.paths;
import com.djrapitops.plan.settings.config.paths.key.BooleanSetting;
import com.djrapitops.plan.settings.config.paths.key.Setting;
import com.djrapitops.plan.settings.config.paths.key.StringSetting;
public class CustomizedFileSettings {
public static final Setting<Boolean> WEB_DEV_MODE = new BooleanSetting("Customized_files.Enable_web_dev_mode");
public static final Setting<String> PATH = new StringSetting("Customized_files.Path");
private CustomizedFileSettings() {

View File

@ -42,7 +42,6 @@ public class PluginSettings {
public static final Setting<Boolean> CHECK_FOR_UPDATES = new BooleanSetting("Plugin.Update_notifications.Check_for_updates");
public static final Setting<Boolean> NOTIFY_ABOUT_DEV_RELEASES = new BooleanSetting("Plugin.Update_notifications.Notify_about_DEV_releases");
public static final Setting<Boolean> PROXY_COPY_CONFIG = new BooleanSetting("Plugin.Configuration.Allow_proxy_to_manage_settings");
public static final Setting<Boolean> LEGACY_FRONTEND = new BooleanSetting("Plugin.Use_Legacy_Frontend");
private PluginSettings() {
/* static variable class */

View File

@ -16,8 +16,6 @@
*/
package com.djrapitops.plan.settings.locale;
import com.djrapitops.plan.settings.locale.lang.HtmlLang;
import com.djrapitops.plan.settings.locale.lang.JSLang;
import com.djrapitops.plan.settings.locale.lang.Lang;
import com.djrapitops.plan.storage.file.FileResource;
import com.djrapitops.plan.storage.file.PlanFiles;
@ -25,9 +23,9 @@ import com.djrapitops.plan.storage.file.PlanFiles;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.HashMap;
import java.util.Objects;
import java.util.Optional;
/**
* Represents loaded language information.
@ -36,8 +34,6 @@ import java.util.regex.Pattern;
*/
public class Locale extends HashMap<Lang, Message> {
private static final Pattern FIND_SCRIPT = Pattern.compile("(<script id=[\"|'].*[\"|']>[\\s\\S]*?</script>|<script>[\\s\\S]*?</script>|<script src=[\"|'].*[\"|']></script>|<link [\\s\\S]*?>)");
public static Locale forLangCodeString(PlanFiles files, String code) throws IOException {
return forLangCode(LangCode.fromString(code), files);
}
@ -108,99 +104,6 @@ public class Locale extends HashMap<Lang, Message> {
this.langCode = locale.langCode;
}
public String replaceLanguageInHtml(String from) {
if (isEmpty()) {
return from;
}
Matcher scriptMatcher = FIND_SCRIPT.matcher(from);
List<String> foundScripts = new ArrayList<>();
while (scriptMatcher.find()) {
foundScripts.add(scriptMatcher.toMatchResult().group(0));
}
TranslatedString translated = new TranslatedString(from);
Arrays.stream(HtmlLang.values())
// Longest first so that entries that contain each other don't partially replace.
.sorted((one, two) -> Integer.compare(
two.getIdentifier().length(),
one.getIdentifier().length()
))
.forEach(lang -> getNonDefault(lang).ifPresent(replacement ->
translated.translate(lang.getDefault(), replacement.toString()))
);
StringBuilder complete = new StringBuilder(translated.length());
String[] parts = FIND_SCRIPT.split(translated.toString());
for (int i = 0; i < parts.length; i++) {
complete.append(parts[i]);
if (i < parts.length - 1) {
complete.append(replaceLanguageInJavascript(foundScripts.get(i)));
}
}
return complete.toString();
}
public String replaceLanguageInJavascript(String from) {
if (isEmpty()) {
return from;
}
TranslatedString translated = new TranslatedString(from);
Arrays.stream(JSLang.values())
// Longest first so that entries that contain each other don't partially replace.
.sorted((one, two) -> Integer.compare(
two.getIdentifier().length(),
one.getIdentifier().length()
))
.forEach(lang -> getNonDefault(lang).ifPresent(replacement ->
translated.translate(lang.getDefault(), replacement.toString()))
);
for (Lang extra : new Lang[]{
HtmlLang.UNIT_NO_DATA,
HtmlLang.TITLE_WORLD_PLAYTIME,
// HtmlLang.LABEL_OPERATOR,
// HtmlLang.LABEL_BANNED,
HtmlLang.SIDE_SESSIONS,
HtmlLang.LABEL_PLAYTIME,
HtmlLang.LABEL_AFK_TIME,
HtmlLang.LABEL_LONGEST_SESSION,
HtmlLang.LABEL_SESSION_MEDIAN,
HtmlLang.LABEL_PLAYER_KILLS,
HtmlLang.LABEL_MOB_KILLS,
HtmlLang.LABEL_DEATHS,
HtmlLang.LABEL_PLAYERS_ONLINE,
HtmlLang.LABEL_REGISTERED,
HtmlLang.TITLE_SERVER,
HtmlLang.TITLE_LENGTH,
HtmlLang.TITLE_AVG_PING,
HtmlLang.TITLE_BEST_PING,
HtmlLang.TITLE_WORST_PING,
HtmlLang.LABEL_FREE_DISK_SPACE,
HtmlLang.LABEL_NEW_PLAYERS,
HtmlLang.LABEL_UNIQUE_PLAYERS,
HtmlLang.LABEL_ACTIVE_PLAYTIME,
HtmlLang.LABEL_AFK_TIME,
HtmlLang.LABEL_AVG_SESSION_LENGTH,
HtmlLang.LABEL_AVG_PLAYTIME,
HtmlLang.LABEL_AVG_ACTIVE_PLAYTIME,
HtmlLang.LABEL_AVG_AFK_TIME,
HtmlLang.LABEL_AVG_PLAYTIME,
HtmlLang.SIDE_GEOLOCATIONS,
HtmlLang.LABEL_PER_PLAYER,
HtmlLang.TITLE_JOIN_ADDRESSES
}) {
getNonDefault(extra).ifPresent(replacement ->
translated.translate(extra.getDefault(), replacement.toString()));
}
return translated.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -1,133 +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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.settings.locale;
import org.apache.commons.lang3.StringUtils;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility for translating String.
* <p>
* Improves performance by avoiding a double for-each loop since this class can be considered final in the lambda
* expression in {@link Locale#replaceLanguageInHtml(String)}.
*
* @author AuroraLS3
*/
class TranslatedString {
private static final Pattern LINK_MATCHER = Pattern.compile("http(s|)://[\\w.\\-_%/?$#@!()&=]+");
private final List<TranslatedString> translating = new LinkedList<>();
TranslatedString(String translating) {
final Matcher matcher = LINK_MATCHER.matcher(translating);
int start = 0;
while (matcher.find()) {
String link = translating.substring(matcher.start(), matcher.end());
String prev = translating.substring(start, matcher.start());
if (!prev.isEmpty()) {
this.translating.add(new Translatable(prev));
}
start = matcher.end();
this.translating.add(new LockedString(link));
}
String remaining = translating.substring(start);
if (!remaining.isEmpty()) {
this.translating.add(new Translatable(remaining));
}
}
TranslatedString() {
}
public void translate(String replace, String with) {
for (TranslatedString sub : translating) {
sub.translate(replace, with);
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
toString(builder);
return builder.toString();
}
public void toString(StringBuilder builder) {
for (TranslatedString sub : translating) {
sub.toString(builder);
}
}
public int length() {
int length = 0;
for (TranslatedString sub : translating) {
length += sub.length();
}
return length;
}
static class Translatable extends TranslatedString {
private String translating;
Translatable(String translating) {
this.translating = translating;
}
@Override
public void translate(String replace, String with) {
translating = StringUtils.replace(translating, replace, with);
}
@Override
public void toString(StringBuilder builder) {
builder.append(translating);
}
@Override
public int length() {
return translating.length();
}
}
static class LockedString extends TranslatedString {
final String text;
LockedString(String text) {
this.text = text;
}
@Override
public void translate(String replace, String with) {
/* Some elements should not be translated, like URLs */
}
@Override
public void toString(StringBuilder builder) {
builder.append(text);
}
@Override
public int length() {
return text.length();
}
}
}

View File

@ -269,6 +269,10 @@ public enum HtmlLang implements Lang {
LABEL_SERVER_SELECTOR("html.label.serverSelector", "Server selector"),
LABEL_APPLY("html.label.apply", "Apply"),
LABEL_TABLE_VISIBLE_COLUMNS("html.label.table.visibleColumns", "Visible columns"),
LABEL_TABLE_SHOW_N_OF_M("html.label.table.showNofM", "Showing {{n}} of {{mn}} entries"),
LABEL_TABLE_SHOW_PER_PAGE("html.label.table.showPerPage", "Show per page"),
LOGIN_LOGIN("html.login.login", "Login"),
LOGIN_LOGOUT("html.login.logout", "Logout"),
LOGIN_USERNAME("html.login.username", "Username"),

View File

@ -483,4 +483,21 @@ public class LargeStoreQueries {
}
};
}
public static Executable storeAllPreferences(Map<String, String> preferencesByUsername) {
if (preferencesByUsername.isEmpty()) return Executable.empty();
return new ExecBatchStatement(WebUserPreferencesTable.INSERT_STATEMENT) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
for (var entry : preferencesByUsername.entrySet()) {
String username = entry.getKey();
String preferences = entry.getValue();
statement.setString(1, preferences);
statement.setString(2, username);
statement.addBatch();
}
}
};
}
}

View File

@ -17,12 +17,16 @@
package com.djrapitops.plan.storage.database.queries.objects;
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.storage.database.queries.Query;
import com.djrapitops.plan.storage.database.queries.QueryAllStatement;
import com.djrapitops.plan.storage.database.sql.building.Sql;
import com.djrapitops.plan.storage.database.sql.tables.*;
import com.djrapitops.plan.utilities.dev.Untrusted;
import com.djrapitops.plan.utilities.java.Lists;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import org.intellij.lang.annotations.Language;
import java.sql.ResultSet;
@ -239,4 +243,25 @@ public class WebUserQueries {
}
};
}
public static Query<Optional<Preferences>> fetchPreferences(@Untrusted WebUser user) {
return db -> db.queryOptional(WebUserPreferencesTable.SELECT_BY_WEB_USERNAME, WebUserQueries::extractPreferences, user.getUsername());
}
private static Preferences extractPreferences(ResultSet set) throws SQLException {
try {
String preferences = set.getString(WebUserPreferencesTable.PREFERENCES);
return new Gson().fromJson(preferences, Preferences.class);
} catch (JsonSyntaxException jsonChangedIncompatibly) {
return null;
}
}
public static Query<Map<String, String>> fetchAllPreferences() {
String sql = SELECT + WebUserPreferencesTable.PREFERENCES + "," + SecurityTable.USERNAME +
FROM + WebUserPreferencesTable.TABLE_NAME + " p" +
INNER_JOIN + SecurityTable.TABLE_NAME + " s ON s." + SecurityTable.ID + "=p." + WebUserPreferencesTable.WEB_USER_ID;
return db -> db.queryMap(sql, (results, to) ->
to.put(results.getString(SecurityTable.USERNAME), results.getString(WebUserPreferencesTable.PREFERENCES)));
}
}

View File

@ -18,13 +18,16 @@ package com.djrapitops.plan.storage.database.sql.tables;
import com.djrapitops.plan.storage.database.DBType;
import com.djrapitops.plan.storage.database.sql.building.CreateTableBuilder;
import com.djrapitops.plan.storage.database.sql.building.Sql;
import static com.djrapitops.plan.storage.database.sql.building.Sql.*;
/**
* Table information about 'plan_security'
*
* @author AuroraLS3
* @see com.djrapitops.plan.storage.database.transactions.patches.LinkedToSecurityTablePatch
* @see com.djrapitops.plan.storage.database.transactions.patches.SecurityTableGroupPatch
* @see com.djrapitops.plan.storage.database.transactions.patches.SecurityTableIdPatch
*/
public class SecurityTable {
@ -36,6 +39,7 @@ public class SecurityTable {
public static final String SALT_PASSWORD_HASH = "salted_pass_hash";
public static final String GROUP_ID = "group_id";
public static final String SELECT_ID_BY_USERNAME = SELECT + ID + FROM + TABLE_NAME + WHERE + USERNAME + "=?";
public static final String INSERT_STATEMENT = "INSERT INTO " + TABLE_NAME + " (" +
USERNAME + ',' +
LINKED_TO + ',' +
@ -48,11 +52,11 @@ public class SecurityTable {
public static String createTableSQL(DBType dbType) {
return CreateTableBuilder.create(TABLE_NAME, dbType)
.column(ID, Sql.INT).primaryKey()
.column(USERNAME, Sql.varchar(100)).notNull().unique()
.column(LINKED_TO, Sql.varchar(36)).defaultValue("''")
.column(SALT_PASSWORD_HASH, Sql.varchar(100)).notNull().unique()
.column(GROUP_ID, Sql.INT).notNull()
.column(ID, INT).primaryKey()
.column(USERNAME, varchar(100)).notNull().unique()
.column(LINKED_TO, varchar(36)).defaultValue("''")
.column(SALT_PASSWORD_HASH, varchar(100)).notNull().unique()
.column(GROUP_ID, INT).notNull()
.foreignKey(GROUP_ID, WebGroupTable.TABLE_NAME, WebGroupTable.ID)
.toString();
}

View File

@ -0,0 +1,58 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.storage.database.sql.tables;
import com.djrapitops.plan.storage.database.DBType;
import com.djrapitops.plan.storage.database.sql.building.CreateTableBuilder;
import com.djrapitops.plan.storage.database.sql.building.Sql;
import static com.djrapitops.plan.storage.database.sql.building.Sql.*;
/**
* Represents plan_web_user_preferences.
*
* @author AuroraLS3
*/
public class WebUserPreferencesTable {
public static final String TABLE_NAME = "plan_web_user_preferences";
public static final String ID = "id";
public static final String WEB_USER_ID = "web_user_id";
public static final String PREFERENCES = "preferences";
public static final String INSERT_STATEMENT = "INSERT INTO " + TABLE_NAME + " (" + PREFERENCES + ',' + WEB_USER_ID +
") VALUES (?, (" + SecurityTable.SELECT_ID_BY_USERNAME + "))";
public static final String SELECT_BY_WEB_USERNAME = SELECT + PREFERENCES + FROM + TABLE_NAME +
WHERE + WEB_USER_ID + "=(" + SecurityTable.SELECT_ID_BY_USERNAME + ")";
public static final String DELETE_BY_WEB_USERNAME = DELETE_FROM + TABLE_NAME +
WHERE + WEB_USER_ID + "=(" + SecurityTable.SELECT_ID_BY_USERNAME + ")";
private WebUserPreferencesTable() {
/* Static information class */
}
public static String createTableSQL(DBType dbType) {
return CreateTableBuilder.create(TABLE_NAME, dbType)
.column(ID, Sql.INT).primaryKey()
.column(PREFERENCES, "TEXT").notNull()
.column(WEB_USER_ID, Sql.INT)
.foreignKey(WEB_USER_ID, SecurityTable.TABLE_NAME, SecurityTable.ID)
.toString();
}
}

View File

@ -88,6 +88,7 @@ public class BackupCopyTransaction extends RemoveEverythingTransaction {
private void copyPlanWebUsers() {
copy(LargeStoreQueries::storeAllPlanWebUsers, WebUserQueries.fetchAllUsers());
copy(LargeStoreQueries::storeAllPreferences, WebUserQueries.fetchAllPreferences());
}
private void copyPlanServerInformation() {

View File

@ -0,0 +1,60 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.storage.database.transactions;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.storage.database.sql.tables.WebUserPreferencesTable;
import com.djrapitops.plan.utilities.dev.Untrusted;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* Stores user preferences as text in database.
*
* @author AuroraLS3
*/
public class StoreWebUserPreferencesTransaction extends Transaction {
@Untrusted
private final String preferences;
@Untrusted
private final WebUser user;
public StoreWebUserPreferencesTransaction(String preferences, WebUser user) {
this.preferences = preferences;
this.user = user;
}
@Override
protected void performOperations() {
execute(new ExecStatement(WebUserPreferencesTable.DELETE_BY_WEB_USERNAME) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setString(1, user.getUsername());
}
});
commitMidTransaction();
execute(new ExecStatement(WebUserPreferencesTable.INSERT_STATEMENT) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setString(1, preferences);
statement.setString(2, user.getUsername());
}
});
}
}

View File

@ -49,6 +49,7 @@ public class RemoveEverythingTransaction extends Patch {
clearTable(WebGroupToPermissionTable.TABLE_NAME);
clearTable(WebPermissionTable.TABLE_NAME);
clearTable(WebGroupTable.TABLE_NAME);
clearTable(WebUserPreferencesTable.TABLE_NAME);
clearTable(SecurityTable.TABLE_NAME);
clearTable(ServerTable.TABLE_NAME);
clearTable(CookieTable.TABLE_NAME);

View File

@ -18,6 +18,7 @@ package com.djrapitops.plan.storage.database.transactions.init;
import com.djrapitops.plan.storage.database.sql.tables.*;
import com.djrapitops.plan.storage.database.transactions.events.StoreJoinAddressTransaction;
import com.djrapitops.plan.storage.database.transactions.patches.SecurityTableIdPatch;
/**
* Transaction that creates the table schema of Plan database.
@ -52,6 +53,9 @@ public class CreateTablesTransaction extends OperationCriticalTransaction {
execute(WebPermissionTable.createTableSQL(dbType));
execute(WebGroupToPermissionTable.createTableSQL(dbType));
execute(SecurityTable.createTableSQL(dbType));
// Ensure plan_security has id column
executeOther(new SecurityTableIdPatch());
execute(WebUserPreferencesTable.createTableSQL(dbType));
// DataExtension tables
execute(ExtensionIconTable.createTableSQL(dbType));

View File

@ -48,16 +48,26 @@ public class SecurityTableGroupPatch extends Patch {
@Override
protected void applyPatch() {
try {
boolean hasIdColumn = hasColumn(tableName, SecurityTable.ID)
// Either temp table doesn't exist or it has id column so one can be selected.
&& !hasTable(tempTableName) || hasColumn(tempTableName, SecurityTable.ID);
if (hasIdColumn) {
dropForeignKeys(tableName);
ensureNoForeignKeyConstraints(tableName);
}
tempOldTable();
dropTable(tableName);
execute(SecurityTable.createTableSQL(dbType));
execute("INSERT INTO " + tableName + " (" +
(hasIdColumn ? SecurityTable.ID + ',' : "") +
SecurityTable.USERNAME + ',' +
SecurityTable.LINKED_TO + ',' +
SecurityTable.SALT_PASSWORD_HASH + ',' +
SecurityTable.GROUP_ID +
") " + SELECT +
(hasIdColumn ? SecurityTable.ID + ',' : "") +
SecurityTable.USERNAME + ',' +
SecurityTable.LINKED_TO + ',' +
SecurityTable.SALT_PASSWORD_HASH + ',' +

View File

@ -0,0 +1,116 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.storage.database.transactions.patches;
import com.djrapitops.plan.exceptions.database.DBOpException;
import com.djrapitops.plan.storage.database.sql.building.CreateTableBuilder;
import com.djrapitops.plan.storage.database.sql.tables.SecurityTable;
import static com.djrapitops.plan.storage.database.sql.building.Sql.*;
/**
* Adds id to plan_security if it is not yet there.
*
* @author AuroraLS3
*/
public class SecurityTableIdPatch extends Patch {
private static final String TEMP_TABLE_NAME = "temp_security_id_patch";
private static final String TABLE_NAME = SecurityTable.TABLE_NAME;
private static final String PERMISSION_LEVEL = "permission_level";
@Override
public boolean hasBeenApplied() {
return (hasColumn(TABLE_NAME, SecurityTable.ID))
&& !hasTable(TEMP_TABLE_NAME);
}
@Override
protected void applyPatch() {
boolean hasUuidLinks = hasColumn(TABLE_NAME, SecurityTable.LINKED_TO);
if (hasUuidLinks) {
patchWithLinkedUuids();
} else {
patchSimpleTable();
}
}
private void patchSimpleTable() {
try {
tempOldTable();
dropTable(TABLE_NAME);
execute(CreateTableBuilder.create(TABLE_NAME, dbType)
.column(ID, INT).primaryKey()
.column(SecurityTable.USERNAME, varchar(100)).notNull().unique()
.column(SecurityTable.SALT_PASSWORD_HASH, varchar(100)).notNull().unique()
.column(PERMISSION_LEVEL, INT)
.toString());
execute("INSERT INTO " + TABLE_NAME + " (" +
SecurityTable.USERNAME + ',' +
SecurityTable.SALT_PASSWORD_HASH + ',' +
PERMISSION_LEVEL +
") " + SELECT +
SecurityTable.USERNAME + ',' +
SecurityTable.SALT_PASSWORD_HASH + ',' +
PERMISSION_LEVEL +
FROM + TEMP_TABLE_NAME
);
dropTable(TEMP_TABLE_NAME);
} catch (Exception e) {
throw new DBOpException(SecurityTableGroupPatch.class.getSimpleName() + " failed.", e);
}
}
private void patchWithLinkedUuids() {
try {
tempOldTable();
dropTable(TABLE_NAME);
execute(CreateTableBuilder.create(TABLE_NAME, dbType)
.column(ID, INT).primaryKey()
.column(SecurityTable.USERNAME, varchar(100)).notNull().unique()
.column(SecurityTable.SALT_PASSWORD_HASH, varchar(100)).notNull().unique()
.column(SecurityTable.LINKED_TO, varchar(36)).defaultValue("''")
.column(PERMISSION_LEVEL, INT)
.toString());
execute("INSERT INTO " + TABLE_NAME + " (" +
SecurityTable.USERNAME + ',' +
SecurityTable.SALT_PASSWORD_HASH + ',' +
SecurityTable.LINKED_TO + ',' +
PERMISSION_LEVEL +
") " + SELECT +
SecurityTable.USERNAME + ',' +
SecurityTable.SALT_PASSWORD_HASH + ',' +
SecurityTable.LINKED_TO + ',' +
PERMISSION_LEVEL +
FROM + TEMP_TABLE_NAME
);
dropTable(TEMP_TABLE_NAME);
} catch (Exception e) {
throw new DBOpException(SecurityTableGroupPatch.class.getSimpleName() + " failed.", e);
}
}
private void tempOldTable() {
if (!hasTable(TEMP_TABLE_NAME)) {
renameTable(TABLE_NAME, TEMP_TABLE_NAME);
}
}
}

View File

@ -60,7 +60,9 @@ public class Maps {
}
public Builder<K, V> put(K key, V value) {
map.put(key, value);
if (value != null) {
map.put(key, value);
}
return this;
}

View File

@ -49,8 +49,6 @@ public class VersionChecker implements SubSystem {
protected final RunnableFactory runnableFactory;
protected final ErrorLogger errorLogger;
private static final String DOWNLOAD_ICON_HTML = "<i class=\"fa fa-fw fa-download\"></i> ";
protected VersionInfo newVersionAvailable;
@Inject
@ -126,49 +124,6 @@ public class VersionChecker implements SubSystem {
return Optional.ofNullable(newVersionAvailable);
}
public Optional<String> getUpdateButton() {
return getNewVersionAvailable().map(v -> {
String reduceFontSize = v.getVersion().compareTo(new VersionNumber("5.2 build 999")) > 0 ?
"font-size: 0.95rem;" : "";
return "<button class=\"btn bg-white col-plan\" style=\"" + reduceFontSize +
"\" data-bs-target=\"#updateModal\" data-bs-toggle=\"modal\" type=\"button\">" +
DOWNLOAD_ICON_HTML + locale.getString(PluginLang.VERSION_UPDATE) + ": " + v.getVersion().asString() +
"</button>";
}
);
}
public String getCurrentVersionButton() {
return "<button class=\"btn bg-plan\" data-bs-target=\"#updateModal\" data-bs-toggle=\"modal\" type=\"button\">" +
getCurrentVersion() +
"</button>";
}
public String getUpdateModal() {
return getNewVersionAvailable()
.map(v -> "<div class=\"modal-header\">" +
"<h5 class=\"modal-title\" id=\"updateModalLabel\">" +
DOWNLOAD_ICON_HTML + locale.getString(PluginLang.VERSION_UPDATE_AVAILABLE, v.getVersion().asString()) +
"</h5><button aria-label=\"Close\" class=\"btn-close\" data-bs-dismiss=\"modal\" type=\"button\"></button>" +
"</div>" + // Close modal-header
"<div class=\"modal-body\">" +
"<p>" + locale.getString(PluginLang.VERSION_CURRENT, getCurrentVersion()) + ". " + locale.getString(PluginLang.VERSION_UPDATE_INFO) +
(v.isRelease() ? "" : "<br>" + locale.getString(PluginLang.VERSION_UPDATE_DEV)) + "</p>" +
"<a class=\"btn col-plan\" href=\"" + v.getChangeLogUrl() + "\" rel=\"noopener noreferrer\" target=\"_blank\">" +
"<i class=\"fa fa-fw fa-list\"></i> " + locale.getString(PluginLang.VERSION_CHANGE_LOG) + "</a>" +
"<a class=\"btn col-plan\" href=\"" + v.getDownloadUrl() + "\" rel=\"noopener noreferrer\" target=\"_blank\">" +
DOWNLOAD_ICON_HTML + locale.getString(PluginLang.VERSION_DOWNLOAD, v.getVersion().asString()) + "</a>" +
"</div>") // Close modal-body
.orElse("<div class=\"modal-header\">" +
"<h5 class=\"modal-title\" id=\"updateModalLabel\">" +
"<i class=\"far fa-fw fa-check-circle\"></i> " + locale.getString(PluginLang.VERSION_CURRENT, getCurrentVersion()) +
"</h5><button aria-label=\"Close\" class=\"btn-close\" data-bs-dismiss=\"modal\" type=\"button\"></button>" +
"</div>" + // Close modal-header
"<div class=\"modal-body\">" +
"<p>" + locale.getString(PluginLang.VERSION_NEWEST) + "</p>" +
"</div>"); // Close modal-body
}
public String getCurrentVersion() {
return currentVersion.asString();
}

View File

@ -567,6 +567,10 @@ html:
sessions: "会话"
sortBy: "排序方式"
stacked: "堆叠"
table:
showNofM: "Showing {{n}} of {{mn}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
themeSelect: "主题选择"
thirdDeadliestWeapon: "第三致命的 PVP 武器"
thirtyDays: "30 天"

View File

@ -567,6 +567,10 @@ html:
sessions: "Relace"
sortBy: "Seřadit podle"
stacked: "Zaplnění"
table:
showNofM: "Showing {{n}} of {{mn}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
themeSelect: "Zvolené téma"
thirdDeadliestWeapon: "3. PvP Zbraň"
thirtyDays: "30 dní"

View File

@ -567,6 +567,10 @@ html:
sessions: "Sitzungen"
sortBy: "Sort By"
stacked: "Stacked"
table:
showNofM: "Showing {{n}} of {{mn}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
themeSelect: "Thema ausgewählt"
thirdDeadliestWeapon: "3. PvP Waffe"
thirtyDays: "30 Tage"

View File

@ -567,6 +567,10 @@ html:
sessions: "Sessions"
sortBy: "Sort By"
stacked: "Stacked"
table:
showNofM: "Showing {{n}} of {{mn}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
themeSelect: "Theme Select"
thirdDeadliestWeapon: "3rd PvP Weapon"
thirtyDays: "30 days"

View File

@ -567,6 +567,10 @@ html:
sessions: "Sesiones"
sortBy: "Sort By"
stacked: "Stacked"
table:
showNofM: "Showing {{n}} of {{mn}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
themeSelect: "Selección de tema"
thirdDeadliestWeapon: "3ª arma PvP"
thirtyDays: "30 días"

View File

@ -567,6 +567,10 @@ html:
sessions: "Istunnot"
sortBy: "Järjestä"
stacked: "Päällekäin"
table:
showNofM: "Showing {{n}} of {{mn}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
themeSelect: "Teemavalikko"
thirdDeadliestWeapon: "3. PvP Ase"
thirtyDays: "30 päivää"

View File

@ -567,6 +567,10 @@ html:
sessions: "Sessions"
sortBy: "Sort By"
stacked: "Stacked"
table:
showNofM: "Showing {{n}} of {{mn}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
themeSelect: "Sélection du Thème"
thirdDeadliestWeapon: "3ᵉ Arme de Combat"
thirtyDays: "30 jours"

View File

@ -567,6 +567,10 @@ html:
sessions: "Sessioni"
sortBy: "Sort By"
stacked: "Stacked"
table:
showNofM: "Showing {{n}} of {{mn}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
themeSelect: "Selezione Tema"
thirdDeadliestWeapon: "3° Arma PvP Preferita"
thirtyDays: "30 giorni"

View File

@ -567,6 +567,10 @@ html:
sessions: "接続履歴"
sortBy: "並べ替え"
stacked: "スタックビュー"
table:
showNofM: "Showing {{n}} of {{mn}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
themeSelect: "テーマ選択"
thirdDeadliestWeapon: "3番目にPvPで使用されている武器"
thirtyDays: "1ヶ月"

View File

@ -567,6 +567,10 @@ html:
sessions: "세션 목록"
sortBy: "Sort By"
stacked: "Stacked"
table:
showNofM: "Showing {{n}} of {{mn}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
themeSelect: "테마 선택"
thirdDeadliestWeapon: "3rd PvP 무기"
thirtyDays: "30일"

View File

@ -567,6 +567,10 @@ html:
sessions: "Sessies"
sortBy: "Sort By"
stacked: "Stacked"
table:
showNofM: "Showing {{n}} of {{mn}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
themeSelect: "Thema selecteren"
thirdDeadliestWeapon: "3e PvP-wapen"
thirtyDays: "30 dagen"

View File

@ -567,6 +567,10 @@ html:
sessions: "Sessões"
sortBy: "Sort By"
stacked: "Stacked"
table:
showNofM: "Showing {{n}} of {{mn}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
themeSelect: "Theme Select"
thirdDeadliestWeapon: "3rd PvP Weapon"
thirtyDays: "30 days"

View File

@ -567,6 +567,10 @@ html:
sessions: "Сессии"
sortBy: "Sort By"
stacked: "Stacked"
table:
showNofM: "Showing {{n}} of {{mn}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
themeSelect: "Выбор темы"
thirdDeadliestWeapon: "3-е PvP оружие"
thirtyDays: "30 дней"

View File

@ -567,6 +567,10 @@ html:
sessions: "Oturumlar"
sortBy: "Sort By"
stacked: "Stacked"
table:
showNofM: "Showing {{n}} of {{mn}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
themeSelect: "Tema Seçimi"
thirdDeadliestWeapon: "3. PvP Silahı"
thirtyDays: "30 gün"

View File

@ -567,6 +567,10 @@ html:
sessions: "會話"
sortBy: "排序方式"
stacked: "堆疊的"
table:
showNofM: "Showing {{n}} of {{mn}} entries"
showPerPage: "Show per page"
visibleColumns: "Visible columns"
themeSelect: "主題色選擇"
thirdDeadliestWeapon: "第三致命的 PvP 武器"
thirtyDays: "30 天"

View File

@ -1,3 +0,0 @@
#logout-button {
display: none;
}

View File

@ -1,3 +0,0 @@
.query-buttons {
display: inline-block !important;
}

File diff suppressed because it is too large Load Diff

View File

@ -51,16 +51,10 @@
<button class="btn bg-plan" data-bs-target="#colorChooserModal" data-bs-toggle="modal" type="button">
<i class="fa fa-palette"></i>
</button>
<button class="btn bg-plan" data-bs-target="#informationModal" data-bs-toggle="modal" type="button">
<i class="fa fa-fw fa-question-circle"></i>
</button>
<a class="btn bg-plan" href="auth/logout" id="logout-button">
<i class="fa fa-fw fa-door-open"></i> Logout
</a>
</div>
<div class="ms-md-3 text-center text-md-start">
${versionButton}
</div>
</ul>
<!-- End of Sidebar -->
@ -153,77 +147,6 @@
</div>
</div>
</div>
<!-- Information Modal -->
<div aria-hidden="true" aria-labelledby="informationModalLabel" class="modal fade" id="informationModal"
role="dialog" tabindex="-1">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="informationModalLabel"><i class="fa fa-fw fa-question-circle"></i>
Information about the plugin
</h5>
<button aria-label="Close" class="btn-close" data-bs-dismiss="modal" type="button"></button>
</div>
<div class="modal-body">
<p>Player Analytics is developed and licensed under <a
href="https://opensource.org/licenses/LGPL-3.0" rel="noopener noreferrer"
target="_blank">Lesser General Public License
v3.0</a></p>
<hr>
<a class="btn col-plan" href="https://github.com/plan-player-analytics/Plan/wiki"
rel="noopener noreferrer" target="_blank"><i class="fa fa-fw fa-graduation-cap"></i>
Plan Wiki, Tutorials & Documentation</a>
<a class="btn col-plan" href="https://github.com/plan-player-analytics/Plan/issues"
rel="noopener noreferrer" target="_blank"><i class="fa fa-fw fa-bug"></i> Report Issues</a>
<a class="btn col-plan" href="https://discord.gg/yXKmjzT" rel="noopener noreferrer"
target="_blank"><i class="fab fa-fw fa-discord"></i> General Support on Discord</a>
<hr>
<p>Player Analytics is developed by AuroraLS3.</p>
<p>In addition following <span class="col-plan">awesome people</span> have contributed:</p>
<ul class="row contributors">
${contributors}
<li>& Bug reporters!</li>
</ul>
<small><i class="fa fa-fw fa-code"></i> code contributor <i class="fa fa-fw fa-language"></i>
translator
</small>
<hr>
<p class="col-plan">Extra special thanks to those who have monetarily supported the development.
<i class="fa fa-fw fa-star col-amber"></i></p>
<hr>
<h6>bStats Metrics</h6>
<a class="btn col-plan" href="https://bstats.org/plugin/bukkit/Plan" rel="noopener noreferrer"
target="_blank"><i class="fa fa-fw fa-chart-area"></i> Bukkit</a>
<a class="btn col-plan" href="https://bstats.org/plugin/bungeecord/Plan"
rel="noopener noreferrer" target="_blank"><i class="fa fa-fw fa-chart-area"></i>
BungeeCord</a>
<a class="btn col-plan" href="https://bstats.org/plugin/sponge/plan" rel="noopener noreferrer"
target="_blank"><i class="fa fa-fw fa-chart-area"></i> Sponge</a>
<a class="btn col-plan" href="https://bstats.org/plugin/velocity/Plan/10326"
rel="noopener noreferrer"
target="_blank"><i class="fa fa-fw fa-chart-area"></i> Velocity</a>
</div>
<div class="modal-footer">
<button class="btn bg-plan" data-bs-dismiss="modal" type="button">OK</button>
</div>
</div>
</div>
</div>
<!-- Update Modal -->
<div aria-hidden="true" aria-labelledby="updateModalLabel" class="modal fade" id="updateModal" role="dialog"
tabindex="-1">
<div class="modal-dialog" role="document">
<div class="modal-content">
${updateModal}
<div class="modal-footer">
<button class="btn bg-plan" data-bs-dismiss="modal" type="button">OK</button>
</div>
</div>
</div>
</div>
</div>
<!-- End of Page Wrapper -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,498 +0,0 @@
(function ($) {
const bgElements = ['.sidebar', '.btn', 'body'];
const textElements = ['a', 'button'];
const colors = ['plan',
'red', 'pink', 'purple', 'deep-purple',
'indigo', 'blue', 'light-blue', 'cyan',
'teal', 'green', 'light-green', 'lime',
'yellow', 'amber', 'orange', 'deep-orange',
'brown', 'grey', 'blue-grey'];
const selectedColor = window.localStorage.getItem('themeColor');
const themeDefaultColor = '${defaultTheme}';
let currentColor = 'plan';
// Function for changing color
function setColor(nextColor) {
if (selectedColor === null) {
window.localStorage.setItem('themeColor', currentColor);
}
const bodyElement = document.querySelector('body');
bodyElement.classList.remove(`theme-${currentColor}`);
bodyElement.classList.add(`theme-${nextColor}`);
if (!nextColor || nextColor == currentColor) {
return;
}
bgElements.map(element => element + '.bg-' + currentColor + ":not(.color-chooser)")
.forEach(selector => {
document.querySelectorAll(selector).forEach(element => {
element.classList.remove(`bg-${currentColor}`);
element.classList.add(`bg-${nextColor}`);
});
});
textElements.map(element => element + '.col-' + currentColor)
.forEach(selector => {
document.querySelectorAll(selector).forEach(element => {
element.classList.remove(`col-${currentColor}`);
element.classList.add(`col-${nextColor}`);
});
});
if (nextColor != 'night') {
window.localStorage.setItem('themeColor', nextColor);
}
currentColor = nextColor;
}
// Set the color changing function for all color change buttons
function enableColorSetters() {
for (const color of colors) {
const selector = document.getElementById(`choose-${color}`);
selector.removeAttribute('disabled');
selector.classList.remove('disabled');
selector.classList.add(`bg-${color}`);
selector.addEventListener('click', () => setColor(color));
}
}
enableColorSetters();
function disableColorSetters() {
for (const color of colors) {
const selector = document.getElementById(`choose-${color}`);
selector.classList.add('disabled');
selector.setAttribute('disabled', 'true');
}
}
// Change the color of the theme
setColor(selectedColor ? selectedColor : themeDefaultColor);
let nightMode = window.localStorage.getItem('nightMode') == 'true' || '${defaultTheme}' == 'night';
const saturationReduction = 0.70;
// From https://stackoverflow.com/a/3732187
function withReducedSaturation(colorHex) {
// To RGB
let r = parseInt(colorHex.substr(1, 2), 16); // Grab the hex representation of red (chars 1-2) and convert to decimal (base 10).
let g = parseInt(colorHex.substr(3, 2), 16);
let b = parseInt(colorHex.substr(5, 2), 16);
// To HSL
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s;
const l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
// To css property
return 'hsl(' + h * 360 + ',' + s * 100 * saturationReduction + '%,' + l * 95 + '%)';
}
const red = withReducedSaturation('#F44336');
const pink = withReducedSaturation('#E91E63');
const purple = withReducedSaturation('#9C27B0');
const deepPurple = withReducedSaturation('#673AB7');
const indigo = withReducedSaturation('#3F51B5');
const blue = withReducedSaturation('#2196F3');
const lightBlue = withReducedSaturation('#03A9F4');
const cyan = withReducedSaturation('#00BCD4');
const teal = withReducedSaturation('#009688');
const green = withReducedSaturation('#4CAF50');
const lightGreen = withReducedSaturation('#8BC34A');
const lime = withReducedSaturation('#CDDC39');
const yellow = withReducedSaturation('#ffe821');
const amber = withReducedSaturation('#FFC107');
const warningColor = withReducedSaturation('#f6c23e');
const orange = withReducedSaturation('#FF9800');
const deepOrange = withReducedSaturation('#FF5722');
const dangerColor = withReducedSaturation('#e74a3b');
const brown = withReducedSaturation('#795548');
const grey = withReducedSaturation('#9E9E9E');
const blueGrey = withReducedSaturation('#607D8B');
const black = withReducedSaturation('#555555');
const planColor = withReducedSaturation('#368F17');
const successColor = withReducedSaturation('#1cc88a');
const nightModeColors = `.bg-red {background-color: ${red};color: #eee8d5;}` +
`.bg-pink {background-color: ${pink};color: #eee8d5;}` +
`.bg-purple {background-color: ${purple};color: #eee8d5;}` +
`.bg-deep-purple {background-color: ${deepPurple};color: #eee8d5;}` +
`.bg-indigo {background-color: ${indigo};color: #eee8d5;}` +
`.bg-blue {background-color: ${blue};color: #eee8d5;}` +
`.bg-light-blue {background-color: ${lightBlue};color: #eee8d5;}` +
`.bg-cyan {background-color: ${cyan};color: #eee8d5;}` +
`.bg-teal {background-color: ${teal};color: #eee8d5;}` +
`.bg-green {background-color: ${green};color: #eee8d5;}` +
`.bg-light-green {background-color: ${lightGreen};color: #eee8d5;}` +
`.bg-lime {background-color: ${lime};color: #eee8d5;}` +
`.bg-yellow {background-color: ${yellow};color: #eee8d5;}` +
`.bg-amber {background-color: ${amber};color: #eee8d5;}` +
`.bg-warning {background-color: ${warningColor};color: #eee8d5;}` +
`.bg-orange {background-color: ${orange};color: #eee8d5;}` +
`.bg-deep-orange {background-color: ${deepOrange};color: #eee8d5;}` +
`.bg-danger {background-color: ${dangerColor};color: #eee8d5;}` +
`.bg-brown {background-color: ${brown};color: #eee8d5;}` +
`.bg-grey {background-color: ${grey};color: #eee8d5;}` +
`.bg-blue-grey {background-color: ${blueGrey};color: #eee8d5;}` +
`.bg-black {background-color: ${black};color: #eee8d5;}` +
`.bg-plan,.page-item.active .page-link {background-color: ${planColor};color: #eee8d5;}` +
`.bg-success {background-color: ${successColor};color: #eee8d5;}` +
`.bg-night {background-color: #44475a;color: #eee8d5;}` +
`.bg-red-outline {outline-color: ${red};border-color: ${red};}` +
`.bg-pink-outline {outline-color: ${pink};border-color: ${pink};}` +
`.bg-purple-outline {outline-color: ${purple};border-color: ${purple};}` +
`.bg-deep-purple-outline {outline-color: ${deepPurple};border-color: ${deepPurple};}` +
`.bg-indigo-outline {outline-color: ${indigo};border-color: ${indigo};}` +
`.bg-blue-outline {outline-color: ${blue};border-color: ${blue};}` +
`.bg-light-blue-outline {outline-color: ${lightBlue};border-color: ${lightBlue};}` +
`.bg-cyan-outline {outline-color: ${cyan};border-color: ${cyan};}` +
`.bg-teal-outline {outline-color: ${teal};border-color: ${teal};}` +
`.bg-green-outline {outline-color: ${green};border-color: ${green};}` +
`.bg-light-green-outline {outline-color: ${lightGreen};border-color: ${lightGreen};}` +
`.bg-lime-outline {outline-color: ${lime};border-color: ${lime};}` +
`.bg-yellow-outline {outline-color: ${yellow};border-color: ${yellow};}` +
`.bg-amber-outline {outline-color: ${amber};border-color: ${amber};}` +
`.bg-orange-outline {outline-color: ${orange};border-color: ${orange};}` +
`.bg-deep-orange-outline {outline-color: ${deepOrange};border-color: ${deepOrange};}` +
`.bg-brown-outline {outline-color: ${brown};border-color: ${brown};}` +
`.bg-grey-outline {outline-color: ${grey};border-color: ${grey};}` +
`.bg-blue-grey-outline {outline-color: ${blueGrey};border-color: ${blueGrey};}` +
`.bg-black-outline {outline-color: ${black};border-color: ${black};}` +
`.bg-plan-outline {outline-color: ${planColor};border-color: ${planColor};}` +
`.col-red {color: ${red};}` +
`.col-pink {color: ${pink};}` +
`.col-purple {color: ${purple};}` +
`.col-deep-purple {color: ${deepPurple};}` +
`.col-indigo {color: ${indigo};}` +
`.col-blue {color: ${blue};}` +
`.col-light-blue {color: ${lightBlue};}` +
`.col-cyan {color: ${cyan};}` +
`.col-teal {color: ${teal};}` +
`.col-green {color: ${green};}` +
`.col-light-green {color: ${lightGreen};}` +
`.col-lime {color: ${lime};}` +
`.col-yellow {color: ${yellow};}` +
`.col-amber {color: ${amber};}` +
`.text-warning {color: ${warningColor};}` +
`.col-orange {color: ${orange};}` +
`.col-deep-orange {color: ${deepOrange};}` +
`.text-danger {color: ${dangerColor};}` +
`.col-brown {color: ${brown};}` +
`.col-grey {color: ${grey};}` +
`.col-blue-grey {color: ${blueGrey};}` +
`.col-plan {color: ${planColor};}` +
`.text-success {color: ${successColor};}`;
function changeNightModeCSS() {
if (nightMode) {
// Background colors from dracula theme
$('head').append('<style id="nightmode">' +
'#content {background-color:#282a36;}' +
'.btn {color: #eee8d5;}' +
'.card,.bg-white,.modal-content,.page-loader,.nav-tabs .nav-link:hover,.nav-tabs,hr,form .btn, .btn-outline-secondary{background-color:#44475a!important;border-color:#6272a4!important;}' +
'.bg-white.collapse-inner {border:1px solid;}' +
'.card-header {background-color:#44475a;border-color:#6272a4;}' +
'#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:#eee8d5 !important;}' +
'.collapse-item:hover,.nav-link.active {background-color: #606270 !important;}' +
'.nav-tabs .nav-link.active {background-color: #44475a !important;border-color:#6272a4 #6272a4 #44475a !important;}' +
'.fc-today {background:#646e8c !important}' +
'.fc-popover-body,.fc-popover-header{background-color: #44475a !important;color: #eee8d5 !important;}' +
'select,input,.dataTables_paginate .page-item:not(.active) a,.input-group-text,.input-group-text > * {background-color:#44475a !important;border-color:#6272a4 !important;color: #eee8d5 !important;}' +
nightModeColors +
'</style>');
// Turn bright tables to dark
$('.table').addClass('table-dark');
// Turn modal close buttons light
document.querySelectorAll('button.btn-close').forEach(element => { element.classList.add('btn-close-white'); })
// Sidebar is grey when in night mode
disableColorSetters();
setColor('night');
} else {
// Remove night mode style sheet
$('#nightmode').remove();
// Turn dark tables bright again
$('.table').removeClass('table-dark');
// Turn modal close buttons dark
document.querySelectorAll('button.btn-close').forEach(element => { element.classList.remove('btn-close-white'); })
// Sidebar is colorful
enableColorSetters();
setColor(window.localStorage.getItem('themeColor'));
}
}
function changeHighChartsNightMode() {
try {
Highcharts.theme = nightMode ? {
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'
}
} : { // Defaults
chart: {
backgroundColor: null,
plotBorderColor: '#cccccc'
},
title: {
style: {color: '#3E576F'}
},
subtitle: {
style: {color: '#3E576F'}
},
xAxis: {
gridLineColor: '#E6E6E6',
labels: {
style: {color: '#333333'}
},
lineColor: '#E6E6E6',
minorGridLineColor: '#505053',
tickColor: '#E6E6E6',
title: {
style: {color: '#333333'}
}
},
yAxis: {
gridLineColor: '#E6E6E6',
labels: {
style: {color: '#333333'}
},
lineColor: '#E6E6E6',
minorGridLineColor: '#505053',
tickColor: '#E6E6E6',
tickWidth: 1,
title: {
style: {color: '#333333'}
}
},
tooltip: {
backgroundColor: 'rgba(247,247,247,0.85)',
style: {color: '#333333'}
},
plotOptions: {
series: {
dataLabels: {color: undefined},
marker: {lineColor: undefined}
}
},
legend: {
itemStyle: {color: '#333333'},
itemHoverStyle: {color: '#000000'},
itemHiddenStyle: {color: '#cccccc'}
},
labels: {
style: {color: '#333333'}
},
drilldown: {
activeAxisLabelStyle: {color: '#333333'},
activeDataLabelStyle: {color: '#333333'}
},
navigation: {
buttonOptions: {
symbolStroke: '#333333',
theme: {fill: '#CCCCCC'}
}
},
// scroll charts
rangeSelector: {
buttonTheme: {
fill: '#F7F7F7',
stroke: '#333',
style: {color: '#4B336A'},
states: {
hover: {
fill: '#E6EBF5',
stroke: '#333',
style: {color: 'black'}
},
select: {
fill: '#E6EBF5',
stroke: '#333',
style: {color: 'black'}
}
}
},
inputBoxBorderColor: '#CCCCCC',
inputStyle: {
backgroundColor: '#333',
color: '#666666'
},
labelStyle: {color: "#666666"}
},
navigator: {
handles: {
backgroundColor: '#f2f2f2',
borderColor: '#999999'
},
outlineColor: '#cccccc',
maskFill: 'rgba(102,133,194,0.3)',
series: {lineColor: "#3FA0FF"},
xAxis: {gridLineColor: '#e6e6e6'}
},
scrollbar: {
barBackgroundColor: '#cccccc',
barBorderColor: '#cccccc',
buttonArrowColor: '#333333',
buttonBackgroundColor: '#e6e6e6',
buttonBorderColor: '#cccccc',
rifleColor: '#333333',
trackBackgroundColor: '#f2f2f2',
trackBorderColor: '#f2f2f2'
}
};
Highcharts.setOptions(Highcharts.theme);
updateGraphs();
} catch (e) {
if ("Highcharts is not defined" === e.message || "updateGraphs is not defined" === e.message) {
// Highcharts isn't loaded, can be ignored
} else {
console.error(e);
}
}
}
changeNightModeCSS();
changeHighChartsNightMode();
function toggleNightMode() {
nightMode = !nightMode;
setTimeout(function () {
window.localStorage.setItem('nightMode', nightMode);
changeNightModeCSS();
changeHighChartsNightMode();
}, 0);
}
$('#night-mode-toggle').on('click', toggleNightMode);
})(jQuery);

View File

@ -1,38 +0,0 @@
function insertElementBefore(elementSelector, createElementFunction) {
const placeBefore = document.querySelector(elementSelector);
insertElementBeforeElement(placeBefore, createElementFunction);
}
function insertElementBeforeElement(placeBefore, createElementFunction) {
const element = createElementFunction();
placeBefore.insertAdjacentElement('beforebegin', element);
}
function insertElementAfter(elementSelector, createElementFunction) {
const placeBefore = document.querySelector(elementSelector);
insertElementAfterElement(placeBefore, createElementFunction)
}
function insertElementAfterElement(placeBefore, createElementFunction) {
const element = createElementFunction();
placeBefore.insertAdjacentElement('afterend', element);
}
/**
* Shows an error using the page loader.
* @param {string} [message] The message to be shown.
*/
function loaderError(message) {
let loader = document.querySelector(".page-loader");
let loaderText = document.querySelector('.loader-text');
if (loader.style.display === "none") {
loader.style.display = "block";
}
if (message !== undefined) {
loaderText.innerText = message;
} else {
loaderText.innerText = "Error occurred, see the Developer Console (Ctrl+Shift+I) for details."
}
}

View File

@ -1,274 +0,0 @@
class Filter {
constructor(kind) {
this.kind = kind;
}
render(filterCount) {
return 'Unimplemented render function'
}
toObject() {
return {kind: this.kind}
}
}
class MultipleChoiceFilter extends Filter {
constructor(
id, kind, label, options
) {
super(kind);
this.id = id;
this.label = label;
this.options = options;
}
render(filterCount) {
const select = filterCount === 0 ? "of Players who " : "and ";
let html =
`<div id="${this.id}" class="mt-2">
<label class="form-label" for="${this.id}">${select}${this.label}:</label>
<div class="row">
<div class="col-11 flex-fill">
<select class="form-control" multiple style="margin-bottom: 0.5rem;">`;
for (const option of this.options.options) {
html += `<option>${option}</option>`;
}
html +=
` </select>
</div>
<div class="col-1 col-md-auto my-auto">
<button class="filter-remover btn btn-outline-secondary float-end"
onclick="removeFilter('${this.id}')"><i class="far fa-fw fa-trash-alt"></i></button>
</div>
</div>
</div>`;
return html;
}
toObject() {
let selected = [];
for (let option of document.querySelector(`#${this.id} select`).options) {
if (option.selected) selected.push(option.text);
}
selected = JSON.stringify(selected);
return {
kind: this.kind,
parameters: {selected}
}
}
}
class ActivityIndexFilter extends MultipleChoiceFilter {
constructor(
id, options
) {
super(id, "activityIndexNow", `are in Activity Groups`, options);
}
}
class BannedFilter extends MultipleChoiceFilter {
constructor(
id, options
) {
super(id, "banned", `are`, options);
}
}
class OperatorsFilter extends MultipleChoiceFilter {
constructor(
id, options
) {
super(id, "operators", `are`, options);
}
}
class JoinAddressFilter extends MultipleChoiceFilter {
constructor(
id, options
) {
super(id, "joinAddresses", `joined with address`, options);
}
}
// Lowercase due to locale translation: Geolocations
class geolocationsFilter extends MultipleChoiceFilter {
constructor(
id, options
) {
super(id, "geolocations", `have joined from country`, options);
}
}
class PluginBooleanGroupsFilter extends MultipleChoiceFilter {
constructor(
id, options
) {
super(id, "pluginsBooleanGroups", `have Plugin boolean value`, options);
}
}
class PluginGroupsFilter extends MultipleChoiceFilter {
constructor(
id, kind, options
) {
super(id, kind, `are in ${options.plugin}'s ${options.group} Groups`, options);
}
}
class BetweenDateFilter extends Filter {
constructor(id, kind, label, options) {
super(kind);
this.id = id;
this.label = label;
this.afterDate = options.after[0];
this.afterTime = options.after[1];
this.beforeDate = options.before[0];
this.beforeTime = options.before[1];
}
render(filterCount) {
const id = this.id;
const select = filterCount === 0 ? "of Players who " : "and ";
return (
`<div id="${id}">
<label>${select}${this.label}:</label>
<div class="my-2 row justify-content-start">
<div class="col-6 col-md-3">
<div class="input-group">
<div class="input-group-text">
<i class="far fa-calendar"></i>
</div>
<input id="${id}-afterdate" class="form-control" placeholder="${this.afterDate}" type="text"
onkeyup="setFilterOption('${id}', '${id}-afterdate', 'afterDate', isValidDate, correctDate)">
</div>
</div>
<div class="col-6 col-md-2">
<div class="input-group">
<div class="input-group-text">
<i class="far fa-clock"></i>
</div>
<input id="${id}-aftertime" class="form-control" placeholder="${this.afterTime}" type="text"
onkeyup="setFilterOption('${id}', '${id}-aftertime', 'afterTime', isValidTime, correctTime)">
</div>
</div>
<div class="col-12 col-md-1 text-center my-1 my-md-2 flex-fill">
<label for="inlineFormCustomSelectPref">&</label>
</div>
<div class="col-6 col-md-3">
<div class="input-group">
<div class="input-group-text">
<i class="far fa-calendar"></i>
</div>
<input id="${id}-beforedate" class="form-control" placeholder="${this.beforeDate}" type="text"
onkeyup="setFilterOption('${id}', '${id}-beforedate', 'beforeDate', isValidDate, correctDate)">
</div>
</div>
<div class="col-5 col-md-2">
<div class="input-group">
<div class="input-group-text">
<i class="far fa-clock"></i>
</div>
<input id="${id}-beforetime" class="form-control" placeholder="${this.beforeTime}" type="text"
onkeyup="setFilterOption('${id}', '${id}-beforetime', 'beforeTime', isValidTime, correctTime)">
</div>
</div>
<div class="col-1 col-md-auto">
<button class="filter-remover btn btn-outline-secondary float-end"
onclick="removeFilter('${this.id}')"><i class="far fa-fw fa-trash-alt"></i></button>
</div>
</div>
</div>`
);
}
toObject() {
return {
kind: this.kind,
parameters: {
afterDate: this.afterDate,
afterTime: this.afterTime,
beforeDate: this.beforeDate,
beforeTime: this.beforeTime
}
}
}
}
class PlayedBetweenFilter extends BetweenDateFilter {
constructor(id, options) {
super(id, "playedBetween", "Played between", options);
}
}
class RegisteredBetweenFilter extends BetweenDateFilter {
constructor(id, options) {
super(id, "registeredBetween", "Registered between", options);
}
}
class PlayedOnServerFilter extends MultipleChoiceFilter {
constructor(id, options) {
super(id, "playedOnServer", "have played on at least one of", options);
}
}
function createFilter(filter, id) {
if (filter.kind.startsWith("pluginGroups-")) {
return new PluginGroupsFilter(id, filter.kind, filter.options);
}
switch (filter.kind) {
case "activityIndexNow":
return new ActivityIndexFilter(id, filter.options);
case "banned":
return new BannedFilter(id, filter.options);
case "operators":
return new OperatorsFilter(id, filter.options);
case "joinAddresses":
return new JoinAddressFilter(id, filter.options);
case "geolocations":
return new geolocationsFilter(id, filter.options);
case "playedBetween":
return new PlayedBetweenFilter(id, filter.options);
case "registeredBetween":
return new RegisteredBetweenFilter(id, filter.options);
case "pluginsBooleanGroups":
return new PluginBooleanGroupsFilter(id, filter.options);
case "playedOnServer":
return new PlayedOnServerFilter(id, filter.options);
default:
throw new Error("Unsupported filter kind: '" + filter.kind + "'");
}
}
function getReadableFilterName(filter) {
if (filter.kind.startsWith("pluginGroups-")) {
return "Group: " + filter.kind.substring(13);
}
switch (filter.kind) {
case "allPlayers":
return "All players"
case "activityIndexNow":
return "Current activity group";
case "banned":
return "Ban status";
case "operators":
return "Operator status";
case "joinAddresses":
return "Join Addresses";
case "geolocations":
return "Geolocations";
case "playedBetween":
return "Played between";
case "registeredBetween":
return "Registered between";
case "pluginsBooleanGroups":
return "Has plugin boolean value";
case "playedOnServer":
return "Has played on one of servers";
default:
return filter.kind;
}
}

View File

@ -1,831 +0,0 @@
const linegraphButtons = [{
type: 'hour',
count: 12,
text: '12h'
}, {
type: 'hour',
count: 24,
text: '24h'
}, {
type: 'day',
count: 7,
text: '7d'
}, {
type: 'month',
count: 1,
text: '30d'
}, {
type: 'all',
text: 'All'
}];
const graphs = [];
window.calendars = {};
function activityPie(id, activitySeries) {
graphs.push(Highcharts.chart(id, {
chart: {
plotBackgroundColor: null,
plotBorderWidth: null,
plotShadow: false,
type: 'pie'
},
title: {text: ''},
tooltip: {
pointFormat: '{series.name}: <b>{point.y}</b>'
},
plotOptions: {
pie: {
allowPointSelect: true,
cursor: 'pointer',
dataLabels: {
enabled: false
},
showInLegend: true
}
},
series: [activitySeries]
}));
}
function diskChart(id, series) {
graphs.push(Highcharts.stockChart(id, {
rangeSelector: {
selected: 2,
buttons: linegraphButtons
},
yAxis: {
labels: {
formatter: function () {
return this.value + ' MB';
}
},
softMax: 2,
softMin: 0
},
title: {text: ''},
legend: {
enabled: true
},
series: series
}));
}
function horizontalBarChart(id, categories, series, text) {
graphs.push(Highcharts.chart(id, {
chart: {
type: 'bar'
},
title: {
text: ''
},
xAxis: {
categories: categories,
title: {
text: null
}
},
yAxis: {
min: 0,
title: {
text: text,
align: 'high'
},
labels: {
overflow: 'justify'
}
},
legend: {
enabled: false
},
plotOptions: {
bar: {
dataLabels: {
enabled: true
}
}
},
credits: {
enabled: true
},
series: series
}));
}
function lineChart(id, series) {
graphs.push(Highcharts.stockChart(id, {
rangeSelector: {
selected: 2,
buttons: linegraphButtons
},
yAxis: {
softMax: 2,
softMin: 0
},
title: {text: ''},
legend: {
enabled: true
},
series: series
}));
}
function dayByDay(id, series) {
graphs.push(Highcharts.stockChart(id, {
rangeSelector: {
selected: 2,
buttons: linegraphButtons
},
yAxis: {
softMax: 2,
softMin: 0
},
title: {text: ''},
legend: {
enabled: true
},
time: {timezoneOffset: 0},
series: series
}));
}
function onlineActivityCalendar(id, event_data, firstDay) {
window.calendars.online_activity = new FullCalendar.Calendar(document.querySelector(id), {
timeZone: "UTC",
themeSystem: 'bootstrap',
eventColor: '#2196F3',
firstDay: firstDay,
initialView: 'dayGridMonth',
eventDidMount: function (info) {
$(info.el).popover({
content: info.event.title,
trigger: 'hover',
placement: 'top',
container: 'body'
});
},
events: function (fetchInfo, successCallback, failureCallback) {
successCallback(event_data)
},
height: 800,
contentHeight: 795,
headerToolbar: {
left: 'title',
center: '',
right: 'today prev,next'
}
});
window.calendars.online_activity.render();
}
function mapToDataSeries(performanceData) {
const playersOnline = [];
const tps = [];
const cpu = [];
const ram = [];
const entities = [];
const chunks = [];
const disk = [];
return new Promise((resolve => {
let i = 0;
const length = performanceData.length;
function processNextThousand() {
const to = Math.min(i + 1000, length);
for (i; i < to; i++) {
const entry = performanceData[i];
const date = entry[0];
playersOnline[i] = [date, entry[1]];
tps[i] = [date, entry[2]];
cpu[i] = [date, entry[3]];
ram[i] = [date, entry[4]];
entities[i] = [date, entry[5]];
chunks[i] = [date, entry[6]];
disk[i] = [date, entry[7]];
}
if (i >= length) {
resolve({playersOnline, tps, cpu, ram, entities, chunks, disk})
} else {
setTimeout(processNextThousand, 10);
}
}
processNextThousand();
}))
}
function performanceChart(id, playersOnlineSeries, tpsSeries, cpuSeries, ramSeries, entitySeries, chunkSeries) {
const chart = Highcharts.stockChart(id, {
rangeSelector: {
selected: 2,
buttons: linegraphButtons
},
title: {text: ''},
yAxis: [{
labels: {
formatter: function () {
return this.value + ' P';
}
}
}, {
opposite: true,
labels: {
formatter: function () {
return this.value + ' TPS';
}
}
}, {
opposite: true,
labels: {
formatter: function () {
return this.value + '%';
}
}
}, {
labels: {
formatter: function () {
return this.value + ' MB';
}
}
}, {
opposite: true,
labels: {
formatter: function () {
return this.value + ' E';
}
}
}, {
labels: {
formatter: function () {
return this.value + ' C';
}
}
}],
legend: {
enabled: true
},
series: [playersOnlineSeries, tpsSeries, cpuSeries, ramSeries, entitySeries, chunkSeries]
});
function toggleLabels() {
if (!chart || !chart.yAxis || !chart.yAxis.length) return;
const newWidth = $(window).width();
chart.yAxis[0].update({labels: {enabled: newWidth >= 900}});
chart.yAxis[1].update({labels: {enabled: newWidth >= 900}});
chart.yAxis[2].update({labels: {enabled: newWidth >= 1000}});
chart.yAxis[3].update({labels: {enabled: newWidth >= 1000}});
chart.yAxis[4].update({labels: {enabled: newWidth >= 1400}});
chart.yAxis[5].update({labels: {enabled: newWidth >= 1400}});
}
$(window).resize(toggleLabels);
toggleLabels();
graphs.push(chart);
}
function playersChart(id, playersOnlineSeries, sel) {
function groupByIntervalStartingFrom(startDate, interval) {
let previousGroupStart = startDate;
const groupByInterval = [[]];
for (let point of playersOnlineSeries.data) {
const date = point[0];
if (date < startDate) {
continue;
}
if (previousGroupStart + interval < date) {
previousGroupStart = date;
groupByInterval.push([]);
}
const currentGroup = groupByInterval[groupByInterval.length - 1];
currentGroup.push(point);
}
return groupByInterval;
}
function averageGroupPoints(groupByInterval, minDate) {
const averages = [];
for (let group of groupByInterval) {
let totalDate = 0;
let total = 0;
let count = group.length;
for (let point of group) {
totalDate += (point[0] - minDate); // Remove the minDate from dates to calculate a smaller total
total += point[1];
}
if (count !== 0) {
const middleDate = Math.trunc((totalDate / count) + minDate);
const average = Math.trunc(total / count);
averages.push([middleDate, average]);
}
}
return averages;
}
function getAveragePlayersSeries(minDate, twentyPointInterval) {
const groupByInterval = groupByIntervalStartingFrom(minDate, twentyPointInterval);
return {
name: s.name.averagePlayersOnline,
type: s.type.spline,
tooltip: s.tooltip.zeroDecimals,
data: averageGroupPoints(groupByInterval, minDate),
color: "#02458d",
yAxis: 0
};
}
function updateAveragePlayers(event) {
const minDate = event.min;
const maxDate = event.max;
const twentyPointInterval = (maxDate - minDate) / 20;
const averagePlayersSeries = getAveragePlayersSeries(minDate, twentyPointInterval);
const playersOnlineGraph = graphs.find(graph => graph && graph.renderTo && graph.renderTo.id === id);
playersOnlineGraph.series[1].update(averagePlayersSeries);
}
const emptyAveragePlayersSeries = {
name: s.name.averagePlayersOnline,
type: s.type.spline,
tooltip: s.tooltip.zeroDecimals,
data: [],
color: "#02458d",
yAxis: 0
};
graphs.push(Highcharts.stockChart(id, {
rangeSelector: {
selected: sel,
buttons: linegraphButtons
},
yAxis: {
softMax: 2,
softMin: 0
},
/* Average online players graph Disabled
xAxis: {
events: {
afterSetExtremes: updateAveragePlayers
}
},
*/
title: {text: ''},
plotOptions: {
areaspline: {
fillOpacity: 0.4
}
},
series: [playersOnlineSeries, /*emptyAveragePlayersSeries*/]
}));
}
function playersChartNoNav(id, playersOnlineSeries) {
graphs.push(Highcharts.stockChart(id, {
rangeSelector: {
selected: 3,
buttons: linegraphButtons
},
navigator: {
enabled: false
},
yAxis: {
softMax: 2,
softMin: 0
},
title: {text: ''},
plotOptions: {
areaspline: {
fillOpacity: 0.4
}
},
series: [playersOnlineSeries]
}));
}
function punchCard(id, punchcardSeries) {
graphs.push(Highcharts.chart(id, {
chart: {
defaultSeriesType: 'scatter'
},
title: {text: ''},
xAxis: {
type: 'datetime',
dateTimeLabelFormats: {
// https://www.php.net/manual/en/function.strftime.php
hour: '%I %P',
day: '%I %P'
},
tickInterval: 3600000
},
time: {
timezoneOffset: 0
},
yAxis: {
title: {
text: "Day of the Week"
},
reversed: true,
categories: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
},
tooltip: {
pointFormat: 'Activity: {point.z}'
},
series: [punchcardSeries]
}));
}
function resourceChart(id, cpuSeries, ramSeries, playersOnlineSeries) {
graphs.push(Highcharts.stockChart(id, {
rangeSelector: {
selected: 1,
buttons: linegraphButtons
},
tooltip: {
split: true
},
title: {text: ''},
plotOptions: {
areaspline: {
fillOpacity: 0.4
}
},
yAxis: [{
labels: {
formatter: function () {
return this.value + ' Players';
}
}
}, {
opposite: true,
labels: {
formatter: function () {
return this.value + '%';
}
}
}, {
labels: {
formatter: function () {
return this.value + ' MB';
}
}
}],
legend: {
enabled: true
},
series: [cpuSeries, ramSeries, playersOnlineSeries]
}));
}
function serverPie(id, serverSeries) {
graphs.push(Highcharts.chart(id, {
chart: {
plotBackgroundColor: null,
plotBorderWidth: null,
plotShadow: false,
type: 'pie'
},
title: {text: ''},
plotOptions: {
pie: {
allowPointSelect: true,
cursor: 'pointer',
dataLabels: {
enabled: false
},
showInLegend: true
}
},
tooltip: {
formatter: function () {
return '<b>' + this.point.name + ':</b> ' + formatTimeAmount(this.y) + ' (' + this.percentage.toFixed(2) + '%)';
}
},
series: [serverSeries]
}));
}
function joinAddressPie(id, joinAddresses) {
if (joinAddresses.data.length < 2) {
document.getElementById(id).innerHTML = '<div class="card-body"><p></p></div>'
document.getElementById(id).classList.remove('chart-area');
// XSS danger appending join addresses directly, using innerText is safe.
for (let slice of joinAddresses.data) {
document.querySelector(`#${id} p`).innerText = `${slice.name}: ${slice.y}`;
}
} else {
document.getElementById(id).innerHTML = '';
document.getElementById(id).classList.add('chart-area');
graphs.push(Highcharts.chart(id, {
chart: {
plotBackgroundColor: null,
plotBorderWidth: null,
plotShadow: false,
type: 'pie'
},
title: {text: ''},
plotOptions: {
pie: {
allowPointSelect: true,
cursor: 'pointer',
dataLabels: {
enabled: false
},
showInLegend: true
}
},
tooltip: {
formatter: function () {
return '<b>' + this.point.name + ':</b> ' + this.y + ' (' + this.percentage.toFixed(2) + '%)';
}
},
series: [joinAddresses]
}));
}
}
function 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;
}
function sessionCalendar(id, event_data, firstDay) {
document.querySelector(id + " .loader").remove();
window.calendars.sessions = new FullCalendar.Calendar(document.querySelector(id), {
timeZone: "UTC",
themeSystem: 'bootstrap',
eventColor: '#009688',
dayMaxEventRows: 4,
firstDay: firstDay,
initialView: 'dayGridMonth',
eventDidMount: function (info) {
$(info.el).popover({
content: info.event.title,
trigger: 'hover',
placement: 'top',
container: 'body'
});
},
events: function (fetchInfo, successCallback, failureCallback) {
successCallback(event_data)
},
navLinks: true,
height: 450,
contentHeight: 445,
headerToolbar: {
left: 'title',
center: '',
right: 'dayGridMonth dayGridWeek dayGridDay today prev,next'
}
});
setTimeout(function () {
window.calendars.sessions.render();
}, 0);
}
function stackChart(id, categories, series, label) {
graphs.push(Highcharts.chart(id, {
chart: {
type: 'area'
},
title: {
text: ''
},
xAxis: {
categories: categories,
tickmarkPlacement: 'on',
title: {
enabled: false
},
ordinal: false
},
yAxis: {
title: {
text: label
},
labels: {
formatter: function () {
return this.value;
}
},
softMax: 2,
softMin: 0
},
tooltip: {
split: true,
valueSuffix: ' ' + label
},
plotOptions: {
area: {
stacking: 'normal',
lineWidth: 1
}
},
series: series
}));
}
function tpsChart(id, tpsSeries, playersOnlineSeries) {
graphs.push(Highcharts.stockChart(id, {
rangeSelector: {
selected: 1,
buttons: linegraphButtons
},
tooltip: {
split: true
},
title: {text: ''},
plotOptions: {
areaspline: {
fillOpacity: 0.4
}
},
yAxis: [{
labels: {
formatter: function () {
return this.value + ' Players';
}
}
}, {
opposite: true,
labels: {
formatter: function () {
return this.value + ' TPS';
}
}
}],
legend: {
enabled: true
},
series: [tpsSeries, playersOnlineSeries]
}));
}
function worldChart(id, entitySeries, chunkSeries, playersOnlineSeries) {
graphs.push(Highcharts.stockChart(id, {
rangeSelector: {
selected: 1,
buttons: linegraphButtons
},
tooltip: {
split: true
},
title: {text: ''},
plotOptions: {
areaspline: {
fillOpacity: 0.4
}
},
yAxis: [{
labels: {
formatter: function () {
return this.value + ' Players';
}
}
}, {
opposite: true,
labels: {
formatter: function () {
return this.value + ' Entities';
}
}
}, {
labels: {
formatter: function () {
return this.value + ' Chunks';
}
}
}],
legend: {
enabled: true
},
series: [entitySeries, chunkSeries, playersOnlineSeries]
}));
}
function worldMap(id, colorMin, colorMax, mapSeries) {
graphs.push(Highcharts.mapChart(id, {
chart: {
animation: true
},
title: {text: ''},
mapNavigation: {
enabled: true,
enableDoubleClickZoomTo: true
},
colorAxis: {
min: 1,
type: 'logarithmic',
minColor: colorMin,
maxColor: colorMax
},
series: [mapSeries]
}));
}
function worldPie(id, worldSeries, gmSeries) {
const defaultTitle = '';
const defaultSubtitle = 'Click to expand';
const chart = Highcharts.chart(id, {
chart: {
plotBackgroundColor: null,
plotBorderWidth: null,
plotShadow: false,
type: 'pie',
events: {
drilldown: function (e) {
chart.setTitle({text: '' + e.point.name}, {text: ''});
},
drillup: function (e) {
chart.setTitle({text: defaultTitle}, {text: defaultSubtitle});
}
}
},
title: {text: defaultTitle},
subtitle: {
text: defaultSubtitle
},
plotOptions: {
pie: {
allowPointSelect: true,
cursor: 'pointer',
dataLabels: {
enabled: false
},
showInLegend: true
}
},
tooltip: {
formatter: function () {
return '<b>' + this.point.name + ':</b> ' + formatTimeAmount(this.y) + ' (' + this.percentage.toFixed(2) + '%)';
}
},
series: [worldSeries],
drilldown: {
series: gmSeries.map(function (d) {
return {name: d.name, id: d.id, colors: gmPieColors, data: d.data}
})
}
});
graphs.push(chart);
}
function updateGraphs() {
// HighCharts nukes the scrollbar variable from the given parameter
// If the graph doesn't support srollbars (bar, pie and map charts for example)
// This workaround stores a copy of the scrollbar so that it can be set
const scrollbar = {...Highcharts.theme.scrollbar};
function updateGraph(graph, index, array) {
// Empty objects can be left in the array if existing graph is re-rendered
if (Object.keys(graph).length === 0) {
array.splice(index, 1);
return;
}
// scrollbar workaround
if (!Highcharts.theme["scrollbar"]) Highcharts.theme["scrollbar"] = {...scrollbar};
graph.update(Highcharts.theme);
}
graphs.forEach(updateGraph);
}

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