From 83663a786b66520c352feaedadf1b4cf09bf9eda Mon Sep 17 00:00:00 2001 From: carbotaniuman <41451839+carbotaniuman@users.noreply.github.com> Date: Thu, 11 Jun 2020 16:06:57 -0500 Subject: [PATCH 1/7] Squash a lot of things --- src/main/java/mdnet/base/MangaDexClient.java | 79 ++-- src/main/java/mdnet/base/ServerHandler.java | 1 + .../base/{ => settings}/ClientSettings.java | 22 +- .../java/mdnet/base/settings/WebSettings.java | 25 ++ src/main/java/mdnet/webui/WebConsole.java | 107 +++++ src/main/kotlin/mdnet/base/Application.kt | 7 +- src/main/resources/webui/dataReceive.js | 405 ++++++++++++++++++ src/main/resources/webui/icons/console.png | Bin 0 -> 675 bytes src/main/resources/webui/icons/dashboard.png | Bin 0 -> 756 bytes src/main/resources/webui/icons/info.png | Bin 0 -> 723 bytes src/main/resources/webui/icons/options.png | Bin 0 -> 793 bytes src/main/resources/webui/icons/showmore.png | Bin 0 -> 374 bytes src/main/resources/webui/index.html | 149 +++++++ src/main/resources/webui/layout.css | 372 ++++++++++++++++ src/main/resources/webui/themes/darkTheme.css | 167 ++++++++ .../resources/webui/themes/eyekillerTheme.css | 97 +++++ .../resources/webui/themes/lightTheme.css | 166 +++++++ .../resources/webui/themes/midnightTheme.css | 163 +++++++ .../resources/webui/themes/sharpStyle.css | 22 + src/main/resources/webui/themes/softStyle.css | 145 +++++++ 20 files changed, 1897 insertions(+), 30 deletions(-) rename src/main/java/mdnet/base/{ => settings}/ClientSettings.java (76%) create mode 100644 src/main/java/mdnet/base/settings/WebSettings.java create mode 100644 src/main/java/mdnet/webui/WebConsole.java create mode 100644 src/main/resources/webui/dataReceive.js create mode 100644 src/main/resources/webui/icons/console.png create mode 100644 src/main/resources/webui/icons/dashboard.png create mode 100644 src/main/resources/webui/icons/info.png create mode 100644 src/main/resources/webui/icons/options.png create mode 100644 src/main/resources/webui/icons/showmore.png create mode 100644 src/main/resources/webui/index.html create mode 100644 src/main/resources/webui/layout.css create mode 100644 src/main/resources/webui/themes/darkTheme.css create mode 100644 src/main/resources/webui/themes/eyekillerTheme.css create mode 100644 src/main/resources/webui/themes/lightTheme.css create mode 100644 src/main/resources/webui/themes/midnightTheme.css create mode 100644 src/main/resources/webui/themes/sharpStyle.css create mode 100644 src/main/resources/webui/themes/softStyle.css diff --git a/src/main/java/mdnet/base/MangaDexClient.java b/src/main/java/mdnet/base/MangaDexClient.java index b02ba49..e818258 100644 --- a/src/main/java/mdnet/base/MangaDexClient.java +++ b/src/main/java/mdnet/base/MangaDexClient.java @@ -1,15 +1,15 @@ package mdnet.base; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import mdnet.base.settings.ClientSettings; import mdnet.cache.DiskLruCache; +import mdnet.webui.WebConsole; import org.http4k.server.Http4kServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; +import java.io.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -171,36 +171,63 @@ public class MangaDexClient { + ") initializing\n"); System.out.println("Copyright (c) 2020, MangaDex Network"); + String file = "settings.json"; + if (args.length == 1) { + file = args[0]; + } else if (args.length != 0) { + MangaDexClient.dieWithError("Expected one argument: path to config file, or nothing"); + } + + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + ClientSettings settings; + try { - String file = "settings.json"; - if (args.length == 1) { - file = args[0]; - } else if (args.length != 0) { - MangaDexClient.dieWithError("Expected one argument: path to config file, or nothing"); + settings = gson.fromJson(new FileReader(file), ClientSettings.class); + } catch (FileNotFoundException ignored) { + settings = new ClientSettings(); + LOGGER.warn("Settings file {} not found, generating file", file); + try (FileWriter writer = new FileWriter(file)) { + writer.write(gson.toJson(settings)); + } catch (IOException e) { + MangaDexClient.dieWithError(e); } + } - ClientSettings settings = new Gson().fromJson(new FileReader(file), ClientSettings.class); + if (!ClientSettings.isSecretValid(settings.getClientSecret())) + MangaDexClient.dieWithError("Config Error: API Secret is invalid, must be 52 alphanumeric characters"); - if (!ClientSettings.isSecretValid(settings.getClientSecret())) - MangaDexClient.dieWithError("Config Error: API Secret is invalid, must be 52 alphanumeric characters"); + if (settings.getClientPort() == 0) { + MangaDexClient.dieWithError("Config Error: Invalid port number"); + } - if (settings.getClientPort() == 0) { - MangaDexClient.dieWithError("Config Error: Invalid port number"); - } + if (settings.getMaxCacheSizeMib() < 1024) { + MangaDexClient.dieWithError("Config Error: Invalid max cache size, must be >= 1024 MiB (1GiB)"); + } - if (settings.getMaxCacheSizeMib() < 1024) { - MangaDexClient.dieWithError("Config Error: Invalid max cache size, must be >= 1024 MiB (1GiB)"); - } + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Client settings loaded: {}", settings); + } - if (LOGGER.isInfoEnabled()) { - LOGGER.info("Client settings loaded: {}", settings); - } + MangaDexClient client = new MangaDexClient(settings); + Runtime.getRuntime().addShutdownHook(new Thread(client::shutdown)); + client.runLoop(); - MangaDexClient client = new MangaDexClient(settings); - Runtime.getRuntime().addShutdownHook(new Thread(client::shutdown)); - client.runLoop(); - } catch (FileNotFoundException e) { - MangaDexClient.dieWithError(e); + if (settings.getWebSettings() != null) { + // java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + // System.setOut(new java.io.PrintStream(out)); + // TODO: system.out redirect + ClientSettings finalSettings = settings; + new Thread(() -> { + WebConsole webConsole = new WebConsole(finalSettings.getWebSettings().getClientWebsocketPort()) { + @Override + protected void parseMessage(String message) { + System.out.println(message); + // TODO: something happens here + // the message should be formatted in json + } + }; + // TODO: webConsole.sendMessage(t,m) whenever system.out is written to + }).start(); } } diff --git a/src/main/java/mdnet/base/ServerHandler.java b/src/main/java/mdnet/base/ServerHandler.java index a6a3e00..a639bde 100644 --- a/src/main/java/mdnet/base/ServerHandler.java +++ b/src/main/java/mdnet/base/ServerHandler.java @@ -3,6 +3,7 @@ package mdnet.base; import kong.unirest.HttpResponse; import kong.unirest.Unirest; import kong.unirest.json.JSONObject; +import mdnet.base.settings.ClientSettings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/mdnet/base/ClientSettings.java b/src/main/java/mdnet/base/settings/ClientSettings.java similarity index 76% rename from src/main/java/mdnet/base/ClientSettings.java rename to src/main/java/mdnet/base/settings/ClientSettings.java index fdf55b2..8e854c5 100644 --- a/src/main/java/mdnet/base/ClientSettings.java +++ b/src/main/java/mdnet/base/settings/ClientSettings.java @@ -1,4 +1,4 @@ -package mdnet.base; +package mdnet.base.settings; import com.google.gson.annotations.SerializedName; @@ -18,15 +18,28 @@ public final class ClientSettings { private final String clientSecret; @SerializedName("threads") private final int threads; + @SerializedName("web_settings") + private final WebSettings webSettings; + + public ClientSettings() { + this.maxCacheSizeMib = 20480; + this.maxBandwidthMibPerHour = 0; + this.maxBurstRateKibPerSecond = 0; + this.clientPort = 1200; + this.clientSecret = "PASTE-YOUR-SECRET-HERE"; + this.threads = 32; + this.webSettings = new WebSettings(); + } public ClientSettings(long maxCacheSizeMib, long maxBandwidthMibPerHour, long maxBurstRateKibPerSecond, - int clientPort, String clientSecret, int threads) { + int clientPort, String clientSecret, int threads, WebSettings webSettings) { this.maxCacheSizeMib = maxCacheSizeMib; this.maxBandwidthMibPerHour = maxBandwidthMibPerHour; this.maxBurstRateKibPerSecond = maxBurstRateKibPerSecond; this.clientPort = clientPort; this.clientSecret = Objects.requireNonNull(clientSecret); this.threads = threads; + this.webSettings = webSettings; } public long getMaxCacheSizeMib() { @@ -48,9 +61,12 @@ public final class ClientSettings { public String getClientSecret() { return clientSecret; } + public WebSettings getWebSettings() { + return webSettings; + } public int getThreads() { - return (threads > 0) ? threads : 16; + return threads; } @Override diff --git a/src/main/java/mdnet/base/settings/WebSettings.java b/src/main/java/mdnet/base/settings/WebSettings.java new file mode 100644 index 0000000..7708789 --- /dev/null +++ b/src/main/java/mdnet/base/settings/WebSettings.java @@ -0,0 +1,25 @@ +package mdnet.base.settings; + +import com.google.gson.annotations.SerializedName; + +public final class WebSettings { + @SerializedName("client_websocket_port") + private final int clientWebsocketPort; + + public WebSettings() { + this.clientWebsocketPort = 33333; + } + + public WebSettings(int clientWebsocketPort) { + this.clientWebsocketPort = clientWebsocketPort; + } + + public int getClientWebsocketPort() { + return clientWebsocketPort; + } + + @Override + public String toString() { + return "WebSettings{" + "clientWebsocketPort=" + clientWebsocketPort + '}'; + } +} diff --git a/src/main/java/mdnet/webui/WebConsole.java b/src/main/java/mdnet/webui/WebConsole.java new file mode 100644 index 0000000..9515793 --- /dev/null +++ b/src/main/java/mdnet/webui/WebConsole.java @@ -0,0 +1,107 @@ +package mdnet.webui; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicReference; +import mdnet.base.Statistics; +import org.java_websocket.WebSocket; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.server.WebSocketServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class WebConsole extends WebSocketServer { + + private final static Logger LOGGER = LoggerFactory.getLogger(WebConsole.class); + + public WebConsole(int port) { + super(new InetSocketAddress(port)); + } + + @Override + public void onOpen(WebSocket conn, ClientHandshake handshake) { + LOGGER.info("Webclient {} connected", conn); + } + + @Override + public void onClose(WebSocket conn, int code, String reason, boolean remote) { + LOGGER.info("Webclient {} disconnected: {} ", conn, reason); + } + + @Override + public void onMessage(WebSocket conn, String message) { + parseMessage(message); + } + + @Override + public void onMessage(WebSocket conn, ByteBuffer message) { + // parseMessage(message.array().toString()); + } + + @Override + public void onError(WebSocket conn, Exception ex) { + ex.printStackTrace(); + if (conn != null) { + // some errors like port binding failed may not be assignable to a specific + // websocket + } + } + + @Override + public void onStart() { + LOGGER.info("Listening for connections on port: {}", this.getPort()); + setConnectionLostTimeout(0); + setConnectionLostTimeout(100); + } + + protected abstract void parseMessage(String message); + + // void parseCommand(String x) { + // switch (x) { + // case "help": + // this.broadcast(formatMessage("command", "Available commands:")); + // this.broadcast(formatMessage("command", "you")); + // this.broadcast(formatMessage("command", "are")); + // this.broadcast(formatMessage("command", "big")); + // this.broadcast(formatMessage("command", "gay")); + // break; + // case "stop": + // this.broadcast(formatMessage("command", "Mangadex Client has shut down, + // shutting down web client now")); + // return; + // default: + // this.broadcast(formatMessage("command", "That command was not recognized")); + // this.broadcast(formatMessage("command", "Try help for a list of available + // commands")); + // break; + // } + // } + + public void sendMessage(String type, Object message) { +// JSONObject out = new JSONObject(); +// switch (type) { +// case "command" : +// out.put("type", "command"); +// out.put("data", message.toString()); +// break; +// case "stats" : +// out.put("type", "stats"); +// AtomicReference temp = (AtomicReference) message; +// out.put("hits", temp.get().getCacheHits()); +// out.put("misses", temp.get().getCacheMisses()); +// out.put("bytes_sent", temp.get().getBytesSent()); +// out.put("req_served", temp.get().getRequestsServed()); +// out.put("dataval", "empty"); +// out.put("dataval", "empty"); +// out.put("dataval", "empty"); +// break; +// case "auth" : +// break; +// default : +// out.put("type", "command"); +// out.put("data", message.toString()); +// break; +// } +// broadcast(out.toString()); + } +} diff --git a/src/main/kotlin/mdnet/base/Application.kt b/src/main/kotlin/mdnet/base/Application.kt index 5820604..fbe4b3a 100644 --- a/src/main/kotlin/mdnet/base/Application.kt +++ b/src/main/kotlin/mdnet/base/Application.kt @@ -1,6 +1,7 @@ /* ktlint-disable no-wildcard-imports */ package mdnet.base +import mdnet.base.settings.ClientSettings import mdnet.cache.DiskLruCache import org.apache.http.client.config.CookieSpecs import org.apache.http.client.config.RequestConfig @@ -18,8 +19,10 @@ import org.http4k.filter.CachingFilters import org.http4k.filter.MaxAgeTtl import org.http4k.filter.ServerFilters import org.http4k.lens.Path +import org.http4k.routing.ResourceLoader import org.http4k.routing.bind import org.http4k.routing.routes +import org.http4k.routing.singlePageApp import org.http4k.server.Http4kServer import org.http4k.server.asServer import org.slf4j.LoggerFactory @@ -211,7 +214,9 @@ fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSetting "/data/{chapterHash}/{fileName}" bind Method.GET to app(false), "/data-saver/{chapterHash}/{fileName}" bind Method.GET to app(true), "/{token}/data/{chapterHash}/{fileName}" bind Method.GET to app(false), - "/{token}/data-saver/{chapterHash}/{fileName}" bind Method.GET to app(true) + "/{token}/data-saver/{chapterHash}/{fileName}" bind Method.GET to app(true), + + singlePageApp(ResourceLoader.Classpath("/webui")) ) ) .asServer(Netty(serverSettings.tls, clientSettings, statistics)) diff --git a/src/main/resources/webui/dataReceive.js b/src/main/resources/webui/dataReceive.js new file mode 100644 index 0000000..a23255a --- /dev/null +++ b/src/main/resources/webui/dataReceive.js @@ -0,0 +1,405 @@ +let connection; +let theme; +let style; +let port; +let ip; +let refreshRate; +let maxConsoleLines; +let graphTimeFrame; +let showConsoleLatest; +let doAnimations; +//non-option var +let statRequest; +//stat vars + + +jQuery(document).ready(function () { + loadOptions(); + $("#theme").attr("href", "themes/" + theme + ".css"); + $("#style").attr("href", "themes/" + style + ".css"); + if (doAnimations) { + $(".optionInput").addClass("smooth"); + $(".slider").addClass("smoothslider").addClass("smooth"); + $(".content").addClass("slide_up"); + $(".sideOption").addClass("smooth"); + $(".button").addClass("smooth"); + } + if (showConsoleLatest) + $("#consoleLatest").attr("hidden", false); + reconnect(); + $("#console_input").keyup(function (e) { + if (e.keyCode === 13) { + sendCommand($(this).text()); + $(this).text(""); + $('#console_text').scrollTop($("#console_text")[0].scrollHeight) + } + }) +}); + +//site functions, no connections involved + +$(window).on("click", function () { + let sideBar = $("#sideBar"); + if (sideBar.hasClass("expanded")) { + sideBar.removeClass("expanded").addClass("retract").on( + "animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", + function () { + $(this).removeClass("retract").removeClass("expanded"); + } + ); + } +}); + +function loadOptions() { + let options = JSON.parse(localStorage.getItem("options")); + if (options === null) { + options = { + refresh_rate: 5000, + theme: "lightTheme", + style: "sharpStyle", + client_port: 33333, + client_ip: "localhost", + max_console_lines: 1000, + show_console_latest: false, + graph_time_frame: 30000, + do_animations: true + } + } + theme = options.theme; + style = options.style; + port = options.client_port; + ip = options.client_ip; + refreshRate = options.refresh_rate; + maxConsoleLines = options.max_console_lines; + graphTimeFrame = options.graph_time_frame; + showConsoleLatest = options.show_console_latest; + doAnimations = options.do_animations; + $("#dataRefreshRate").val(refreshRate); + $("#port").val(port); + $("#ip").val(ip); + $("#maxConsoleLines").val(maxConsoleLines); + $("#graphTimeFrame").val(graphTimeFrame); + $("#themeIn").val(theme); + $("#styleIn").val(style); + $("#newestconsole").prop("checked", showConsoleLatest); + $("#doAnimations").prop("checked", doAnimations); +} + +function resetOptions() { + if (confirm("Do you really want to reset to defaults?")) { + $("#dataRefreshRate").val(5000); + $("#port").val(33333); + $("#ip").val("localhost"); + $("#maxConsoleLines").val(1000); + $("#graphTimeFrame").val(30000); + $("#themeIn").val("lightTheme"); + $("#styleIn").val("sharpStyle"); + $("#newestconsole").prop("checked", false); + $("#doAnimations").prop("checked", true); + applyOptions() + } +} + +function applyOptions() { + let options = { + refresh_rate: parseInt($("#dataRefreshRate").val()), + theme: $("#themeIn").val(), + style: $("#styleIn").val(), + client_port: parseInt($("#port").val()), + client_ip: $("#ip").val(), + max_console_lines: parseInt($("#maxConsoleLines").val()), + show_console_latest: $("#newestconsole").prop("checked"), + graph_time_frame: parseInt($("#graphTimeFrame").val()), + do_animations: $("#doAnimations").prop("checked") + }; + if (options.do_animations !== doAnimations) { + doAnimations = options.do_animations; + if (doAnimations) { + $(".optionInput").addClass("smooth"); + $(".slider").addClass("smoothslider").addClass("smooth"); + $(".content").addClass("slide_up"); + $(".sideOption").addClass("smooth"); + $(".button").addClass("smooth"); + } else { + $(".optionInput").removeClass("smooth"); + $(".slider").removeClass("smoothslider").removeClass("smooth"); + $(".content").removeClass("slide_up"); + $(".sideOption").removeClass("smooth"); + $(".button").removeClass("smooth"); + } + $("#doAnimationscb").addClass("updated").on( + "animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", + function () { + $(this).removeClass("updated"); + } + ).prop("checked", doAnimations); + } + if (options.refresh_rate !== refreshRate) { + console.log(options.refresh_rate + " " + refreshRate); + refreshRate = Math.max(options.refresh_rate, 500); + $("#dataRefreshRate").addClass("updated").on( + "animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", + function () { + $(this).removeClass("updated"); + } + ).val(refreshRate); + } + if (options.style !== style) { + style = options.style; + applyStyle(options.style); + $("#styleIn").addClass("updated").on( + "animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", + function () { + $(this).removeClass("updated"); + } + ); + } + if (options.theme !== theme) { + theme = options.theme; + applyTheme(options.theme); + $("#themeIn").addClass("updated").on( + "animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", + function () { + $(this).removeClass("updated"); + } + ); + } + if (options.client_port !== port) { + port = options.client_port; + $("#port").addClass("updated").on( + "animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", + function () { + $(this).removeClass("updated"); + } + ).val(port); + reconnect(); + } + if (options.client_ip !== ip) { + ip = options.client_ip; + $("#ip").addClass("updated").on( + "animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", + function () { + $(this).removeClass("updated"); + } + ).val(ip); + reconnect(); + } + if (options.graph_time_frame !== graphTimeFrame) { + graphTimeFrame = Math.max(options.graph_time_frame, 5000); + $("#graphTimeFrame").addClass("updated").on( + "animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", + function () { + $(this).removeClass("updated"); + } + ).val(graphTimeFrame); + } + if (options.max_console_lines !== maxConsoleLines) { + maxConsoleLines = Math.max(options.max_console_lines, 100); + $("#maxConsoleLines").addClass("updated").on( + "animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", + function () { + $(this).removeClass("updated"); + } + ).val(maxConsoleLines); + } + if (options.show_console_latest !== showConsoleLatest) { + showConsoleLatest = options.show_console_latest; + if (showConsoleLatest) + $("#consoleLatest").attr("hidden", false); + else + $("#consoleLatest").attr("hidden", true); + $("#newestconsolecb").addClass("updated").on( + "animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", + function () { + $(this).removeClass("updated"); + } + ).prop("checked", showConsoleLatest); + } + localStorage.setItem("options", JSON.stringify(options)); +} + +function selectTab(t, l) { + let sideBar = $("#sideBar"); + sideBar.children("div").each(function () { + let tmp = $(this); + if (tmp.attr("id") === t) { + tmp.addClass("sideSelected"); + } else + tmp.removeClass("sideSelected"); + }); + $("#content").children("div").each(function () { + let tmp = $(this); + if (tmp.attr("id") === l) { + tmp.attr("hidden", false); + } else + tmp.attr("hidden", true); + }); + if (sideBar.hasClass("expanded")) { + sideBar.removeClass("expanded").addClass("retract").on( + "animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", + function () { + $(this).removeClass("retract").removeClass("expanded"); + } + ); + } +} + +function expSide() { + let sideBar = $("#sideBar"); + if (sideBar.hasClass("expanded")) { + sideBar.removeClass("expanded").addClass("retract").on( + "animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", + function () { + $(this).removeClass("retract").removeClass("expanded"); + } + ); + } else { + sideBar.addClass("expand").on( + "animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", + function () { + $(this).addClass("expanded").removeClass("expand"); + } + ); + } + +} + +function applyTheme(t) { + if (doAnimations) + $("*").each(function () { + if (!$(this).attr("hidden")) + $(this).addClass("tempsmooth").on( + "webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend", + function () { + $(this).removeClass("tempsmooth"); + } + ); + }); + $("#theme").attr("href", "themes/" + t + ".css"); +} + +function applyStyle(s) { + if (doAnimations) + $("*").each(function () { + if (!$(this).attr("hidden")) + $(this).addClass("tempsmooth").on( + "webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend", + function () { + $(this).removeClass("tempsmooth"); + } + ); + }); + $("#style").attr("href", "themes/" + s + ".css"); +} + +//update data functions + +function updateWithMessage(m) { + //TODO: get this to talk with client + let result; + try { + result = JSON.parse(m); + switch (result.type) { + case "command": + updateConsole(result.data, 2); + break; + case "stats": + + updateValues(); + break; + default: + updateConsole("[WEB-INFO] The message received is improperly formatted: " + result.data, 2); + break; + } + } catch (e) { + updateConsole("[WEB-INFO] There was an error parsing the data \n" + e, 2); + } +} + +function updateValues() { + //TODO: use values and update web info +} + +//console functions + +function updateConsole(x, status) { + let scroll = false; + let temp = $('#console_text'); + let latest = $("#consoleLatest"); + if (temp.scrollTop() === (temp[0].scrollHeight - temp[0].clientHeight)) + scroll = true; + switch (status) { + case 1: + temp.append('
' + x + '
'); + break; + case 0: + temp.append('
' + x + '
'); + break; + default: + temp.append('
' + x + '
'); + latest.html('
' + x + '
'); + } + let childs = temp.children(); + if (childs.length > maxConsoleLines) { + let length = childs.length; + for (let i = 0; i < length - maxConsoleLines; i++) { + childs[i].remove(); + } + } + if (scroll) + temp.scrollTop(temp[0].scrollHeight); +} + +function sendCommand(x) { + if (x === "") + return; + if (connection.readyState === "OPEN") { + let data = { + type: "command", + data: x + }; + let message = JSON.stringify(data); + connection.send(message); + } else { + updateConsole(x, 0); + } +} + +//network commuication + +function reconnect() { + if (connection != null) + connection.close(); + updateConsole("[WEB-CONSOLE] Attempting to connect to client on " + ip + ":" + port, 2); + connection = new WebSocket("ws://" + ip + ":" + port); + $("#connection").removeClass("disconnected").removeClass("connected").addClass("connecting").text("Connecting"); + addListeners(connection) +} + +function addListeners(c) { + let opened = false; + c.onopen = function (event) { + $("#connection").removeClass("disconnected").removeClass("connecting").addClass("connected").text("Connected"); + opened = true; + updateConsole("[WEB-CONSOLE] Successfully to connect to client on " + ip + ":" + port, 2); + statRequest = setInterval(function () { + requestStats(); + }, refreshRate); + }; + c.onclose = function (event) { + $("#connection").addClass("disconnected").removeClass("connecting").removeClass("connected").text("Disconnected"); + if (opened) + updateConsole("[WEB-CONSOLE] Disconnected from client"); + else + updateConsole("[WEB-CONSOLE] Failed to connect to client on " + ip + ":" + port, 2); + clearInterval(statRequest); + }; + c.onmessage = function (event) { + updateWithMessage(event.data()); + }; +} + +function requestStats() { + let req = {type: "stats"}; + connection.send(JSON.stringify(req)); +} \ No newline at end of file diff --git a/src/main/resources/webui/icons/console.png b/src/main/resources/webui/icons/console.png new file mode 100644 index 0000000000000000000000000000000000000000..560c4e48634e75b8d4d45630f5b7efc9c77d0c5f GIT binary patch literal 675 zcmV;U0$lxxP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!TaidC%m8|UR1Ln zJOfWq^E~hRNy;L+^`{hgO+3&YW1gdFBT!4xUVxt0A!Kp?fGDOTixMAV@A4*#KDY9L}2rT`p&Y9L}6M*Zo5h(&bD z_g{!mJbI%?ss;K%@^u z1oTEvun>xBy*u*2k1il*PVp7@4{&dQ{=dWd=$3APe00h%d;t@9uJL2>!omOm002ov JPDHLkV1gmp7sCJm literal 0 HcmV?d00001 diff --git a/src/main/resources/webui/icons/dashboard.png b/src/main/resources/webui/icons/dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..e314766b0b5307ddd1e7fe744562a842e3affbb1 GIT binary patch literal 756 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGf6951U69E94oEQKA0*OgPK~#8N?Usph z!ypVqlm7odyR0f@Bq1=^iHQquhG5L4T@L55Sj^2crS0!=PJTF_mSq6v)8g*{l>a;C z&Ra)-^J!UkKuYU>z|1TIIG>jFKY;UTSq5-EEw?sc+vqssCp~UbCVaWXoiD*qT@w9F zWa9CTF%@^+0mY?FLX-Alb;h0n7)d9Ouh;2Qn6a_qNL=jZZaW|o&yVIAyMP~exr+hC zU4^r)&UZIoVE~_55{YLngQ9RC=N(C}dMfMPz)vx#)e{`I@;F6-5fG=vMu-6-o(-bs zgy}na2OJS^R@f8ODe)P@48W>r5O_&gROjDCZCXGF9ZB~9CwR^415YbftD`)2>yk%m43bbg&{`(tEX2s1)hreZLW#L9TDsqVn9I*Zn#W5 zzvt5X!N_{j&&?E2Tk(K*#b)Lk{TZaAfW$>}G#sr)bun}Y0PKQ@%FZ|&qZxO&0f3JJ zmkggUq00atz+S!POpuz|92%NWc2wo!G~*4w0~%;G(g=eQrQ@Z!g8|@>2`${kfQ+=d z^z}gES478Wl|X0Rq1EZLL_yNrvTfBF&f1D-X@fpmxHcuY3(R;Uc0eQ^yxy}INg;u- zLtJe@Q4kaU9X>*j-c@Ez=vYSS)7d@%RN^0zfDgc9M4h0000Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!T z!!QU%o8AA;U8u}NDaP0a>@0oKl&WfN{sU*(j_33FxHI7bj^mix;H>W`FGC0sFdMo? zgbY^!QNcN$B3uNtf{zv)wJv2!fL5Sqmw>3fh6U6L@(#}N-nU!8s^A>2I=Tc%n`Q^+ z%+~Z%u1$6f4@foCrbEIs0lS9h@H#CuHU*3h*b`81f{{Q$^ryU?&P%|Z#-H>0Ymf?v=*GEz zdDREh<_*u9CZIRW$agF(7eM!th}fUPv!>kyL4prz|Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!TK))sP_JNXytm3o469=VY=Gi!4Js^hE@%t%RwZBQQ8*(FsEKX|^omcne_+O<^a??X z_U0cNASFyOvxV27d0EYr0jrDJ1*3cT4!BCeBfJ`@b3m)T&IYT|HQ>r!fM}mS37!c> z8)U#0y8zYfo&!oshgDO&Ra%u7%>!?N+19ssBnLWf74$p6U{*;jh>epw=O-}T_Nj%S zleG%)VwISTqLc1`88Jv~Yh25e7O1w8ZU^)xMJFla6=8;wV`Nq<$FBh^U0zXqhRwy+ z@!A4jkPXgPdURS?t*;Jf8K4%sa`iJSa@IBGJZ~>S`(u^xGtsL_ItTnGJ_e-U+ww6W z{oa;01MG8=uk`5hjOyv016m-T$2!;SCsoOZ)RXhP3@Gt}Sph2zSW$eP<82GrfPAG# zQy{C+V>z7e(s&dSZVz~k`^ Xph0UhCyc}w00000NkvXXu0mjf&!1jF literal 0 HcmV?d00001 diff --git a/src/main/resources/webui/icons/showmore.png b/src/main/resources/webui/icons/showmore.png new file mode 100644 index 0000000000000000000000000000000000000000..839745f1bf49e2de5aef24c35070b437fac9441f GIT binary patch literal 374 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc4cwkCjv*CsZ)Z4i9X8-$dHnDH`8?*;-VPPj zTuJE*a;je(RE{j$^zB}_k$kESN5HWr24w2f_J*z3_Syfkl>42yBKphw|K9QLNt_m; ze^ef>?JMj$%oDK9;_7T0;TH_4-JAQ>_z(TydmvH5Dx#Y7>k#)nF70H;1|u;Rjof#^ z37@Wh=lss#eqiMzb + + + + MD@H Client + + + + + + + +
+ + mangadex + +

MangaDex@Home Client Interface

+ + +
+ +
+
+
+

Dashboard

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+ + \ No newline at end of file diff --git a/src/main/resources/webui/layout.css b/src/main/resources/webui/layout.css new file mode 100644 index 0000000..171f9f9 --- /dev/null +++ b/src/main/resources/webui/layout.css @@ -0,0 +1,372 @@ +body { + margin: 0; + font-family: Calibri, serif; + overflow: hidden; +} + +.smooth { + -webkit-transition: .4s; + transition: .4s; +} + +.tempsmooth { + -webkit-transition: .4s; + transition: .4s; +} + +/*Content holder positions*/ + +#pageBar { + height: 75px; + width: 100%; + position: absolute; +} + +#consoleLatest { + position: absolute; + min-height: 20px; + width: calc(100% - 755px); + margin: 30px 20px 25px 20px; + left: 545px; + overflow-x: scroll; + overflow-y: hidden; +} + +#consoleLatest::-webkit-scrollbar { + height: 3px; +} + +#connection { + position: absolute; + right: 0; + margin: 20px 20px 20px 20px; + width: 150px; + height: 35px; + border-style: solid; + outline: none; +} + +.connecting { + animation: connecting 1.5s; + animation-iteration-count: infinite; +} + +@keyframes connecting { + 0%, 100% { + filter: brightness(120%) + } + 50% { + filter: brightness(80%) + } +} + +#sideBar { + height: calc(100% - 75px); + width: 50px; + top: 75px; + position: absolute; + z-index: 10; + overflow: hidden; + user-select: none; +} + +.sideOption { + width: 100%; + height: 50px; + float: left;; +} + +.expand { + animation: expand 150ms ease-out; + -webkit-animation-fill-mode: forwards; +} + +.expanded { + width: 200px !important; +} + +.retract { + animation: expand 150ms reverse ease-in; + -webkit-animation-fill-mode: forwards; +} + +@keyframes expand { + 0% { + width: 50px + } + 100% { + width: 200px + } +} + + +#content { + height: calc(100% - 75px); + width: calc(100% - 50px); + top: 75px; + left: 50px; + position: absolute; + /*overflow-y: auto;*/ +} + +.contentHeader { + width: calc(100% - 40px); + height: 80px; + margin: 20px; +} + +/*Main dashboard positions*/ + +#dashb { + width: 100%; + height: 100%; + position: absolute; +} + +#nDat { + width: 40%; + height: calc(100% - 140px); + margin: 20px; + top: 100px; + position: absolute; + +} + +.numerical_data { + height: 150px; + width: calc(50% - 20px); + margin: 10px; + float: left; +} + +#gDat { + height: calc(100% - 140px); + width: calc(60% - 60px); + margin: 20px; + position: absolute; + top: 100px; + left: calc(40% + 20px); + overflow-y: scroll; +} + +.line_graph_data { + height: 200px; + width: calc((100% - 20px)); + margin: 10px; + float: left; +} + +/*Console and options positions*/ + +#console { + width: 100%; + height: 100%; + position: absolute; +} + +#buttonBoard { + width: calc(100% - 40px); + height: calc(40% - 20px); + margin: 20px; + position: absolute; + top: 100px; +} + +#liveConsole { + width: calc(100% - 40px); + height: calc(60% - 180px); + margin: 20px; + position: absolute; + top: calc(40% + 100px); + padding-bottom: 40px; + font-family: monospace; +} + +.consoleLine { + width: calc(100% - 5px); + float: left; + margin: 0 5px 0; + left: 0; + white-space: nowrap; +} + +.consoleLine > p { + margin: 0; +} + +#console_input { + position: absolute; + width: calc(100% - 30px); + height: 20px; + bottom: 10px; + left: 0; + margin: 0 15px; + padding-top: 10px; + padding-bottom: 10px; + white-space: nowrap; + border-width: 0; + outline: none; + background-color: inherit; + overflow: hidden; +} + +#console_text { + height: calc(100% - 40px); + width: calc(100% - 20px); + position: absolute; + outline: none; + border-width: 0; + resize: none; + font-family: monospace; + padding-left: 10px; + padding-right: 10px; + padding-bottom: 40px; + background-color: rgba(0, 0, 0, 0); + overflow: scroll; +} + +/*Web option positions*/ +#dashOptions { + width: 100%; + height: 100%; + position: absolute; +} + +#options { + width: calc(100% - 80px); + height: calc(100% - 140px); + position: absolute; + top: 100px; + margin: 20px 20px 20px 60px; +} + +#apply { + position: fixed; + bottom: 20px; + right: 20px; + width: 150px; + height: 30px; +} + +#reset{ + position: fixed; + bottom: 20px; + right: 190px; + width: 150px; + height: 30px; +} + +.option { + height: 40px; + margin: 10px; +} + +.option > h4 { + margin: 0; + float: left; + font-weight: normal; +} + +.optionLabel { + +} + +.optionInput { + left: 200px; + position: absolute; + border-style: solid; + border-width: 2px; +} + +.switchInput > span { + left: 200px; + position: absolute; + border-style: solid; +} + +.updated { + animation: fade 1.5s linear; +} + +@keyframes fade { + 0%, 100% { + filter: alpha(100%); + } + 100% { + filter: alpha(0%); + } +} + +/*misc modifications*/ + +.input { + outline: 0; +} + +.button { + outline: none; + border-width: 2px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + +} + +.slider:before { + /*border-width: 1px;*/ + position: absolute; + content: ""; +} + +.smoothslider:before { + position: absolute; + content: ""; + -webkit-transition: .4s; + transition: .4s; +} + +/*Webkit modifications*/ + +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-button { + width: 0; + height: 0; +} + +::-webkit-scrollbar-thumb { +} + +::-webkit-scrollbar-track { + +} + +::-webkit-scrollbar-corner { + background-color: rgba(0, 0, 0, 0); +} + +/*animations*/ + +.slide_up { + animation: slideup .1s ease-out; +} + +@keyframes slideup { + 0% { + transform: translateY(50px); + opacity: 0; + } + 100% { + transform: translateY(0px); + opacity: 1; + } +} \ No newline at end of file diff --git a/src/main/resources/webui/themes/darkTheme.css b/src/main/resources/webui/themes/darkTheme.css new file mode 100644 index 0000000..cf2485d --- /dev/null +++ b/src/main/resources/webui/themes/darkTheme.css @@ -0,0 +1,167 @@ +body { + background-color: #404040; + color: #f0f0f0; +} + +#pageBar { + background-color: #303030; +} + +#consoleLatest{ + background-color: black; + color: #f0f0f0; +} + +.connected { + border-color: #0fff00 !important; +} + +.disconnected { + border-color: #e50100 !important; +} + +.connecting { + border-color: #e5d700 !important; +} + +#sideBar { + background-color: #303030; +} + +.sideOption { + background-color: #303030; +} + +.sideOption:hover { + background-color: #404040; +} + +.sideSelected { + background-color: #606060; +} + +#content { + background-color: #404040; +} + +.contentHeader { + background-color: #606060; +} + +/*Main dashboard colors*/ + +#dashb { + +} + +#nDat { +} + +.numerical_data { + background-color: #606060; +} + +#gDat { +} + +.line_graph_data { + background-color: #606060; +} + +/*Console and options colors*/ + +#liveConsole { + background-color: black; + caret-color: #f0f0f0; +} + +#console_text { + color: #f0f0f0; +} + +#console_input { + color: #f0f0f0; +} + +.unsent { + color: #e50100; +} + +.sent { + color: #0fff00; +} + + +/*Web option colors*/ +#dashOptions { +} + +#options { +} + +.option { +} + +.optionLabel { + +} + +.optionInput { + border-color: rgba(0, 0, 0, 0); + background-color: #606060; + color: #f0f0f0; +} + +/*misc*/ + +.button{ + border-color: rgba(0, 0, 0, 0); + background-color: #606060; + color: #f0f0f0; +} + +.button:hover{ + background-color: #909090; +} + +.img { + filter: invert(100%); +} + +.updated { + border-color: #1ec70d !important; +} + +.slider { + border-color: rgba(0, 0, 0, 0); + background-color: #606060; +} + +.slider::before{ + background-color: #f0f0f0; +} + +input:checked + .slider { + background-color: #909090; +} + +/*Webkit colors*/ + +::-webkit-scrollbar { +} + +::-webkit-scrollbar-button { + +} + +::-webkit-scrollbar-thumb { + background-color: #555555; +} + +::-webkit-scrollbar-thumb:hover { + background-color: #888888; +} + +::-webkit-scrollbar-track { + +} diff --git a/src/main/resources/webui/themes/eyekillerTheme.css b/src/main/resources/webui/themes/eyekillerTheme.css new file mode 100644 index 0000000..d020138 --- /dev/null +++ b/src/main/resources/webui/themes/eyekillerTheme.css @@ -0,0 +1,97 @@ +body { + background-color: #ffffff; + color: #202020; +} + +#pageBar { + background-color: #faff00; +} + +#sideBar { + background-color: #faff00; +} + +.sideOption { + +} + +.sideOption:hover { + background-color: #faff00; +} + +.sideSelected { + background-color: #faff00; +} + +#content { + background-color: #0fff00; +} + +.contentHeader { + background-color: #ff00af; +} + +/*Main dashboard colors*/ + +#dashb { + +} + +#nDat { +} + +.numerical_data { + background-color: #00ffec; +} + +#gDat { +} + +.line_graph_data { + background-color: #00ffec; +} + +/*Console and options colors*/ + +#liveConsole { + background-color: black; + caret-color: #f0f0f0; +} + +#console_text { + color: #f0f0f0; +} + +#console_input { + color: #f0f0f0; +} + +/*misc*/ + +.img { +} + +.updated { + border-color: #1ec70d; +} + +/*Webkit colors*/ + +::-webkit-scrollbar { +} + +::-webkit-scrollbar-button { + +} + +::-webkit-scrollbar-thumb { + background-color: #aaaaaa; +} + +::-webkit-scrollbar-thumb:hover { + background-color: #888888; +} + +::-webkit-scrollbar-track { + +} diff --git a/src/main/resources/webui/themes/lightTheme.css b/src/main/resources/webui/themes/lightTheme.css new file mode 100644 index 0000000..8076817 --- /dev/null +++ b/src/main/resources/webui/themes/lightTheme.css @@ -0,0 +1,166 @@ +body { + background-color: #ffffff; + color: #202020; +} + +#pageBar { + background-color: #f8f9fa; +} + +#consoleLatest{ + background-color: black; + color: #f0f0f0; +} + +.connected { + border-color: #0fff00 !important; +} + +.disconnected { + border-color: #e50100 !important; +} + +.connecting { + border-color: #e5d700 !important; +} + +#sideBar { + background-color: #f8f9fa; +} + +.sideOption { + +} + +.sideOption:hover { + background-color: #eeeeee; +} + +.sideSelected { + background-color: #e1e1e1; +} + +#content { + background-color: #ffffff; +} + +.contentHeader { + background-color: #ededed; +} + +/*Main dashboard colors*/ + +#dashb { + +} + +#nDat { +} + +.numerical_data { + background-color: #ededed; +} + +#gDat { +} + +.line_graph_data { + background-color: #ededed; +} + +/*Console and options colors*/ + +#liveConsole { + background-color: black; + caret-color: #f0f0f0; +} + +#console_text { + color: #f0f0f0; +} + +#console_input { + color: #f0f0f0; +} + +.unsent { + color: #cb0000; +} + +.sent { + color: #1ec70d; +} + + +/*Web option colors*/ +#dashOptions { +} + +#options { +} + +.option { +} + +.optionLabel { + +} + +.optionInput { + border-color: rgba(0, 0, 0, 0); + background-color: #eaeaea; + color: #202020; +} + +/*misc*/ + +.button{ + border-color: rgba(0, 0, 0, 0); + background-color: #eaeaea; + color: #202020; +} + +.button:hover{ + background-color: #dadada; +} + +.img { +} + +.updated { + border-color: #1ec70d !important; +} + +.slider { + border-color: rgba(0, 0, 0, 0); + background-color: #eaeaea; +} + +.slider::before{ + background-color: #202020; +} + +input:checked + .slider { + background-color: #adadad; +} + +/*Webkit colors*/ + +::-webkit-scrollbar { +} + +::-webkit-scrollbar-button { + +} + +::-webkit-scrollbar-thumb { + background-color: #cacaca; +} + +::-webkit-scrollbar-thumb:hover { + background-color: #dedede; +} + +::-webkit-scrollbar-track { + +} diff --git a/src/main/resources/webui/themes/midnightTheme.css b/src/main/resources/webui/themes/midnightTheme.css new file mode 100644 index 0000000..ea260b0 --- /dev/null +++ b/src/main/resources/webui/themes/midnightTheme.css @@ -0,0 +1,163 @@ +body { + background-color: #101010; + color: #bfbfbf; +} + +#pageBar { + background-color: #202020; +} + +#consoleLatest { + background-color: black; + color: #f0f0f0; +} + +.connected { + border-color: #0fff00 !important; +} + +.disconnected { + border-color: #e50100 !important; +} + +.connecting { + border-color: #e5d700 !important; +} + +#sideBar { + background-color: #202020; +} + +.sideOption:hover { + background-color: #404040; +} + +.sideSelected { + background-color: #505050; +} + +#content { + background-color: #101010; +} + +.contentHeader { + background-color: #404040; +} + +/*Main dashboard colors*/ + +#dashb { + +} + +#nDat { +} + +.numerical_data { + background-color: #404040; +} + +#gDat { +} + +.line_graph_data { + background-color: #404040; +} + +/*Console and options colors*/ + +#liveConsole { + background-color: black; + caret-color: #f0f0f0; +} + +#console_text { + color: #f0f0f0; +} + +#console_input { + color: #f0f0f0; +} + +.unsent { + color: #e50100; +} + +.sent { + color: #0fff00; +} + + +/*Web option colors*/ +#dashOptions { +} + +#options { +} + +.option { +} + +.optionLabel { + +} + +.optionInput { + border-color: rgba(0, 0, 0, 0); + background-color: #404040; + color: #bfbfbf; +} + +/*misc*/ + +.button{ + border-color: rgba(0, 0, 0, 0); + background-color: #404040; + color: #bfbfbf; +} + +.button:hover{ + background-color: #797979; +} + +.img { + filter: invert(100%); +} + +.updated { + border-color: #1ec70d !important; +} + +.slider { + border-color: rgba(0, 0, 0, 0); + background-color: #404040; +} + +.slider::before { + background-color: #bfbfbf; +} + +input:checked + .slider { + background-color: #757575; +} + +/*Webkit colors*/ + +::-webkit-scrollbar { +} + +::-webkit-scrollbar-button { + +} + +::-webkit-scrollbar-thumb { + background-color: #555555; +} + +::-webkit-scrollbar-thumb:hover { + background-color: #888888; +} + +::-webkit-scrollbar-track { + +} diff --git a/src/main/resources/webui/themes/sharpStyle.css b/src/main/resources/webui/themes/sharpStyle.css new file mode 100644 index 0000000..68e5d02 --- /dev/null +++ b/src/main/resources/webui/themes/sharpStyle.css @@ -0,0 +1,22 @@ +.input { + padding-left: 2px; +} + +.slider { + width: 60px; + height: 12px; + margin-top: 2px; +} + +.slider:before { + height: 12px; + width: 30px; + left: 0; + bottom: 0; +} + +input:checked + .slider:before { + -webkit-transform: translateX(30px); + -ms-transform: translateX(30px); + transform: translateX(30px); +} \ No newline at end of file diff --git a/src/main/resources/webui/themes/softStyle.css b/src/main/resources/webui/themes/softStyle.css new file mode 100644 index 0000000..2279813 --- /dev/null +++ b/src/main/resources/webui/themes/softStyle.css @@ -0,0 +1,145 @@ +/*Content holder positions*/ + +#pageBar { +} + +#connection { + border-radius: 10px; +} + +#consoleLatest { + border-radius: 10px; +} + +#sideBar { +} + +.sideSelected { + border-radius: 10px; +} + +.sideOption:hover { + border-radius: 10px; +} + +#expSide { +} + +#dash { +} + +#cons { +} + +#opt { +} + +#content { +} + +.contentHeader { + border-radius: 10px; +} + +/*Main dashboard positions*/ + +#dashb { +} + +#nDat { + +} + +.numerical_data { + border-radius: 10px; +} + +#gDat { + border-radius: 10px; +} + +.line_graph_data { + border-radius: 10px; +} + +/*Console and options positions*/ + +#console { +} + +#buttonBoard { +} + +#liveConsole { + border-radius: 10px; +} + +#console_input { +} + +#console_text { + border-radius: 10px; +} + +/*Web option positions*/ +#dashOptions { +} + +#options { +} + +#apply { + border-radius: 10px; +} + +.option { +} + +.optionLabel { + +} + +.optionInput { +} + +/*misc modifications*/ + +.input { + padding-left: 5px; + border-radius: 10px; +} + +.slider { + border-radius: 30px; + width: 60px; + height: 16px; +} + +.slider:before { + border-radius: 7px; + height: 14px; + width: 28px; + left: 3px; + bottom: 1px; +} + +input:checked + .slider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +/*Webkit modifications*/ + +::-webkit-scrollbar { +} + +::-webkit-scrollbar-button { +} + +::-webkit-scrollbar-thumb { + border-radius: 10px; +} + +::-webkit-scrollbar-track { +} \ No newline at end of file From b56a86495a68090de125f7f6b02b6d95eda00892 Mon Sep 17 00:00:00 2001 From: M Date: Thu, 11 Jun 2020 17:42:42 -0500 Subject: [PATCH 2/7] all the dockables --- src/main/resources/webui/dataReceive.js | 184 +++++++++- src/main/resources/webui/icons/close.png | Bin 0 -> 460 bytes src/main/resources/webui/index.html | 28 +- src/main/resources/webui/layout.css | 338 +++++++++++++++++- src/main/resources/webui/themes/darkTheme.css | 167 +++++++++ .../resources/webui/themes/lightTheme.css | 163 +++++++++ src/main/resources/webui/themes/softStyle.css | 24 +- 7 files changed, 864 insertions(+), 40 deletions(-) create mode 100644 src/main/resources/webui/icons/close.png diff --git a/src/main/resources/webui/dataReceive.js b/src/main/resources/webui/dataReceive.js index a23255a..8f56a07 100644 --- a/src/main/resources/webui/dataReceive.js +++ b/src/main/resources/webui/dataReceive.js @@ -8,13 +8,160 @@ let maxConsoleLines; let graphTimeFrame; let showConsoleLatest; let doAnimations; +let lockDash; //non-option var let statRequest; //stat vars +//dockable things +let config = { + settings: { + hasHeaders: true, + constrainDragToContainer: false, + reorderEnabled: true, + selectionEnabled: false, + popoutWholeStack: false, + blockedPopoutsThrowError: true, + closePopoutsOnUnload: true, + showPopoutIcon: false, + showMaximiseIcon: false, + showCloseIcon: lockDash + }, + dimensions: { + borderWidth: 20, + minItemHeight: 10, + minItemWidth: 10, + headerHeight: 20, + dragProxyWidth: 300, + dragProxyHeight: 200 + }, + labels: { + close: 'close', + maximise: 'maximise', + minimise: 'minimise', + popout: 'open in new window' + }, + content: [{ + type: 'column', + content: [{ + type: 'row', + content: [{ + type: 'column', + content: [{ + type: 'row', + content: [{ + type: 'component', + componentName: 'Hit Percent', + width: 50, + componentState: {label: 'F'} + }, { + type: 'component', + componentName: 'Hits', + componentState: {label: 'B'} + }, { + type: 'component', + componentName: 'Misses', + componentState: {label: 'C'} + }] + }, { + type: 'row', + content: [{ + type: 'component', + componentName: 'Requests Served', + componentState: {label: 'B'} + }, { + type: 'component', + componentName: 'Bytes Sent', + componentState: {label: 'C'} + }] + }] + }, { + type: 'column', + content: [{ + type: 'component', + componentName: 'Network Utilization', + componentState: {label: 'B'} + }, { + type: 'component', + componentName: 'CPU Utilization', + componentState: {label: 'C'} + }, { + type: 'component', + componentName: 'Disk Utilization', + componentState: {label: 'D'} + }, { + type: 'component', + componentName: 'RAM Utilization', + componentState: {label: 'E'} + }] + }] + }, { + type: 'row', + height: 20, + content: [{ + type: 'component', + componentName: 'Cache Size', + componentState: {label: 'F'} + }] + }] + }] +}; +let dashlayout; + +function loadDash() { + let savedState = localStorage.getItem("dashState"); + if (savedState !== null) { + dashlayout = new GoldenLayout(JSON.parse(savedState), $("#dashboard")); + } else { + dashlayout = new GoldenLayout(config, $("#dashboard")); + } + //graphs + dashlayout.registerComponent('Network Utilization', function (container, state) { + container.getElement().append('
'); + }); + dashlayout.registerComponent('CPU Utilization', function (container, state) { + container.getElement().append('
'); + }); + dashlayout.registerComponent('Disk Utilization', function (container, state) { + container.getElement().append('
'); + }); + dashlayout.registerComponent('Cache Size', function (container, state) { + + container.getElement().append('
'); + }); + dashlayout.registerComponent('RAM Utilization', function (container, state) { + container.getElement().append('
'); + }); + // numbers + dashlayout.registerComponent('Hits', function (container, state) { + container.getElement().append('
'); + }); + dashlayout.registerComponent('Misses', function (container, state) { + container.getElement().append('
'); + }); + dashlayout.registerComponent('Requests Served', function (container, state) { + container.getElement().append('
'); + }); + dashlayout.registerComponent('Bytes Sent', function (container, state) { + container.getElement().append('
'); + }); + dashlayout.registerComponent('Hit Percent', function (container, state) { + container.getElement().append('
'); + }); + + dashlayout.init(); + dashlayout.on('stateChanged', function () { + localStorage.setItem('dashState', JSON.stringify(dashlayout.toConfig())); + }); +} jQuery(document).ready(function () { + loadDash(); loadOptions(); + $(window).resize(function () { + let dash = $("#dashboard"); + dashlayout.updateSize(dash.width(), dash.height()); + }); $("#theme").attr("href", "themes/" + theme + ".css"); $("#style").attr("href", "themes/" + style + ".css"); if (doAnimations) { @@ -62,7 +209,8 @@ function loadOptions() { max_console_lines: 1000, show_console_latest: false, graph_time_frame: 30000, - do_animations: true + do_animations: true, + lock_dashboard: true } } theme = options.theme; @@ -74,6 +222,7 @@ function loadOptions() { graphTimeFrame = options.graph_time_frame; showConsoleLatest = options.show_console_latest; doAnimations = options.do_animations; + lockDash = options.lock_dashboard; $("#dataRefreshRate").val(refreshRate); $("#port").val(port); $("#ip").val(ip); @@ -83,10 +232,11 @@ function loadOptions() { $("#styleIn").val(style); $("#newestconsole").prop("checked", showConsoleLatest); $("#doAnimations").prop("checked", doAnimations); + $("#lockDash").prop("checked", lockDash) } function resetOptions() { - if (confirm("Do you really want to reset to defaults?")) { + if (confirm("Do you really want to reset all customizations to defaults?")) { $("#dataRefreshRate").val(5000); $("#port").val(33333); $("#ip").val("localhost"); @@ -96,6 +246,12 @@ function resetOptions() { $("#styleIn").val("sharpStyle"); $("#newestconsole").prop("checked", false); $("#doAnimations").prop("checked", true); + dashlayout.destroy(); + localStorage.removeItem('dashState'); + loadDash(); + selectTab('dash', 'dashb'); + let dash = $("#dashboard"); + dashlayout.updateSize(dash.width(), dash.height()); applyOptions() } } @@ -110,7 +266,8 @@ function applyOptions() { max_console_lines: parseInt($("#maxConsoleLines").val()), show_console_latest: $("#newestconsole").prop("checked"), graph_time_frame: parseInt($("#graphTimeFrame").val()), - do_animations: $("#doAnimations").prop("checked") + do_animations: $("#doAnimations").prop("checked"), + lock_dashboard: $("#lockDash").prop("checked") }; if (options.do_animations !== doAnimations) { doAnimations = options.do_animations; @@ -215,6 +372,19 @@ function applyOptions() { } ).prop("checked", showConsoleLatest); } + if (options.lock_dashboard !== lockDash) { + lockDash = options.lock_dashboard; + config.settings.showCloseIcon = !lockDash; + // localStorage.setItem('dashState', JSON.stringify(dashlayout.toConfig())); + // $("#dashboard").empty(); + // loadDash(); + $("#lockDashcb").addClass("updated").on( + "animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", + function () { + $(this).removeClass("updated"); + } + ).prop("checked", showConsoleLatest); + } localStorage.setItem("options", JSON.stringify(options)); } @@ -266,8 +436,8 @@ function expSide() { function applyTheme(t) { if (doAnimations) - $("*").each(function () { - if (!$(this).attr("hidden")) + $(document.body).children().each(function () { + if (!($(this).attr("hidden"))) $(this).addClass("tempsmooth").on( "webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend", function () { @@ -280,8 +450,8 @@ function applyTheme(t) { function applyStyle(s) { if (doAnimations) - $("*").each(function () { - if (!$(this).attr("hidden")) + $(document.body).children().each(function () { + if (!($(this).attr("hidden"))) $(this).addClass("tempsmooth").on( "webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend", function () { diff --git a/src/main/resources/webui/icons/close.png b/src/main/resources/webui/icons/close.png new file mode 100644 index 0000000000000000000000000000000000000000..8c030e843441d2dc8a93a153959e0633f89f5098 GIT binary patch literal 460 zcmV;-0WPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0bxl*K~z{r?UYTf z!!QVhE7GZuEk0Rj1*J_*<|M$67(c%oa-Tbru{j(h~1n?LQN8AuPION3TJ z%}g9-7mui1L$$V-wg(VS#HTj~q+8%IKPKpLe|~~#mEHk>p!gu5LnpGih;jm-VkrTX zFxvu~W2KI$CV&G~dR|wPm>xo#XW}x~9Vc_W517`h0{tq@1kCohHR*K>D`4Z1ID9^OW@)Jw(SiyQS;dG^C@Bg0000 MD@H Client + @@ -46,19 +47,9 @@

Dashboard

-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
@@ -132,6 +123,13 @@
+
+

Lock Dashboard
(does not work)

+ +
diff --git a/src/main/resources/webui/layout.css b/src/main/resources/webui/layout.css index 171f9f9..e98bb26 100644 --- a/src/main/resources/webui/layout.css +++ b/src/main/resources/webui/layout.css @@ -123,20 +123,17 @@ body { position: absolute; } -#nDat { - width: 40%; - height: calc(100% - 140px); - margin: 20px; - top: 100px; +#dashboard{ position: absolute; - + margin: 20px; + width: calc(100% - 40px); + height: calc(100% - 140px); + top: 100px; } .numerical_data { - height: 150px; - width: calc(50% - 20px); - margin: 10px; - float: left; + height: 100%; + width: 100%; } #gDat { @@ -150,10 +147,8 @@ body { } .line_graph_data { - height: 200px; - width: calc((100% - 20px)); - margin: 10px; - float: left; + height: 100%; + width: 100%; } /*Console and options positions*/ @@ -369,4 +364,317 @@ body { transform: translateY(0px); opacity: 1; } -} \ No newline at end of file +} + +/*golden layout*/ +.lm_root { + position: relative +} + +.lm_row > .lm_item { + float: left +} + +.lm_content { + overflow: hidden; + position: relative +} + +.lm_dragging, .lm_dragging * { + cursor: move !important; + user-select: none +} + +.lm_maximised { + position: absolute; + top: 0; + left: 0; + z-index: 40 +} + +.lm_maximise_placeholder { + display: none +} + +.lm_splitter { + position: relative; + z-index: 20 +} + +.lm_splitter:hover, .lm_splitter.lm_dragging { + background: orange +} + +.lm_splitter.lm_vertical .lm_drag_handle { + width: 100%; + height: 15px; + position: absolute; + top: -5px; + cursor: ns-resize +} + +.lm_splitter.lm_horizontal { + float: left; + height: 100% +} + +.lm_splitter.lm_horizontal .lm_drag_handle { + width: 15px; + height: 100%; + position: absolute; + left: -5px; + cursor: ew-resize +} + +.lm_header { + overflow: visible; + position: relative; + z-index: 1 +} + +.lm_header [class^=lm_] { + box-sizing: content-box !important +} + +.lm_header .lm_controls { + position: absolute; + right: 3px +} + +.lm_header .lm_controls > li { + cursor: pointer; + float: left; + width: 18px; + height: 18px; + text-align: center +} + +.lm_header ul { + margin: 0; + padding: 0; + list-style-type: none +} + +.lm_header .lm_tabs { + position: absolute; +} + +.lm_header .lm_tab { + cursor: pointer; + float: left; + height: 14px; + padding: 0 25px 5px 10px; + position: relative +} + +.lm_header .lm_tab i { + width: 2px; + height: 19px; + position: absolute +} + +.lm_header .lm_tab i.lm_left { + top: 0; + left: -2px +} + +.lm_header .lm_tab i.lm_right { + top: 0; + right: -2px +} + +.lm_header .lm_tab .lm_title { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis +} + +.lm_header .lm_tab .lm_close_tab { + width: 14px; + height: 14px; + position: absolute; + top: 0; + right: 0; + text-align: center +} + +.lm_stack.lm_left .lm_header, .lm_stack.lm_right .lm_header { + height: 100% +} + +.lm_dragProxy.lm_left .lm_header, .lm_dragProxy.lm_right .lm_header, .lm_stack.lm_left .lm_header, .lm_stack.lm_right .lm_header { + width: 20px; + float: left; + vertical-align: top +} + +.lm_dragProxy.lm_left .lm_header .lm_tabs, .lm_dragProxy.lm_right .lm_header .lm_tabs, .lm_stack.lm_left .lm_header .lm_tabs, .lm_stack.lm_right .lm_header .lm_tabs { + transform-origin: left top; + top: 0; + width: 1000px +} + +.lm_dragProxy.lm_left .lm_header .lm_controls, .lm_dragProxy.lm_right .lm_header .lm_controls, .lm_stack.lm_left .lm_header .lm_controls, .lm_stack.lm_right .lm_header .lm_controls { + bottom: 0 +} + +.lm_dragProxy.lm_left .lm_items, .lm_dragProxy.lm_right .lm_items, .lm_stack.lm_left .lm_items, .lm_stack.lm_right .lm_items { + float: left +} + +.lm_dragProxy.lm_left .lm_header .lm_tabs, .lm_stack.lm_left .lm_header .lm_tabs { + transform: rotate(-90deg) scaleX(-1); + left: 0 +} + +.lm_dragProxy.lm_left .lm_header .lm_tabs .lm_tab, .lm_stack.lm_left .lm_header .lm_tabs .lm_tab { + transform: scaleX(-1); + margin-top: 1px +} + +.lm_dragProxy.lm_left .lm_header .lm_tabdropdown_list, .lm_stack.lm_left .lm_header .lm_tabdropdown_list { + top: initial; + right: initial; + left: 20px +} + +.lm_dragProxy.lm_right .lm_content { + float: left +} + +.lm_dragProxy.lm_right .lm_header .lm_tabs, .lm_stack.lm_right .lm_header .lm_tabs { + transform: rotate(90deg) scaleX(1); + left: 100%; + margin-left: 0 +} + +.lm_dragProxy.lm_right .lm_header .lm_controls, .lm_stack.lm_right .lm_header .lm_controls { + left: 3px +} + +.lm_dragProxy.lm_right .lm_header .lm_tabdropdown_list, .lm_stack.lm_right .lm_header .lm_tabdropdown_list { + top: initial; + right: 20px +} + +.lm_dragProxy.lm_bottom .lm_header .lm_tab, .lm_stack.lm_bottom .lm_header .lm_tab { + margin-top: 0; + border-top: none +} + +.lm_dragProxy.lm_bottom .lm_header .lm_controls, .lm_stack.lm_bottom .lm_header .lm_controls { + top: 3px +} + +.lm_dragProxy.lm_bottom .lm_header .lm_tabdropdown_list, .lm_stack.lm_bottom .lm_header .lm_tabdropdown_list { + top: initial; + bottom: 20px +} + +.lm_drop_tab_placeholder { + float: left; + width: 100px; + height: 10px; + visibility: hidden +} + +.lm_header .lm_controls .lm_tabdropdown:before { + content: ''; + width: 0; + height: 0; + vertical-align: middle; + display: inline-block; + border-top: 5px dashed; + border-right: 5px solid transparent; + border-left: 5px solid transparent; + color: white +} + +.lm_header .lm_tabdropdown_list { + position: absolute; + top: 20px; + right: 0; + z-index: 5; + overflow: hidden +} + +.lm_header .lm_tabdropdown_list .lm_tab { + clear: both; + padding-right: 10px; + margin: 0 +} + +.lm_header .lm_tabdropdown_list .lm_tab .lm_title { + width: 100px +} + +.lm_header .lm_tabdropdown_list .lm_close_tab { + display: none !important +} + +.lm_dragProxy { + position: absolute; + top: 0; + left: 0; + z-index: 30 +} + +.lm_dragProxy .lm_header { + background: transparent +} + +.lm_dragProxy .lm_content { + border-top: none; + overflow: hidden +} + +.lm_dropTargetIndicator { + display: none; + position: absolute; + z-index: 20 +} + +.lm_dropTargetIndicator .lm_inner { + width: 100%; + height: 100%; + position: relative; + top: 0; + left: 0 +} + +.lm_transition_indicator { + display: none; + width: 20px; + height: 20px; + position: absolute; + top: 0; + left: 0; + z-index: 20 +} + +.lm_popin { + width: 20px; + height: 20px; + position: absolute; + bottom: 0; + right: 0; + z-index: 9999 +} + +.lm_popin > * { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0 +} + +.lm_popin > .lm_bg { + z-index: 10 +} + +.lm_popin > .lm_icon { + z-index: 20 +} + +/*# sourceMappingURL=goldenlayout-base.css.map */ \ No newline at end of file diff --git a/src/main/resources/webui/themes/darkTheme.css b/src/main/resources/webui/themes/darkTheme.css index cf2485d..96661bb 100644 --- a/src/main/resources/webui/themes/darkTheme.css +++ b/src/main/resources/webui/themes/darkTheme.css @@ -165,3 +165,170 @@ input:checked + .slider { ::-webkit-scrollbar-track { } + +/*golden layout*/ +.lm_goldenlayout { + background: rgba(0, 0, 0, 0); +} + +.lm_content { + background: rgba(0, 0, 0, 0); +} + +.lm_dragProxy .lm_content { + box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.9) +} + +.lm_dropTargetIndicator { + box-shadow: inset 0 0 30px #000000; + outline: 1px dashed #cccccc; + transition: all 200ms ease +} + +.lm_dropTargetIndicator .lm_inner { + background: #000000; + opacity: .2 +} + +.lm_splitter { + background: inherit; + opacity: .001; + transition: opacity 200ms ease +} + +.lm_splitter:hover, .lm_splitter.lm_dragging { + background: #303030; + opacity: 1 +} + +.lm_header { + background-color: #606060; + height: 20px; + user-select: none +} + +.lm_header.lm_selectable { + cursor: pointer +} + +.lm_header .lm_tab { + font-size: 12px; + color: #f0f0f0; + background-color: #303030; + margin-right: 2px; + padding-bottom: 2px; + padding-top: 1px +} + +.lm_header .lm_tab .lm_close_tab { + width: 9px; + height: 9px; + background-image: url("../icons/close.png"); + filter: invert(100%); + background-size: 14px 14px; + background-position: center center; + background-repeat: no-repeat; + top: 4px; + right: 6px; + opacity: .4 +} + +.lm_header .lm_tab .lm_close_tab:hover { + opacity: 1 +} + +.lm_header .lm_tab.lm_active { + background-color: #909090; +} + +.lm_header .lm_tab.lm_active .lm_close_tab:hover { + opacity: 1 +} + +.lm_dragProxy.lm_bottom .lm_header .lm_tab, .lm_stack.lm_bottom .lm_header .lm_tab { + box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.3) +} + +.lm_dragProxy.lm_bottom .lm_header .lm_tab.lm_active, .lm_stack.lm_bottom .lm_header .lm_tab.lm_active { + box-shadow: 0 2px 2px #000000 +} + +.lm_selected .lm_header { + background-color: #452500 +} + +.lm_tab:hover, .lm_tab.lm_active { + background: #202020; + color: #f0f0f0 +} + +.lm_header .lm_controls .lm_tabdropdown:before { + color: #ffffff +} + +.lm_controls > li { + position: relative; + background-position: center center; + background-repeat: no-repeat; + opacity: .4; + transition: opacity 300ms ease +} + +.lm_controls > li:hover { + opacity: 1 +} + +.lm_controls .lm_popout { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAPklEQVR4nI2Q0QoAIAwCNfr/X7aXCpGN8snBdgejJOzckpkxs9jR6K6T5JpU0nWl5pSXTk7qwh8SnNT+CAAWCgkKFpuSWsUAAAAASUVORK5CYII=) +} + +.lm_controls .lm_maximise { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKElEQVR4nGP8////fwYCgImQAgYGBgYWKM2IR81/okwajIpgvsMbVgAwgQYRVakEKQAAAABJRU5ErkJggg==) +} + +.lm_controls .lm_close { + top: 2px; + width: 18px !important; + height: 18px !important; + background-image: url("../icons/close.png"); + background-size: 16px 16px; + background-position: center center; + filter: invert(100%); +} + +.lm_maximised .lm_header { + background-color: #000000 +} + +.lm_maximised .lm_controls .lm_maximise { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAJ0lEQVR4nGP8//8/AzGAiShVI1YhCwMDA8OsWbPwBmZaWhoj0SYCAN1lBxMAX4n0AAAAAElFTkSuQmCC) +} + +.lm_transition_indicator { + background-color: #000000; + border: 1px dashed #555555 +} + +.lm_popin { + cursor: pointer +} + +.lm_popin .lm_bg { + background: #ffffff; + opacity: .3 +} + +.lm_popin .lm_icon { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAJCAYAAADpeqZqAAAAWklEQVR4nJWOyw3AIAxDHcQC7L8jbwT3AlJBfNp3SiI7dtRaLSlKKeoA1oEsKSQZCEluexw8Tm3ohk+E7bnOUHUGcNh+HwbBygw4AZ7FN/Lt84p0l+yTflV8AKQyLdcCRJi/AAAAAElFTkSuQmCC); + background-position: center center; + background-repeat: no-repeat; + border-left: 1px solid #eeeeee; + border-top: 1px solid #eeeeee; + opacity: .7 +} + +.lm_popin:hover .lm_icon { + opacity: 1 +} + +/*# sourceMappingURL=goldenlayout-dark-theme.css.map */ \ No newline at end of file diff --git a/src/main/resources/webui/themes/lightTheme.css b/src/main/resources/webui/themes/lightTheme.css index 8076817..0a26f14 100644 --- a/src/main/resources/webui/themes/lightTheme.css +++ b/src/main/resources/webui/themes/lightTheme.css @@ -164,3 +164,166 @@ input:checked + .slider { ::-webkit-scrollbar-track { } + +/*golden layout overrides*/ + +.lm_goldenlayout { + background: rgba(0, 0, 0, 0); +} + +.lm_content { + background: rgba(0, 0, 0, 0); +} + +.lm_dragProxy .lm_content { + box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.9) +} + +.lm_dropTargetIndicator { + box-shadow: inset 0 0 30px #000000; + outline: 1px dashed #cccccc; + transition: all 200ms ease +} + +.lm_dropTargetIndicator .lm_inner { + background: #000000; + opacity: .2 +} + +.lm_splitter { + background: inherit; + opacity: .001; + transition: opacity 200ms ease +} + +.lm_splitter:hover, .lm_splitter.lm_dragging { + background-color: #dadada; + opacity: 1 +} + +.lm_header { + background-color: #ededed; + height: 20px; + user-select: none +} + +.lm_header.lm_selectable { + cursor: pointer +} + +.lm_header .lm_tab { + font-size: 12px; + color: #202020; + background-color: #f8f9fa; + margin-right: 2px; + padding-bottom: 2px; + padding-top: 1px +} + +.lm_header .lm_tab .lm_close_tab { + width: 9px; + height: 9px; + background-image: url("../icons/close.png"); + background-position: center center; + background-repeat: no-repeat; + top: 4px; + right: 6px; + opacity: .4 +} + +.lm_header .lm_tab .lm_close_tab:hover { + opacity: 1 +} + +.lm_header .lm_tab.lm_active { + background-color: #e1e1e1; +} + +.lm_header .lm_tab.lm_active .lm_close_tab:hover { + opacity: 1 +} + +.lm_dragProxy.lm_bottom .lm_header .lm_tab, .lm_stack.lm_bottom .lm_header .lm_tab { + box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.3) +} + +.lm_dragProxy.lm_bottom .lm_header .lm_tab.lm_active, .lm_stack.lm_bottom .lm_header .lm_tab.lm_active { + box-shadow: 0 2px 2px #000000 +} + +.lm_selected .lm_header { + background-color: #452500 +} + +.lm_tab:hover, .lm_tab.lm_active { + background-color: #dadada; +} + +.lm_header .lm_controls .lm_tabdropdown:before { + color: #ffffff +} + +.lm_controls > li { + position: relative; + background-position: center center; + background-repeat: no-repeat; + opacity: .4; + transition: opacity 300ms ease +} + +.lm_controls > li:hover { + opacity: 1 +} + +.lm_controls .lm_popout { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAPklEQVR4nI2Q0QoAIAwCNfr/X7aXCpGN8snBdgejJOzckpkxs9jR6K6T5JpU0nWl5pSXTk7qwh8SnNT+CAAWCgkKFpuSWsUAAAAASUVORK5CYII=) +} + +.lm_controls .lm_maximise { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKElEQVR4nGP8////fwYCgImQAgYGBgYWKM2IR81/okwajIpgvsMbVgAwgQYRVakEKQAAAABJRU5ErkJggg==) +} + +.lm_controls .lm_close { + top: 2px; + width: 10px !important; + height: 10px !important; + background-image: url("../icons/close.png"); + background-position: center center; +} + +.lm_maximised .lm_header { + background-color: #000000 +} + +.lm_maximised .lm_controls .lm_maximise { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAJ0lEQVR4nGP8//8/AzGAiShVI1YhCwMDA8OsWbPwBmZaWhoj0SYCAN1lBxMAX4n0AAAAAElFTkSuQmCC) +} + +.lm_transition_indicator { + background-color: #000000; + border: 1px dashed #555555 +} + +.lm_popin { + cursor: pointer +} + +.lm_popin .lm_bg { + background: #ffffff; + opacity: .3 +} + +.lm_popin .lm_icon { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAJCAYAAADpeqZqAAAAWklEQVR4nJWOyw3AIAxDHcQC7L8jbwT3AlJBfNp3SiI7dtRaLSlKKeoA1oEsKSQZCEluexw8Tm3ohk+E7bnOUHUGcNh+HwbBygw4AZ7FN/Lt84p0l+yTflV8AKQyLdcCRJi/AAAAAElFTkSuQmCC); + background-position: center center; + background-repeat: no-repeat; + border-left: 1px solid #eeeeee; + border-top: 1px solid #eeeeee; + opacity: .7 +} + +.lm_popin:hover .lm_icon { + opacity: 1 +} + +/*# sourceMappingURL=goldenlayout-dark-theme.css.map */ \ No newline at end of file diff --git a/src/main/resources/webui/themes/softStyle.css b/src/main/resources/webui/themes/softStyle.css index 2279813..61d3763 100644 --- a/src/main/resources/webui/themes/softStyle.css +++ b/src/main/resources/webui/themes/softStyle.css @@ -51,7 +51,7 @@ } .numerical_data { - border-radius: 10px; + border-radius: 0 0 10px 10px; } #gDat { @@ -59,7 +59,7 @@ } .line_graph_data { - border-radius: 10px; + border-radius: 0 0 10px 10px; } /*Console and options positions*/ @@ -142,4 +142,22 @@ input:checked + .slider:before { } ::-webkit-scrollbar-track { -} \ No newline at end of file +} + +/*Golden Layout Overrides*/ + +.lm_header { + border-radius: 10px 10px 0 0; +} + +.lm_tabs:first-child .lm_tab{ + border-top-left-radius: 10px; +} +.lm_tabs:first-child .lm_tab ~ .lm_tab{ + border-top-left-radius: 0; +} + +.lm_splitter { + border-radius: 10px; +} + From 8ccca2ec9381873e533c64f12a4c2239501bcc7d Mon Sep 17 00:00:00 2001 From: M Date: Fri, 12 Jun 2020 12:58:10 -0500 Subject: [PATCH 3/7] merge --- .../mdnet/base/settings/ClientSettings.java | 2 +- src/main/kotlin/mdnet/base/Application.kt | 98 +++++++++---------- src/main/kotlin/mdnet/base/Netty.kt | 8 +- 3 files changed, 52 insertions(+), 56 deletions(-) diff --git a/src/main/java/mdnet/base/settings/ClientSettings.java b/src/main/java/mdnet/base/settings/ClientSettings.java index 8e854c5..3e424e2 100644 --- a/src/main/java/mdnet/base/settings/ClientSettings.java +++ b/src/main/java/mdnet/base/settings/ClientSettings.java @@ -32,7 +32,7 @@ public final class ClientSettings { } public ClientSettings(long maxCacheSizeMib, long maxBandwidthMibPerHour, long maxBurstRateKibPerSecond, - int clientPort, String clientSecret, int threads, WebSettings webSettings) { + int clientPort, String clientSecret, int threads, WebSettings webSettings) { this.maxCacheSizeMib = maxCacheSizeMib; this.maxBandwidthMibPerHour = maxBandwidthMibPerHour; this.maxBurstRateKibPerSecond = maxBurstRateKibPerSecond; diff --git a/src/main/kotlin/mdnet/base/Application.kt b/src/main/kotlin/mdnet/base/Application.kt index fbe4b3a..88b7e18 100644 --- a/src/main/kotlin/mdnet/base/Application.kt +++ b/src/main/kotlin/mdnet/base/Application.kt @@ -19,10 +19,8 @@ import org.http4k.filter.CachingFilters import org.http4k.filter.MaxAgeTtl import org.http4k.filter.ServerFilters import org.http4k.lens.Path -import org.http4k.routing.ResourceLoader import org.http4k.routing.bind import org.http4k.routing.routes -import org.http4k.routing.singlePageApp import org.http4k.server.Http4kServer import org.http4k.server.asServer import org.slf4j.LoggerFactory @@ -52,16 +50,16 @@ fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSetting } val client = ApacheClient(responseBodyMode = BodyMode.Stream, client = HttpClients.custom() - .setDefaultRequestConfig(RequestConfig.custom() - .setCookieSpec(CookieSpecs.IGNORE_COOKIES) - .setConnectTimeout(3000) - .setSocketTimeout(3000) - .setConnectionRequestTimeout(3000) + .setDefaultRequestConfig(RequestConfig.custom() + .setCookieSpec(CookieSpecs.IGNORE_COOKIES) + .setConnectTimeout(3000) + .setSocketTimeout(3000) + .setConnectionRequestTimeout(3000) + .build()) + .setMaxConnTotal(THREADS_TO_ALLOCATE) + .setMaxConnPerRoute(THREADS_TO_ALLOCATE) + // Have it at the maximum open sockets a user can have in most modern OSes. No reason to limit this, just limit it at the Netty side. .build()) - .setMaxConnTotal(THREADS_TO_ALLOCATE) - .setMaxConnPerRoute(THREADS_TO_ALLOCATE) - // Have it at the maximum open sockets a user can have in most modern OSes. No reason to limit this, just limit it at the Netty side. - .build()) val app = { dataSaver: Boolean -> { request: Request -> @@ -89,28 +87,28 @@ fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSetting // Netty doesn't do Content-Length or Content-Type, so we have the pleasure of doing that ourselves fun respondWithImage(input: InputStream, length: String?, type: String, lastModified: String?): Response = - Response(Status.OK) - .header("Content-Type", type) - .header("X-Content-Type-Options", "nosniff") - .header( - "Cache-Control", - listOf("public", MaxAgeTtl(Constants.MAX_AGE_CACHE).toHeaderValue()).joinToString(", ") - ) - .header("Timing-Allow-Origin", "https://mangadex.org") - .let { - if (length != null) { - it.body(input, length.toLong()).header("Content-Length", length) - } else { - it.body(input).header("Transfer-Encoding", "chunked") - } - } - .let { - if (lastModified != null) { - it.header("Last-Modified", lastModified) - } else { - it - } - } + Response(Status.OK) + .header("Content-Type", type) + .header("X-Content-Type-Options", "nosniff") + .header( + "Cache-Control", + listOf("public", MaxAgeTtl(Constants.MAX_AGE_CACHE).toHeaderValue()).joinToString(", ") + ) + .header("Timing-Allow-Origin", "https://mangadex.org") + .let { + if (length != null) { + it.body(input, length.toLong()).header("Content-Length", length) + } else { + it.body(input).header("Transfer-Encoding", "chunked") + } + } + .let { + if (lastModified != null) { + it.header("Last-Modified", lastModified) + } else { + it + } + } val snapshot = cache.get(cacheId) if (snapshot != null) { @@ -126,15 +124,15 @@ fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSetting snapshot.close() Response(Status.NOT_MODIFIED) - .header("Last-Modified", lastModified) + .header("Last-Modified", lastModified) } else { if (LOGGER.isInfoEnabled) { LOGGER.info("Request for $sanitizedUri hit cache") } respondWithImage( - CipherInputStream(BufferedInputStream(snapshot.getInputStream(0)), getRc4(rc4Bytes)), - snapshot.getLength(0).toString(), snapshot.getString(1), snapshot.getString(2) + CipherInputStream(BufferedInputStream(snapshot.getInputStream(0)), getRc4(rc4Bytes)), + snapshot.getLength(0).toString(), snapshot.getString(1), snapshot.getString(2) ) } } else { @@ -171,8 +169,8 @@ fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSetting editor.setString(2, lastModified) val tee = CachingInputStream( - mdResponse.body.stream, - executor, CipherOutputStream(BufferedOutputStream(editor.newOutputStream(0)), getRc4(rc4Bytes)) + mdResponse.body.stream, + executor, CipherOutputStream(BufferedOutputStream(editor.newOutputStream(0)), getRc4(rc4Bytes)) ) { // Note: if neither of the options get called/are in the log // check that tee gets closed and for exceptions in this lambda @@ -207,19 +205,17 @@ fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSetting CachingFilters return catchAllHideDetails() - .then(ServerFilters.CatchLensFailure) - .then(addCommonHeaders()) - .then( - routes( - "/data/{chapterHash}/{fileName}" bind Method.GET to app(false), - "/data-saver/{chapterHash}/{fileName}" bind Method.GET to app(true), - "/{token}/data/{chapterHash}/{fileName}" bind Method.GET to app(false), - "/{token}/data-saver/{chapterHash}/{fileName}" bind Method.GET to app(true), - - singlePageApp(ResourceLoader.Classpath("/webui")) + .then(ServerFilters.CatchLensFailure) + .then(addCommonHeaders()) + .then( + routes( + "/data/{chapterHash}/{fileName}" bind Method.GET to app(false), + "/data-saver/{chapterHash}/{fileName}" bind Method.GET to app(true), + "/{token}/data/{chapterHash}/{fileName}" bind Method.GET to app(false), + "/{token}/data-saver/{chapterHash}/{fileName}" bind Method.GET to app(true) + ) ) - ) - .asServer(Netty(serverSettings.tls, clientSettings, statistics)) + .asServer(Netty(serverSettings.tls, clientSettings, statistics)) } private fun getRc4(key: ByteArray): Cipher { @@ -235,7 +231,7 @@ private fun addCommonHeaders(): Filter { { request: Request -> val response = next(request) response.header("Date", HTTP_TIME_FORMATTER.format(ZonedDateTime.now(ZoneOffset.UTC))) - .header("Server", "Mangadex@Home Node ${Constants.CLIENT_VERSION} (${Constants.CLIENT_BUILD})") + .header("Server", "Mangadex@Home Node ${Constants.CLIENT_VERSION} (${Constants.CLIENT_BUILD})") } } } diff --git a/src/main/kotlin/mdnet/base/Netty.kt b/src/main/kotlin/mdnet/base/Netty.kt index 175d732..5c73c75 100644 --- a/src/main/kotlin/mdnet/base/Netty.kt +++ b/src/main/kotlin/mdnet/base/Netty.kt @@ -46,7 +46,7 @@ class Netty(private val tls: ServerSettings.TlsCert, private val clientSettings: private lateinit var address: InetSocketAddress private val burstLimiter = object : GlobalTrafficShapingHandler( - workerGroup, 1024 * clientSettings.maxBurstRateKibPerSecond, 0, 50) { + workerGroup, 1024 * clientSettings.maxBurstRateKibPerSecond, 0, 50) { override fun doAccounting(counter: TrafficCounter) { stats.get().bytesSent.getAndAdd(counter.cumulativeWrittenBytes()) counter.resetCumulativeTime() @@ -60,9 +60,9 @@ class Netty(private val tls: ServerSettings.TlsCert, private val clientSettings: val (mainCert, chainCert) = getX509Certs(tls.certificate) val sslContext = SslContextBuilder - .forServer(getPrivateKey(tls.privateKey), mainCert, chainCert) - .protocols("TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1") - .build() + .forServer(getPrivateKey(tls.privateKey), mainCert, chainCert) + .protocols("TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1") + .build() val bootstrap = ServerBootstrap() bootstrap.group(masterGroup, workerGroup) From 9086e09c60daf4125acda9a99f8be9883ef7c47e Mon Sep 17 00:00:00 2001 From: M Date: Fri, 12 Jun 2020 13:52:50 -0500 Subject: [PATCH 4/7] slight fixes --- build.gradle | 2 +- src/main/kotlin/mdnet/base/Netty.kt | 1 + src/main/resources/webui/index.html | 2 +- src/main/resources/webui/layout.css | 6 +- src/main/resources/webui/themes/darkTheme.css | 4 +- .../resources/webui/themes/midnightTheme.css | 180 +++++++++++++++++- src/main/resources/webui/themes/softStyle.css | 6 +- 7 files changed, 184 insertions(+), 17 deletions(-) diff --git a/build.gradle b/build.gradle index 356bfc7..5ce2bc6 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ dependencies { implementation group: "org.http4k", name: "http4k-server-netty", version: "$http_4k_version" implementation group: "org.http4k", name: "http4k-client-apache", version: "$http_4k_version" implementation group: "commons-io", name: "commons-io", version: "2.7" - + compile "org.java-websocket:Java-WebSocket:1.5.1" implementation "ch.qos.logback:logback-classic:$logback_version" runtimeOnly 'io.netty:netty-tcnative-boringssl-static:2.0.30.Final' } diff --git a/src/main/kotlin/mdnet/base/Netty.kt b/src/main/kotlin/mdnet/base/Netty.kt index 5c73c75..ccb0490 100644 --- a/src/main/kotlin/mdnet/base/Netty.kt +++ b/src/main/kotlin/mdnet/base/Netty.kt @@ -18,6 +18,7 @@ import io.netty.handler.ssl.SslContextBuilder import io.netty.handler.stream.ChunkedWriteHandler import io.netty.handler.traffic.GlobalTrafficShapingHandler import io.netty.handler.traffic.TrafficCounter +import mdnet.base.settings.ClientSettings import org.http4k.core.HttpHandler import org.http4k.server.Http4kChannelHandler import org.http4k.server.Http4kServer diff --git a/src/main/resources/webui/index.html b/src/main/resources/webui/index.html index 00774c9..33784a6 100644 --- a/src/main/resources/webui/index.html +++ b/src/main/resources/webui/index.html @@ -98,7 +98,7 @@ diff --git a/src/main/resources/webui/layout.css b/src/main/resources/webui/layout.css index e98bb26..c8e2afe 100644 --- a/src/main/resources/webui/layout.css +++ b/src/main/resources/webui/layout.css @@ -40,7 +40,6 @@ body { position: absolute; right: 0; margin: 20px 20px 20px 20px; - width: 150px; height: 35px; border-style: solid; outline: none; @@ -247,8 +246,6 @@ body { position: fixed; bottom: 20px; right: 190px; - width: 150px; - height: 30px; } .option { @@ -301,6 +298,8 @@ body { .button { outline: none; border-width: 2px; + width: 150px; + height: 30px; } .switch input { @@ -464,6 +463,7 @@ body { float: left; height: 14px; padding: 0 25px 5px 10px; + top: 1px; position: relative } diff --git a/src/main/resources/webui/themes/darkTheme.css b/src/main/resources/webui/themes/darkTheme.css index 96661bb..e3c26b1 100644 --- a/src/main/resources/webui/themes/darkTheme.css +++ b/src/main/resources/webui/themes/darkTheme.css @@ -58,14 +58,14 @@ body { } .numerical_data { - background-color: #606060; + background-color: #555555; } #gDat { } .line_graph_data { - background-color: #606060; + background-color: #555555; } /*Console and options colors*/ diff --git a/src/main/resources/webui/themes/midnightTheme.css b/src/main/resources/webui/themes/midnightTheme.css index ea260b0..9791d4b 100644 --- a/src/main/resources/webui/themes/midnightTheme.css +++ b/src/main/resources/webui/themes/midnightTheme.css @@ -50,18 +50,12 @@ body { } -#nDat { -} - .numerical_data { - background-color: #404040; -} - -#gDat { + background-color: #353535; } .line_graph_data { - background-color: #404040; + background-color: #353535; } /*Console and options colors*/ @@ -161,3 +155,173 @@ input:checked + .slider { ::-webkit-scrollbar-track { } + +/*golden layout*/ +.lm_goldenlayout { + background: rgba(0, 0, 0, 0); +} + +.lm_content { + background: rgba(0, 0, 0, 0); +} + +.lm_dragProxy .lm_content { + box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.9) +} + +.lm_dropTargetIndicator { + box-shadow: inset 0 0 30px #000000; + outline: 1px dashed #cccccc; + transition: all 200ms ease +} + +.lm_dropTargetIndicator .lm_inner { + background: #000000; + opacity: .2 +} + +.lm_splitter { + background: inherit; + opacity: .001; + transition: opacity 200ms ease +} + +.lm_splitter:hover, .lm_splitter.lm_dragging { + background: #303030; + opacity: 1 +} + +.lm_header { + background-color: #404040; + height: 20px; + user-select: none +} + +.lm_header.lm_selectable { + cursor: pointer +} + +.lm_header .lm_tab { + font-size: 12px; + color: #bfbfbf; + background-color: #404040; + margin-right: 2px; + padding-bottom: 2px; + padding-top: 1px +} + +.lm_header .lm_tab .lm_close_tab { + width: 9px; + height: 9px; + background-image: url("../icons/close.png"); + filter: invert(100%); + background-size: 14px 14px; + background-position: center center; + background-repeat: no-repeat; + top: 4px; + right: 6px; + opacity: .4 +} + +.lm_header .lm_tab .lm_close_tab:hover { + opacity: 1 +} + +.lm_header .lm_tab.lm_active { + background-color: #606060; +} +.lm_header .lm_tab.lm_active:hover { + background-color: #505050; +} + +.lm_header .lm_tab.lm_active .lm_close_tab:hover { + opacity: 1 +} + +.lm_dragProxy.lm_bottom .lm_header .lm_tab, .lm_stack.lm_bottom .lm_header .lm_tab { + box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.3) +} + +.lm_dragProxy.lm_bottom .lm_header .lm_tab.lm_active, .lm_stack.lm_bottom .lm_header .lm_tab.lm_active { + box-shadow: 0 2px 2px #000000 +} + +.lm_selected .lm_header { + background-color: #452500 +} + +.lm_tab:hover{ + background-color: #505050; + color: #bfbfbf +} + + .lm_header .lm_controls .lm_tabdropdown:before { + color: #ffffff +} + +.lm_controls > li { + position: relative; + background-position: center center; + background-repeat: no-repeat; + opacity: .4; + transition: opacity 300ms ease +} + +.lm_controls > li:hover { + opacity: 1 +} + +.lm_controls .lm_popout { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAPklEQVR4nI2Q0QoAIAwCNfr/X7aXCpGN8snBdgejJOzckpkxs9jR6K6T5JpU0nWl5pSXTk7qwh8SnNT+CAAWCgkKFpuSWsUAAAAASUVORK5CYII=) +} + +.lm_controls .lm_maximise { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKElEQVR4nGP8////fwYCgImQAgYGBgYWKM2IR81/okwajIpgvsMbVgAwgQYRVakEKQAAAABJRU5ErkJggg==) +} + +.lm_controls .lm_close { + top: 2px; + width: 18px !important; + height: 18px !important; + background-image: url("../icons/close.png"); + background-size: 16px 16px; + background-position: center center; + filter: invert(100%); +} + +.lm_maximised .lm_header { + background-color: #000000 +} + +.lm_maximised .lm_controls .lm_maximise { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAJ0lEQVR4nGP8//8/AzGAiShVI1YhCwMDA8OsWbPwBmZaWhoj0SYCAN1lBxMAX4n0AAAAAElFTkSuQmCC) +} + +.lm_transition_indicator { + background-color: #000000; + border: 1px dashed #555555 +} + +.lm_popin { + cursor: pointer +} + +.lm_popin .lm_bg { + background: #ffffff; + opacity: .3 +} + +.lm_popin .lm_icon { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAJCAYAAADpeqZqAAAAWklEQVR4nJWOyw3AIAxDHcQC7L8jbwT3AlJBfNp3SiI7dtRaLSlKKeoA1oEsKSQZCEluexw8Tm3ohk+E7bnOUHUGcNh+HwbBygw4AZ7FN/Lt84p0l+yTflV8AKQyLdcCRJi/AAAAAElFTkSuQmCC); + background-position: center center; + background-repeat: no-repeat; + border-left: 1px solid #eeeeee; + border-top: 1px solid #eeeeee; + opacity: .7 +} + +.lm_popin:hover .lm_icon { + opacity: 1 +} + +/*# sourceMappingURL=goldenlayout-dark-theme.css.map */ diff --git a/src/main/resources/webui/themes/softStyle.css b/src/main/resources/webui/themes/softStyle.css index 61d3763..d1b83e1 100644 --- a/src/main/resources/webui/themes/softStyle.css +++ b/src/main/resources/webui/themes/softStyle.css @@ -4,7 +4,6 @@ } #connection { - border-radius: 10px; } #consoleLatest { @@ -89,7 +88,6 @@ } #apply { - border-radius: 10px; } .option { @@ -109,6 +107,10 @@ border-radius: 10px; } +.button{ + border-radius: 10px; +} + .slider { border-radius: 30px; width: 60px; From f153a11717022298f3df6ea6af4d27c1c976eb55 Mon Sep 17 00:00:00 2001 From: carbotaniuman <41451839+carbotaniuman@users.noreply.github.com> Date: Fri, 12 Jun 2020 14:56:51 -0500 Subject: [PATCH 5/7] Stuff --- build.gradle | 8 ++- src/main/java/mdnet/base/MangaDexClient.java | 12 +++-- src/main/java/mdnet/base/Statistics.java | 24 +++++++-- .../mdnet/base/settings/ClientSettings.java | 2 +- .../java/mdnet/base/settings/WebSettings.java | 24 ++++++--- .../{base => cache}/CachingInputStream.java | 2 +- src/main/java/mdnet/cache/DiskLruCache.java | 7 ++- src/main/java/mdnet/webui/WebConsole.java | 52 +++++++++---------- .../mdnet/base/{ => web}/Application.kt | 37 +++---------- src/main/kotlin/mdnet/base/web/WebUi.kt | 37 +++++++++++++ src/main/kotlin/mdnet/base/web/common.kt | 43 +++++++++++++++ 11 files changed, 170 insertions(+), 78 deletions(-) rename src/main/java/mdnet/{base => cache}/CachingInputStream.java (99%) rename src/main/kotlin/mdnet/base/{ => web}/Application.kt (90%) create mode 100644 src/main/kotlin/mdnet/base/web/WebUi.kt create mode 100644 src/main/kotlin/mdnet/base/web/common.kt diff --git a/build.gradle b/build.gradle index 5ce2bc6..09482d7 100644 --- a/build.gradle +++ b/build.gradle @@ -22,8 +22,12 @@ dependencies { implementation group: "org.http4k", name: "http4k-core", version: "$http_4k_version" implementation group: "org.http4k", name: "http4k-server-netty", version: "$http_4k_version" implementation group: "org.http4k", name: "http4k-client-apache", version: "$http_4k_version" + implementation group: "org.http4k", name: "http4k-format-gson", version: "3.249.0" + implementation group: "commons-io", name: "commons-io", version: "2.7" - compile "org.java-websocket:Java-WebSocket:1.5.1" + + implementation group: 'org.java-websocket', name: 'Java-WebSocket', version: '1.5.1' + implementation "ch.qos.logback:logback-classic:$logback_version" runtimeOnly 'io.netty:netty-tcnative-boringssl-static:2.0.30.Final' } @@ -35,12 +39,14 @@ java { spotless { java { + indentWithSpaces(4) eclipse() removeUnusedImports() trimTrailingWhitespace() endWithNewline() } kotlin { + indentWithSpaces(4) ktlint() trimTrailingWhitespace() endWithNewline() diff --git a/src/main/java/mdnet/base/MangaDexClient.java b/src/main/java/mdnet/base/MangaDexClient.java index e818258..3a09e17 100644 --- a/src/main/java/mdnet/base/MangaDexClient.java +++ b/src/main/java/mdnet/base/MangaDexClient.java @@ -3,6 +3,8 @@ package mdnet.base; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import mdnet.base.settings.ClientSettings; +import mdnet.base.web.ApplicationKt; +import mdnet.base.web.WebUiKt; import mdnet.cache.DiskLruCache; import mdnet.webui.WebConsole; import org.http4k.server.Http4kServer; @@ -29,6 +31,7 @@ public class MangaDexClient { // if this is null, then the server has shutdown private Http4kServer engine; + private Http4kServer webUi; private DiskLruCache cache; public MangaDexClient(ClientSettings clientSettings) { @@ -46,7 +49,7 @@ public class MangaDexClient { // This function also does most of the program initialization. public void runLoop() { - statistics.set(new Statistics()); + statistics.set(new Statistics(0)); loginAndStartServer(); if (serverSettings.getLatestBuild() > Constants.CLIENT_BUILD) { if (LOGGER.isWarnEnabled()) { @@ -59,6 +62,9 @@ public class MangaDexClient { LOGGER.info("MDNet initialization completed successfully. Starting normal operation."); } + webUi = WebUiKt.getUiServer(clientSettings.getWebSettings(), statistics); + webUi.start(); + // we don't really care about the Atomic part here AtomicInteger counter = new AtomicInteger(); // ping keep-alive every 45 seconds @@ -71,7 +77,7 @@ public class MangaDexClient { if (LOGGER.isInfoEnabled()) { LOGGER.info("Hourly update: refreshing statistics"); } - statistics.set(new Statistics()); + statistics.set(new Statistics(statistics.get().getSequenceNumber() + 1)); if (engine == null) { if (LOGGER.isInfoEnabled()) { @@ -218,7 +224,7 @@ public class MangaDexClient { // TODO: system.out redirect ClientSettings finalSettings = settings; new Thread(() -> { - WebConsole webConsole = new WebConsole(finalSettings.getWebSettings().getClientWebsocketPort()) { + WebConsole webConsole = new WebConsole(finalSettings.getWebSettings().getUiWebsocketPort()) { @Override protected void parseMessage(String message) { System.out.println(message); diff --git a/src/main/java/mdnet/base/Statistics.java b/src/main/java/mdnet/base/Statistics.java index 29651d3..1da1fee 100644 --- a/src/main/java/mdnet/base/Statistics.java +++ b/src/main/java/mdnet/base/Statistics.java @@ -1,19 +1,28 @@ package mdnet.base; +import com.google.gson.annotations.SerializedName; + import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; public class Statistics { + @SerializedName("requests_served") private final AtomicInteger requestsServed; + @SerializedName("cache_hits") private final AtomicInteger cacheHits; + @SerializedName("cache_misses") private final AtomicInteger cacheMisses; + @SerializedName("bytes_sent") private final AtomicLong bytesSent; + @SerializedName("sequence_number") + private final int sequenceNumber; - public Statistics() { + public Statistics(int sequenceNumber) { requestsServed = new AtomicInteger(); cacheHits = new AtomicInteger(); cacheMisses = new AtomicInteger(); bytesSent = new AtomicLong(); + this.sequenceNumber = sequenceNumber; } public AtomicInteger getRequestsServed() { @@ -32,9 +41,18 @@ public class Statistics { return bytesSent; } + public int getSequenceNumber() { + return sequenceNumber; + } + @Override public String toString() { - return "Statistics{" + "requestsServed=" + requestsServed + ", cacheHits=" + cacheHits + ", cacheMisses=" - + cacheMisses + ", bytesSent=" + bytesSent + '}'; + return "Statistics{" + + "requestsServed=" + requestsServed + + ", cacheHits=" + cacheHits + + ", cacheMisses=" + cacheMisses + + ", bytesSent=" + bytesSent + + ", sequenceNumber=" + sequenceNumber + + '}'; } } diff --git a/src/main/java/mdnet/base/settings/ClientSettings.java b/src/main/java/mdnet/base/settings/ClientSettings.java index 3e424e2..8e854c5 100644 --- a/src/main/java/mdnet/base/settings/ClientSettings.java +++ b/src/main/java/mdnet/base/settings/ClientSettings.java @@ -32,7 +32,7 @@ public final class ClientSettings { } public ClientSettings(long maxCacheSizeMib, long maxBandwidthMibPerHour, long maxBurstRateKibPerSecond, - int clientPort, String clientSecret, int threads, WebSettings webSettings) { + int clientPort, String clientSecret, int threads, WebSettings webSettings) { this.maxCacheSizeMib = maxCacheSizeMib; this.maxBandwidthMibPerHour = maxBandwidthMibPerHour; this.maxBurstRateKibPerSecond = maxBurstRateKibPerSecond; diff --git a/src/main/java/mdnet/base/settings/WebSettings.java b/src/main/java/mdnet/base/settings/WebSettings.java index 7708789..03ea12d 100644 --- a/src/main/java/mdnet/base/settings/WebSettings.java +++ b/src/main/java/mdnet/base/settings/WebSettings.java @@ -3,23 +3,31 @@ package mdnet.base.settings; import com.google.gson.annotations.SerializedName; public final class WebSettings { - @SerializedName("client_websocket_port") - private final int clientWebsocketPort; + @SerializedName("ui_websocket_port") + private final int uiWebsocketPort; + @SerializedName("ui_port") + private final int uiPort; public WebSettings() { - this.clientWebsocketPort = 33333; + this.uiWebsocketPort = 33333; + this.uiPort = 8080; } - public WebSettings(int clientWebsocketPort) { - this.clientWebsocketPort = clientWebsocketPort; + public WebSettings(int uiWebsocketPort, int uiPort) { + this.uiWebsocketPort = uiWebsocketPort; + this.uiPort = uiPort; } - public int getClientWebsocketPort() { - return clientWebsocketPort; + public int getUiWebsocketPort() { + return uiWebsocketPort; + } + + public int getUiPort() { + return uiPort; } @Override public String toString() { - return "WebSettings{" + "clientWebsocketPort=" + clientWebsocketPort + '}'; + return "WebSettings{" + "uiWebsocketPort=" + uiWebsocketPort + ", uiPort=" + uiPort + '}'; } } diff --git a/src/main/java/mdnet/base/CachingInputStream.java b/src/main/java/mdnet/cache/CachingInputStream.java similarity index 99% rename from src/main/java/mdnet/base/CachingInputStream.java rename to src/main/java/mdnet/cache/CachingInputStream.java index c544e2b..9ea5fcd 100644 --- a/src/main/java/mdnet/base/CachingInputStream.java +++ b/src/main/java/mdnet/cache/CachingInputStream.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package mdnet.base; +package mdnet.cache; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.ProxyInputStream; diff --git a/src/main/java/mdnet/cache/DiskLruCache.java b/src/main/java/mdnet/cache/DiskLruCache.java index 91ad816..e8c0556 100644 --- a/src/main/java/mdnet/cache/DiskLruCache.java +++ b/src/main/java/mdnet/cache/DiskLruCache.java @@ -82,10 +82,9 @@ import java.util.regex.Pattern; *
  • When an entry is being edited, it is not necessary to * supply data for every value; values default to their previous value. * - * Every {@link #editImpl} call must be matched by a call to - * {@link Editor#commit} or {@link Editor#abort}. Committing is atomic: a read - * observes the full set of values as they were before or after the commit, but - * never a mix of values. + * Every {@link #edit} call must be matched by a call to {@link Editor#commit} + * or {@link Editor#abort}. Committing is atomic: a read observes the full set + * of values as they were before or after the commit, but never a mix of values. * *

    * Clients call {@link #get} to read a snapshot of an entry. The read will diff --git a/src/main/java/mdnet/webui/WebConsole.java b/src/main/java/mdnet/webui/WebConsole.java index 9515793..7644b82 100644 --- a/src/main/java/mdnet/webui/WebConsole.java +++ b/src/main/java/mdnet/webui/WebConsole.java @@ -2,8 +2,6 @@ package mdnet.webui; import java.net.InetSocketAddress; import java.nio.ByteBuffer; -import java.util.concurrent.atomic.AtomicReference; -import mdnet.base.Statistics; import org.java_websocket.WebSocket; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.server.WebSocketServer; @@ -78,30 +76,30 @@ public abstract class WebConsole extends WebSocketServer { // } public void sendMessage(String type, Object message) { -// JSONObject out = new JSONObject(); -// switch (type) { -// case "command" : -// out.put("type", "command"); -// out.put("data", message.toString()); -// break; -// case "stats" : -// out.put("type", "stats"); -// AtomicReference temp = (AtomicReference) message; -// out.put("hits", temp.get().getCacheHits()); -// out.put("misses", temp.get().getCacheMisses()); -// out.put("bytes_sent", temp.get().getBytesSent()); -// out.put("req_served", temp.get().getRequestsServed()); -// out.put("dataval", "empty"); -// out.put("dataval", "empty"); -// out.put("dataval", "empty"); -// break; -// case "auth" : -// break; -// default : -// out.put("type", "command"); -// out.put("data", message.toString()); -// break; -// } -// broadcast(out.toString()); + // JSONObject out = new JSONObject(); + // switch (type) { + // case "command" : + // out.put("type", "command"); + // out.put("data", message.toString()); + // break; + // case "stats" : + // out.put("type", "stats"); + // AtomicReference temp = (AtomicReference) message; + // out.put("hits", temp.get().getCacheHits()); + // out.put("misses", temp.get().getCacheMisses()); + // out.put("bytes_sent", temp.get().getBytesSent()); + // out.put("req_served", temp.get().getRequestsServed()); + // out.put("dataval", "empty"); + // out.put("dataval", "empty"); + // out.put("dataval", "empty"); + // break; + // case "auth" : + // break; + // default : + // out.put("type", "command"); + // out.put("data", message.toString()); + // break; + // } + // broadcast(out.toString()); } } diff --git a/src/main/kotlin/mdnet/base/Application.kt b/src/main/kotlin/mdnet/base/web/Application.kt similarity index 90% rename from src/main/kotlin/mdnet/base/Application.kt rename to src/main/kotlin/mdnet/base/web/Application.kt index 88b7e18..3daead4 100644 --- a/src/main/kotlin/mdnet/base/Application.kt +++ b/src/main/kotlin/mdnet/base/web/Application.kt @@ -1,15 +1,18 @@ /* ktlint-disable no-wildcard-imports */ -package mdnet.base +package mdnet.base.web +import mdnet.base.Constants +import mdnet.base.Netty +import mdnet.base.ServerSettings +import mdnet.base.Statistics import mdnet.base.settings.ClientSettings +import mdnet.cache.CachingInputStream import mdnet.cache.DiskLruCache import org.apache.http.client.config.CookieSpecs import org.apache.http.client.config.RequestConfig import org.apache.http.impl.client.HttpClients import org.http4k.client.ApacheClient import org.http4k.core.BodyMode -import org.http4k.core.Filter -import org.http4k.core.HttpHandler import org.http4k.core.Method import org.http4k.core.Request import org.http4k.core.Response @@ -28,8 +31,6 @@ import java.io.BufferedInputStream import java.io.BufferedOutputStream import java.io.InputStream import java.security.MessageDigest -import java.time.ZoneOffset -import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.util.* import java.util.concurrent.Executors @@ -40,7 +41,7 @@ import javax.crypto.CipherOutputStream import javax.crypto.spec.SecretKeySpec private val LOGGER = LoggerFactory.getLogger("Application") -private val THREADS_TO_ALLOCATE = 262144 // 2**18 // Honestly, no reason to not just let 'er rip. Inactive connections will expire on their own :D +private const val THREADS_TO_ALLOCATE = 262144 // 2**18 // Honestly, no reason to not just let 'er rip. Inactive connections will expire on their own :D fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSettings: ClientSettings, statistics: AtomicReference): Http4kServer { val executor = Executors.newCachedThreadPool() @@ -226,30 +227,6 @@ private fun getRc4(key: ByteArray): Cipher { private val HTTP_TIME_FORMATTER = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss O", Locale.ENGLISH) -private fun addCommonHeaders(): Filter { - return Filter { next: HttpHandler -> - { request: Request -> - val response = next(request) - response.header("Date", HTTP_TIME_FORMATTER.format(ZonedDateTime.now(ZoneOffset.UTC))) - .header("Server", "Mangadex@Home Node ${Constants.CLIENT_VERSION} (${Constants.CLIENT_BUILD})") - } - } -} - -private fun catchAllHideDetails(): Filter { - return Filter { next: HttpHandler -> - { request: Request -> - try { - next(request) - } catch (e: Exception) { - if (LOGGER.isWarnEnabled) { - LOGGER.warn("Request error detected", e) - } - Response(Status.INTERNAL_SERVER_ERROR) - } - } - } -} private fun md5Bytes(stringToHash: String): ByteArray { val digest = MessageDigest.getInstance("MD5") diff --git a/src/main/kotlin/mdnet/base/web/WebUi.kt b/src/main/kotlin/mdnet/base/web/WebUi.kt new file mode 100644 index 0000000..baa75c1 --- /dev/null +++ b/src/main/kotlin/mdnet/base/web/WebUi.kt @@ -0,0 +1,37 @@ +/* ktlint-disable no-wildcard-imports */ +package mdnet.base.web + +import mdnet.base.Statistics +import mdnet.base.settings.WebSettings +import org.http4k.core.Body +import org.http4k.core.Method +import org.http4k.core.Response +import org.http4k.core.Status +import org.http4k.core.then +import org.http4k.filter.ServerFilters +import org.http4k.routing.ResourceLoader +import org.http4k.routing.bind +import org.http4k.routing.routes +import org.http4k.routing.singlePageApp +import org.http4k.server.Http4kServer +import org.http4k.server.Netty +import org.http4k.server.asServer +import java.util.concurrent.atomic.AtomicReference +import org.http4k.format.Gson.auto + +fun getUiServer(webSettings: WebSettings, statistics: AtomicReference): Http4kServer { + val statisticsLens = Body.auto().toLens() + + return catchAllHideDetails() + .then(ServerFilters.CatchLensFailure) + .then(addCommonHeaders()) + .then( + routes( + "/api/stats" bind Method.GET to { + statisticsLens(statistics.get(), Response(Status.OK)) + }, + singlePageApp(ResourceLoader.Classpath("/webui")) + ) + ) + .asServer(Netty(webSettings.uiPort)) +} diff --git a/src/main/kotlin/mdnet/base/web/common.kt b/src/main/kotlin/mdnet/base/web/common.kt new file mode 100644 index 0000000..47b2c78 --- /dev/null +++ b/src/main/kotlin/mdnet/base/web/common.kt @@ -0,0 +1,43 @@ +/* ktlint-disable no-wildcard-imports */ +package mdnet.base.web + +import mdnet.base.Constants +import org.http4k.core.Filter +import org.http4k.core.HttpHandler +import org.http4k.core.Request +import org.http4k.core.Response +import org.http4k.core.Status +import org.slf4j.LoggerFactory +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.util.* + +private val HTTP_TIME_FORMATTER = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss O", Locale.ENGLISH) + +private val LOGGER = LoggerFactory.getLogger("Application") + +fun addCommonHeaders(): Filter { + return Filter { next: HttpHandler -> + { request: Request -> + val response = next(request) + response.header("Date", HTTP_TIME_FORMATTER.format(ZonedDateTime.now(ZoneOffset.UTC))) + .header("Server", "Mangadex@Home Node ${Constants.CLIENT_VERSION} (${Constants.CLIENT_BUILD})") + } + } +} + +fun catchAllHideDetails(): Filter { + return Filter { next: HttpHandler -> + { request: Request -> + try { + next(request) + } catch (e: Exception) { + if (LOGGER.isWarnEnabled) { + LOGGER.warn("Request error detected", e) + } + Response(Status.INTERNAL_SERVER_ERROR) + } + } + } +} From 505c917e56672d210885bcb3c30441c5758c7caf Mon Sep 17 00:00:00 2001 From: carbotaniuman <41451839+carbotaniuman@users.noreply.github.com> Date: Fri, 12 Jun 2020 22:35:08 -0500 Subject: [PATCH 6/7] Make Statistics immutable --- src/main/java/mdnet/base/MangaDexClient.java | 145 ++++++++++++------ src/main/java/mdnet/base/Statistics.java | 58 ------- .../mdnet/base/settings/ClientSettings.java | 6 - src/main/java/mdnet/cache/DiskLruCache.java | 2 +- src/main/kotlin/mdnet/base/Netty.kt | 14 +- src/main/kotlin/mdnet/base/Statistics.kt | 11 ++ src/main/kotlin/mdnet/base/web/Application.kt | 26 ++-- 7 files changed, 129 insertions(+), 133 deletions(-) delete mode 100644 src/main/java/mdnet/base/Statistics.java create mode 100644 src/main/kotlin/mdnet/base/Statistics.kt diff --git a/src/main/java/mdnet/base/MangaDexClient.java b/src/main/java/mdnet/base/MangaDexClient.java index 3a09e17..928fc8e 100644 --- a/src/main/java/mdnet/base/MangaDexClient.java +++ b/src/main/java/mdnet/base/MangaDexClient.java @@ -2,7 +2,9 @@ package mdnet.base; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; import mdnet.base.settings.ClientSettings; +import mdnet.base.settings.WebSettings; import mdnet.base.web.ApplicationKt; import mdnet.base.web.WebUiKt; import mdnet.cache.DiskLruCache; @@ -12,13 +14,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; +import java.util.ArrayList; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Pattern; public class MangaDexClient { + private final static Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private final static Logger LOGGER = LoggerFactory.getLogger(MangaDexClient.class); // This lock protects the Http4kServer from concurrent restart attempts @@ -27,13 +31,16 @@ public class MangaDexClient { private final ServerHandler serverHandler; private final ClientSettings clientSettings; private final AtomicReference statistics; - private ServerSettings serverSettings; - // if this is null, then the server has shutdown - private Http4kServer engine; + private ServerSettings serverSettings; + private Http4kServer engine; // if this is null, then the server has shutdown private Http4kServer webUi; private DiskLruCache cache; + // these variables are for runLoop(); + private int counter = 0; + private long lastBytesSent = 0; + public MangaDexClient(ClientSettings clientSettings) { this.clientSettings = clientSettings; this.serverHandler = new ServerHandler(clientSettings); @@ -42,14 +49,23 @@ public class MangaDexClient { try { cache = DiskLruCache.open(new File("cache"), 3, 3, clientSettings.getMaxCacheSizeMib() * 1024 * 1024 /* MiB to bytes */); + + DiskLruCache.Snapshot snapshot = cache.get("statistics"); + if (snapshot != null) { + String json = snapshot.getString(0); + snapshot.close(); + statistics.set(GSON.fromJson(json, new TypeToken>() { + }.getType())); + } else { + statistics.set(new Statistics()); + } + lastBytesSent = statistics.get().getBytesSent(); } catch (IOException e) { MangaDexClient.dieWithError(e); } } - // This function also does most of the program initialization. public void runLoop() { - statistics.set(new Statistics(0)); loginAndStartServer(); if (serverSettings.getLatestBuild() > Constants.CLIENT_BUILD) { if (LOGGER.isWarnEnabled()) { @@ -58,26 +74,19 @@ public class MangaDexClient { } } + if (clientSettings.getWebSettings() != null) { + webUi = WebUiKt.getUiServer(clientSettings.getWebSettings(), statistics); + webUi.start(); + } + if (LOGGER.isInfoEnabled()) { LOGGER.info("MDNet initialization completed successfully. Starting normal operation."); } - webUi = WebUiKt.getUiServer(clientSettings.getWebSettings(), statistics); - webUi.start(); - - // we don't really care about the Atomic part here - AtomicInteger counter = new AtomicInteger(); - // ping keep-alive every 45 seconds executorService.scheduleAtFixedRate(() -> { - int num = counter.get(); - if (num == 80) { - counter.set(0); - - // if server is stopped due to egress limits, restart it - if (LOGGER.isInfoEnabled()) { - LOGGER.info("Hourly update: refreshing statistics"); - } - statistics.set(new Statistics(statistics.get().getSequenceNumber() + 1)); + if (counter == 80) { + counter = 0; + lastBytesSent = statistics.get().getBytesSent(); if (engine == null) { if (LOGGER.isInfoEnabled()) { @@ -87,7 +96,7 @@ public class MangaDexClient { loginAndStartServer(); } } else { - counter.set(num + 1); + counter++; } // if the server is offline then don't try and refresh certs @@ -95,8 +104,9 @@ public class MangaDexClient { return; } - if (clientSettings.getMaxBandwidthMibPerHour() != 0 && clientSettings.getMaxBandwidthMibPerHour() * 1024 - * 1024 /* MiB to bytes */ < statistics.get().getBytesSent().get()) { + long currentBytesSent = statistics.get().getBytesSent() - lastBytesSent; + if (clientSettings.getMaxBandwidthMibPerHour() != 0 + && clientSettings.getMaxBandwidthMibPerHour() * 1024 * 1024 /* MiB to bytes */ < currentBytesSent) { if (LOGGER.isInfoEnabled()) { LOGGER.info("Shutting down server as hourly bandwidth limit reached"); } @@ -170,6 +180,12 @@ public class MangaDexClient { logoutAndStopServer(); } + webUi.close(); + try { + cache.close(); + } catch (IOException e) { + LOGGER.error("Cache failed to close", e); + } } public static void main(String[] args) { @@ -184,47 +200,28 @@ public class MangaDexClient { MangaDexClient.dieWithError("Expected one argument: path to config file, or nothing"); } - Gson gson = new GsonBuilder().setPrettyPrinting().create(); ClientSettings settings; try { - settings = gson.fromJson(new FileReader(file), ClientSettings.class); + settings = GSON.fromJson(new FileReader(file), ClientSettings.class); } catch (FileNotFoundException ignored) { settings = new ClientSettings(); LOGGER.warn("Settings file {} not found, generating file", file); try (FileWriter writer = new FileWriter(file)) { - writer.write(gson.toJson(settings)); + writer.write(GSON.toJson(settings)); } catch (IOException e) { MangaDexClient.dieWithError(e); } } - if (!ClientSettings.isSecretValid(settings.getClientSecret())) - MangaDexClient.dieWithError("Config Error: API Secret is invalid, must be 52 alphanumeric characters"); - - if (settings.getClientPort() == 0) { - MangaDexClient.dieWithError("Config Error: Invalid port number"); - } - - if (settings.getMaxCacheSizeMib() < 1024) { - MangaDexClient.dieWithError("Config Error: Invalid max cache size, must be >= 1024 MiB (1GiB)"); - } - - if (LOGGER.isInfoEnabled()) { - LOGGER.info("Client settings loaded: {}", settings); - } - - MangaDexClient client = new MangaDexClient(settings); - Runtime.getRuntime().addShutdownHook(new Thread(client::shutdown)); - client.runLoop(); + validateSettings(settings); if (settings.getWebSettings() != null) { - // java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); - // System.setOut(new java.io.PrintStream(out)); + WebSettings webSettings = settings.getWebSettings(); + // TODO: system.out redirect - ClientSettings finalSettings = settings; new Thread(() -> { - WebConsole webConsole = new WebConsole(finalSettings.getWebSettings().getUiWebsocketPort()) { + WebConsole webConsole = new WebConsole(webSettings.getUiWebsocketPort()) { @Override protected void parseMessage(String message) { System.out.println(message); @@ -235,6 +232,14 @@ public class MangaDexClient { // TODO: webConsole.sendMessage(t,m) whenever system.out is written to }).start(); } + + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Client settings loaded: {}", settings); + } + + MangaDexClient client = new MangaDexClient(settings); + Runtime.getRuntime().addShutdownHook(new Thread(client::shutdown)); + client.runLoop(); } public static void dieWithError(Throwable e) { @@ -246,8 +251,48 @@ public class MangaDexClient { public static void dieWithError(String error) { if (LOGGER.isErrorEnabled()) { - LOGGER.error("Critical Error: " + error); + LOGGER.error("Critical Error: {}", error); } System.exit(1); } + + public static void validateSettings(ClientSettings settings) { + if (!isSecretValid(settings.getClientSecret())) + MangaDexClient.dieWithError("Config Error: API Secret is invalid, must be 52 alphanumeric characters"); + + if (settings.getClientPort() == 0) { + MangaDexClient.dieWithError("Config Error: Invalid port number"); + } + + if (settings.getMaxCacheSizeMib() < 1024) { + MangaDexClient.dieWithError("Config Error: Invalid max cache size, must be >= 1024 MiB (1GiB)"); + } + + if (settings.getThreads() < 4) { + MangaDexClient.dieWithError("Config Error: Invalid number of threads, must be >= 8"); + } + + if (settings.getMaxBandwidthMibPerHour() < 0) { + MangaDexClient.dieWithError("Config Error: Max bandwidth must be >= 0"); + } + + if (settings.getMaxBurstRateKibPerSecond() < 0) { + MangaDexClient.dieWithError("Config Error: Max burst rate must be >= 0"); + } + + if (settings.getWebSettings() != null) { + if (settings.getWebSettings().getUiPort() == 0) { + MangaDexClient.dieWithError("Config Error: Invalid UI port number"); + } + + if (settings.getWebSettings().getUiWebsocketPort() == 0) { + MangaDexClient.dieWithError("Config Error: Invalid websocket port number"); + } + } + } + + public static boolean isSecretValid(String clientSecret) { + final int CLIENT_KEY_LENGTH = 52; + return Pattern.matches("^[a-zA-Z0-9]{" + CLIENT_KEY_LENGTH + "}$", clientSecret); + } } diff --git a/src/main/java/mdnet/base/Statistics.java b/src/main/java/mdnet/base/Statistics.java deleted file mode 100644 index 1da1fee..0000000 --- a/src/main/java/mdnet/base/Statistics.java +++ /dev/null @@ -1,58 +0,0 @@ -package mdnet.base; - -import com.google.gson.annotations.SerializedName; - -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; - -public class Statistics { - @SerializedName("requests_served") - private final AtomicInteger requestsServed; - @SerializedName("cache_hits") - private final AtomicInteger cacheHits; - @SerializedName("cache_misses") - private final AtomicInteger cacheMisses; - @SerializedName("bytes_sent") - private final AtomicLong bytesSent; - @SerializedName("sequence_number") - private final int sequenceNumber; - - public Statistics(int sequenceNumber) { - requestsServed = new AtomicInteger(); - cacheHits = new AtomicInteger(); - cacheMisses = new AtomicInteger(); - bytesSent = new AtomicLong(); - this.sequenceNumber = sequenceNumber; - } - - public AtomicInteger getRequestsServed() { - return requestsServed; - } - - public AtomicInteger getCacheHits() { - return cacheHits; - } - - public AtomicInteger getCacheMisses() { - return cacheMisses; - } - - public AtomicLong getBytesSent() { - return bytesSent; - } - - public int getSequenceNumber() { - return sequenceNumber; - } - - @Override - public String toString() { - return "Statistics{" + - "requestsServed=" + requestsServed + - ", cacheHits=" + cacheHits + - ", cacheMisses=" + cacheMisses + - ", bytesSent=" + bytesSent + - ", sequenceNumber=" + sequenceNumber + - '}'; - } -} diff --git a/src/main/java/mdnet/base/settings/ClientSettings.java b/src/main/java/mdnet/base/settings/ClientSettings.java index 8e854c5..9c628a7 100644 --- a/src/main/java/mdnet/base/settings/ClientSettings.java +++ b/src/main/java/mdnet/base/settings/ClientSettings.java @@ -3,7 +3,6 @@ package mdnet.base.settings; import com.google.gson.annotations.SerializedName; import java.util.Objects; -import java.util.regex.Pattern; public final class ClientSettings { @SerializedName("max_cache_size_mib") @@ -75,9 +74,4 @@ public final class ClientSettings { + maxBandwidthMibPerHour + ", maxBurstRateKibPerSecond=" + maxBurstRateKibPerSecond + ", clientPort=" + clientPort + ", clientSecret='" + "" + '\'' + ", threads=" + getThreads() + '}'; } - - public static boolean isSecretValid(String clientSecret) { - final int CLIENT_KEY_LENGTH = 52; - return Pattern.matches("^[a-zA-Z0-9]{" + CLIENT_KEY_LENGTH + "}$", clientSecret); - } } diff --git a/src/main/java/mdnet/cache/DiskLruCache.java b/src/main/java/mdnet/cache/DiskLruCache.java index e8c0556..8a9da44 100644 --- a/src/main/java/mdnet/cache/DiskLruCache.java +++ b/src/main/java/mdnet/cache/DiskLruCache.java @@ -411,7 +411,7 @@ public final class DiskLruCache implements Closeable { return getImpl(key); } - public synchronized Snapshot getImpl(String key) throws IOException { + private synchronized Snapshot getImpl(String key) throws IOException { checkNotClosed(); Entry entry = lruEntries.get(key); if (entry == null) { diff --git a/src/main/kotlin/mdnet/base/Netty.kt b/src/main/kotlin/mdnet/base/Netty.kt index ccb0490..3b8d9c1 100644 --- a/src/main/kotlin/mdnet/base/Netty.kt +++ b/src/main/kotlin/mdnet/base/Netty.kt @@ -37,26 +37,26 @@ import javax.net.ssl.SSLException private val LOGGER = LoggerFactory.getLogger("Application") -class Netty(private val tls: ServerSettings.TlsCert, private val clientSettings: ClientSettings, private val stats: AtomicReference) : ServerConfig { - private val threadsToAllocate = clientSettings.getThreads() - +class Netty(private val tls: ServerSettings.TlsCert, private val clientSettings: ClientSettings, private val statistics: AtomicReference) : ServerConfig { override fun toServer(httpHandler: HttpHandler): Http4kServer = object : Http4kServer { - private val masterGroup = NioEventLoopGroup(threadsToAllocate) - private val workerGroup = NioEventLoopGroup(threadsToAllocate) + private val masterGroup = NioEventLoopGroup(clientSettings.threads) + private val workerGroup = NioEventLoopGroup(clientSettings.threads) private lateinit var closeFuture: ChannelFuture private lateinit var address: InetSocketAddress private val burstLimiter = object : GlobalTrafficShapingHandler( workerGroup, 1024 * clientSettings.maxBurstRateKibPerSecond, 0, 50) { override fun doAccounting(counter: TrafficCounter) { - stats.get().bytesSent.getAndAdd(counter.cumulativeWrittenBytes()) + statistics.getAndUpdate { + it.copy(bytesSent = it.bytesSent + counter.cumulativeWrittenBytes()) + } counter.resetCumulativeTime() } } override fun start(): Http4kServer = apply { if (LOGGER.isInfoEnabled) { - LOGGER.info("Starting webserver with {} threads", threadsToAllocate) + LOGGER.info("Starting webserver with {} threads", clientSettings.threads) } val (mainCert, chainCert) = getX509Certs(tls.certificate) diff --git a/src/main/kotlin/mdnet/base/Statistics.kt b/src/main/kotlin/mdnet/base/Statistics.kt new file mode 100644 index 0000000..8c725b5 --- /dev/null +++ b/src/main/kotlin/mdnet/base/Statistics.kt @@ -0,0 +1,11 @@ +package mdnet.base + +import com.google.gson.annotations.SerializedName + +data class Statistics( + @field:SerializedName("requests_served") val requestsServed: Int = 0, + @field:SerializedName("cache_hits") val cacheHits: Int = 0, + @field:SerializedName("cache_misses") val cacheMisses: Int = 0, + @field:SerializedName("browser_cached") val browserCached: Int = 0, + @field:SerializedName("bytes_sent") val bytesSent: Long = 0 +) diff --git a/src/main/kotlin/mdnet/base/web/Application.kt b/src/main/kotlin/mdnet/base/web/Application.kt index 3daead4..985132f 100644 --- a/src/main/kotlin/mdnet/base/web/Application.kt +++ b/src/main/kotlin/mdnet/base/web/Application.kt @@ -31,8 +31,6 @@ import java.io.BufferedInputStream import java.io.BufferedOutputStream import java.io.InputStream import java.security.MessageDigest -import java.time.format.DateTimeFormatter -import java.util.* import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicReference import javax.crypto.Cipher @@ -59,7 +57,6 @@ fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSetting .build()) .setMaxConnTotal(THREADS_TO_ALLOCATE) .setMaxConnPerRoute(THREADS_TO_ALLOCATE) - // Have it at the maximum open sockets a user can have in most modern OSes. No reason to limit this, just limit it at the Netty side. .build()) val app = { dataSaver: Boolean -> @@ -83,8 +80,9 @@ fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSetting md5Bytes("$chapterHash.$fileName") } val cacheId = printHexString(rc4Bytes) - - statistics.get().requestsServed.incrementAndGet() + statistics.getAndUpdate { + it.copy(requestsServed = it.requestsServed + 1) + } // Netty doesn't do Content-Length or Content-Type, so we have the pleasure of doing that ourselves fun respondWithImage(input: InputStream, length: String?, type: String, lastModified: String?): Response = @@ -113,10 +111,12 @@ fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSetting val snapshot = cache.get(cacheId) if (snapshot != null) { - statistics.get().cacheHits.incrementAndGet() - // our files never change, so it's safe to use the browser cache if (request.header("If-Modified-Since") != null) { + statistics.getAndUpdate { + it.copy(browserCached = it.browserCached + 1) + } + if (LOGGER.isInfoEnabled) { LOGGER.info("Request for $sanitizedUri cached by browser") } @@ -127,6 +127,10 @@ fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSetting Response(Status.NOT_MODIFIED) .header("Last-Modified", lastModified) } else { + statistics.getAndUpdate { + it.copy(cacheHits = it.cacheHits + 1) + } + if (LOGGER.isInfoEnabled) { LOGGER.info("Request for $sanitizedUri hit cache") } @@ -137,7 +141,10 @@ fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSetting ) } } else { - statistics.get().cacheMisses.incrementAndGet() + statistics.getAndUpdate { + it.copy(cacheMisses = it.cacheMisses + 1) + } + if (LOGGER.isInfoEnabled) { LOGGER.info("Request for $sanitizedUri missed cache") } @@ -225,9 +232,6 @@ private fun getRc4(key: ByteArray): Cipher { return rc4 } -private val HTTP_TIME_FORMATTER = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss O", Locale.ENGLISH) - - private fun md5Bytes(stringToHash: String): ByteArray { val digest = MessageDigest.getInstance("MD5") return digest.digest(stringToHash.toByteArray()) From 9f1f30eacee5b3a99605af9123af50d34605493a Mon Sep 17 00:00:00 2001 From: carbotaniuman <41451839+carbotaniuman@users.noreply.github.com> Date: Sat, 13 Jun 2020 11:16:56 -0500 Subject: [PATCH 7/7] Add hourly cache of stats --- src/main/java/mdnet/base/MangaDexClient.java | 37 +++---- src/main/java/mdnet/cache/DiskLruCache.java | 27 ++--- src/main/java/mdnet/webui/WebConsole.java | 105 ------------------- src/main/kotlin/mdnet/base/web/WebUi.kt | 13 ++- src/main/resources/logback.xml | 1 + 5 files changed, 39 insertions(+), 144 deletions(-) delete mode 100644 src/main/java/mdnet/webui/WebConsole.java diff --git a/src/main/java/mdnet/base/MangaDexClient.java b/src/main/java/mdnet/base/MangaDexClient.java index 928fc8e..fe815c7 100644 --- a/src/main/java/mdnet/base/MangaDexClient.java +++ b/src/main/java/mdnet/base/MangaDexClient.java @@ -4,17 +4,19 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import mdnet.base.settings.ClientSettings; -import mdnet.base.settings.WebSettings; import mdnet.base.web.ApplicationKt; import mdnet.base.web.WebUiKt; import mdnet.cache.DiskLruCache; -import mdnet.webui.WebConsole; import org.http4k.server.Http4kServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; +import java.time.Instant; import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -30,6 +32,14 @@ public class MangaDexClient { private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); private final ServerHandler serverHandler; private final ClientSettings clientSettings; + + private final Map statsMap = Collections + .synchronizedMap(new LinkedHashMap(80) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return this.size() > 80; + } + }); private final AtomicReference statistics; private ServerSettings serverSettings; @@ -74,8 +84,10 @@ public class MangaDexClient { } } + statsMap.put(Instant.now(), statistics.get()); + if (clientSettings.getWebSettings() != null) { - webUi = WebUiKt.getUiServer(clientSettings.getWebSettings(), statistics); + webUi = WebUiKt.getUiServer(clientSettings.getWebSettings(), statistics, statsMap); webUi.start(); } @@ -99,6 +111,8 @@ public class MangaDexClient { counter++; } + statsMap.put(Instant.now(), statistics.get()); + // if the server is offline then don't try and refresh certs if (engine == null) { return; @@ -216,23 +230,6 @@ public class MangaDexClient { validateSettings(settings); - if (settings.getWebSettings() != null) { - WebSettings webSettings = settings.getWebSettings(); - - // TODO: system.out redirect - new Thread(() -> { - WebConsole webConsole = new WebConsole(webSettings.getUiWebsocketPort()) { - @Override - protected void parseMessage(String message) { - System.out.println(message); - // TODO: something happens here - // the message should be formatted in json - } - }; - // TODO: webConsole.sendMessage(t,m) whenever system.out is written to - }).start(); - } - if (LOGGER.isInfoEnabled()) { LOGGER.info("Client settings loaded: {}", settings); } diff --git a/src/main/java/mdnet/cache/DiskLruCache.java b/src/main/java/mdnet/cache/DiskLruCache.java index 8a9da44..7be9b5e 100644 --- a/src/main/java/mdnet/cache/DiskLruCache.java +++ b/src/main/java/mdnet/cache/DiskLruCache.java @@ -966,20 +966,7 @@ public final class DiskLruCache implements Closeable { Path oldCache = Paths.get(directory + File.separator + key + "." + i); Path newCache = Paths.get(directory + subKeyPath + File.separator + key + "." + i); - File newCacheDirectory = new File(directory + subKeyPath, key + "." + i + ".tmp"); - newCacheDirectory.getParentFile().mkdirs(); - - if (Files.exists(oldCache)) { - try { - Files.move(oldCache, newCache, StandardCopyOption.ATOMIC_MOVE); - } catch (FileAlreadyExistsException faee) { - try { - Files.delete(oldCache); - } catch (IOException ex) { - } - } catch (IOException ex) { - } - } + migrateCacheFile(i, oldCache, newCache); return new File(directory + subKeyPath, key + "." + i); } @@ -989,6 +976,12 @@ public final class DiskLruCache implements Closeable { Path oldCache = Paths.get(directory + File.separator + key + "." + i + ".tmp"); Path newCache = Paths.get(directory + subKeyPath + File.separator + key + "." + i + ".tmp"); + migrateCacheFile(i, oldCache, newCache); + + return new File(directory + subKeyPath, key + "." + i + ".tmp"); + } + + private void migrateCacheFile(int i, Path oldCache, Path newCache) { File newCacheDirectory = new File(directory + subKeyPath, key + "." + i + ".tmp"); newCacheDirectory.getParentFile().mkdirs(); @@ -998,13 +991,11 @@ public final class DiskLruCache implements Closeable { } catch (FileAlreadyExistsException faee) { try { Files.delete(oldCache); - } catch (IOException ex) { + } catch (IOException ignored) { } - } catch (IOException ex) { + } catch (IOException ignored) { } } - - return new File(directory + subKeyPath, key + "." + i + ".tmp"); } } } diff --git a/src/main/java/mdnet/webui/WebConsole.java b/src/main/java/mdnet/webui/WebConsole.java deleted file mode 100644 index 7644b82..0000000 --- a/src/main/java/mdnet/webui/WebConsole.java +++ /dev/null @@ -1,105 +0,0 @@ -package mdnet.webui; - -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import org.java_websocket.WebSocket; -import org.java_websocket.handshake.ClientHandshake; -import org.java_websocket.server.WebSocketServer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public abstract class WebConsole extends WebSocketServer { - - private final static Logger LOGGER = LoggerFactory.getLogger(WebConsole.class); - - public WebConsole(int port) { - super(new InetSocketAddress(port)); - } - - @Override - public void onOpen(WebSocket conn, ClientHandshake handshake) { - LOGGER.info("Webclient {} connected", conn); - } - - @Override - public void onClose(WebSocket conn, int code, String reason, boolean remote) { - LOGGER.info("Webclient {} disconnected: {} ", conn, reason); - } - - @Override - public void onMessage(WebSocket conn, String message) { - parseMessage(message); - } - - @Override - public void onMessage(WebSocket conn, ByteBuffer message) { - // parseMessage(message.array().toString()); - } - - @Override - public void onError(WebSocket conn, Exception ex) { - ex.printStackTrace(); - if (conn != null) { - // some errors like port binding failed may not be assignable to a specific - // websocket - } - } - - @Override - public void onStart() { - LOGGER.info("Listening for connections on port: {}", this.getPort()); - setConnectionLostTimeout(0); - setConnectionLostTimeout(100); - } - - protected abstract void parseMessage(String message); - - // void parseCommand(String x) { - // switch (x) { - // case "help": - // this.broadcast(formatMessage("command", "Available commands:")); - // this.broadcast(formatMessage("command", "you")); - // this.broadcast(formatMessage("command", "are")); - // this.broadcast(formatMessage("command", "big")); - // this.broadcast(formatMessage("command", "gay")); - // break; - // case "stop": - // this.broadcast(formatMessage("command", "Mangadex Client has shut down, - // shutting down web client now")); - // return; - // default: - // this.broadcast(formatMessage("command", "That command was not recognized")); - // this.broadcast(formatMessage("command", "Try help for a list of available - // commands")); - // break; - // } - // } - - public void sendMessage(String type, Object message) { - // JSONObject out = new JSONObject(); - // switch (type) { - // case "command" : - // out.put("type", "command"); - // out.put("data", message.toString()); - // break; - // case "stats" : - // out.put("type", "stats"); - // AtomicReference temp = (AtomicReference) message; - // out.put("hits", temp.get().getCacheHits()); - // out.put("misses", temp.get().getCacheMisses()); - // out.put("bytes_sent", temp.get().getBytesSent()); - // out.put("req_served", temp.get().getRequestsServed()); - // out.put("dataval", "empty"); - // out.put("dataval", "empty"); - // out.put("dataval", "empty"); - // break; - // case "auth" : - // break; - // default : - // out.put("type", "command"); - // out.put("data", message.toString()); - // break; - // } - // broadcast(out.toString()); - } -} diff --git a/src/main/kotlin/mdnet/base/web/WebUi.kt b/src/main/kotlin/mdnet/base/web/WebUi.kt index baa75c1..995dc56 100644 --- a/src/main/kotlin/mdnet/base/web/WebUi.kt +++ b/src/main/kotlin/mdnet/base/web/WebUi.kt @@ -18,9 +18,15 @@ import org.http4k.server.Netty import org.http4k.server.asServer import java.util.concurrent.atomic.AtomicReference import org.http4k.format.Gson.auto +import java.time.Instant -fun getUiServer(webSettings: WebSettings, statistics: AtomicReference): Http4kServer { +fun getUiServer( + webSettings: WebSettings, + statistics: AtomicReference, + statsMap: Map +): Http4kServer { val statisticsLens = Body.auto().toLens() + val statsMapLens = Body.auto>().toLens() return catchAllHideDetails() .then(ServerFilters.CatchLensFailure) @@ -30,6 +36,11 @@ fun getUiServer(webSettings: WebSettings, statistics: AtomicReference + log/latest.log