diff --git a/build.gradle b/build.gradle index 356bfc7..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" + 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/dev/settings.json b/dev/settings.json index 515d8e4..38458b5 100644 --- a/dev/settings.json +++ b/dev/settings.json @@ -1,8 +1,12 @@ { - "client_secret": "7rc7p00md0n0xsvqnv4rv17fthvjjrzpdghak1yq45833zvdvnb0", + "client_secret": "s76t1dazfctvtgq9dyvgw9herxc4gcz39q0q0y3taxpkgg0ahq8g", "max_cache_size_mib": 2048, - "client_port": 8080, - "max_burst_rate_kib_per_second": 100, - "max_bandwidth_mib_per_hour": 1, - "threads_per_cpu": 32 + "client_port": 443, + "max_burst_rate_kib_per_second": 0, + "max_bandwidth_mib_per_hour": 0, + "threads_per_cpu": 32, + "web_settings": + { + "ui_port": 8080 + } } diff --git a/src/main/java/mdnet/base/MangaDexClient.java b/src/main/java/mdnet/base/MangaDexClient.java index b02ba49..1ec64be 100644 --- a/src/main/java/mdnet/base/MangaDexClient.java +++ b/src/main/java/mdnet/base/MangaDexClient.java @@ -1,22 +1,30 @@ 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.web.ApplicationKt; +import mdnet.base.web.WebUiKt; import mdnet.cache.DiskLruCache; 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.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; -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 @@ -24,13 +32,25 @@ public class MangaDexClient { private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); 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 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; + 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); @@ -39,14 +59,22 @@ 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, Statistics.class)); + } 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()); loginAndStartServer(); if (serverSettings.getLatestBuild() > Constants.CLIENT_BUILD) { if (LOGGER.isWarnEnabled()) { @@ -55,23 +83,21 @@ public class MangaDexClient { } } + statsMap.put(Instant.now(), statistics.get()); + + if (clientSettings.getWebSettings() != null) { + webUi = WebUiKt.getUiServer(clientSettings.getWebSettings(), statistics, statsMap); + webUi.start(); + } + if (LOGGER.isInfoEnabled()) { LOGGER.info("MDNet initialization completed successfully. Starting normal operation."); } - // 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()); + if (counter == 80) { + counter = 0; + lastBytesSent = statistics.get().getBytesSent(); if (engine == null) { if (LOGGER.isInfoEnabled()) { @@ -81,16 +107,30 @@ public class MangaDexClient { loginAndStartServer(); } } else { - counter.set(num + 1); + counter++; } + statsMap.put(Instant.now(), statistics.get()); + + try { + DiskLruCache.Editor editor = cache.edit("statistics"); + if (editor != null) { + String json = GSON.toJson(statistics.get(), Statistics.class); + editor.setString(0, json); + editor.setString(1, ""); + editor.setString(2, ""); + editor.commit(); + } + } catch (IOException ignored) {} + // if the server is offline then don't try and refresh certs if (engine == null) { 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"); } @@ -164,6 +204,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) { @@ -171,37 +217,36 @@ public class MangaDexClient { + ") initializing\n"); System.out.println("Copyright (c) 2020, MangaDex Network"); - 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"); - } - - 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 (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(); - } catch (FileNotFoundException e) { - MangaDexClient.dieWithError(e); + 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"); } + + ClientSettings settings; + + try { + 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); + } + } + + validateSettings(settings); + + 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) { @@ -213,8 +258,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/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/Statistics.java b/src/main/java/mdnet/base/Statistics.java deleted file mode 100644 index 29651d3..0000000 --- a/src/main/java/mdnet/base/Statistics.java +++ /dev/null @@ -1,40 +0,0 @@ -package mdnet.base; - -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; - -public class Statistics { - private final AtomicInteger requestsServed; - private final AtomicInteger cacheHits; - private final AtomicInteger cacheMisses; - private final AtomicLong bytesSent; - - public Statistics() { - requestsServed = new AtomicInteger(); - cacheHits = new AtomicInteger(); - cacheMisses = new AtomicInteger(); - bytesSent = new AtomicLong(); - } - - public AtomicInteger getRequestsServed() { - return requestsServed; - } - - public AtomicInteger getCacheHits() { - return cacheHits; - } - - public AtomicInteger getCacheMisses() { - return cacheMisses; - } - - public AtomicLong getBytesSent() { - return bytesSent; - } - - @Override - public String toString() { - return "Statistics{" + "requestsServed=" + requestsServed + ", cacheHits=" + cacheHits + ", cacheMisses=" - + cacheMisses + ", bytesSent=" + bytesSent + '}'; - } -} diff --git a/src/main/java/mdnet/base/ClientSettings.java b/src/main/java/mdnet/base/settings/ClientSettings.java similarity index 74% rename from src/main/java/mdnet/base/ClientSettings.java rename to src/main/java/mdnet/base/settings/ClientSettings.java index fdf55b2..9c628a7 100644 --- a/src/main/java/mdnet/base/ClientSettings.java +++ b/src/main/java/mdnet/base/settings/ClientSettings.java @@ -1,9 +1,8 @@ -package mdnet.base; +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") @@ -18,15 +17,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 +60,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 @@ -59,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/base/settings/WebSettings.java b/src/main/java/mdnet/base/settings/WebSettings.java new file mode 100644 index 0000000..03ea12d --- /dev/null +++ b/src/main/java/mdnet/base/settings/WebSettings.java @@ -0,0 +1,33 @@ +package mdnet.base.settings; + +import com.google.gson.annotations.SerializedName; + +public final class WebSettings { + @SerializedName("ui_websocket_port") + private final int uiWebsocketPort; + @SerializedName("ui_port") + private final int uiPort; + + public WebSettings() { + this.uiWebsocketPort = 33333; + this.uiPort = 8080; + } + + public WebSettings(int uiWebsocketPort, int uiPort) { + this.uiWebsocketPort = uiWebsocketPort; + this.uiPort = uiPort; + } + + public int getUiWebsocketPort() { + return uiWebsocketPort; + } + + public int getUiPort() { + return uiPort; + } + + @Override + public String toString() { + 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..7be9b5e 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 @@ -412,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) { @@ -967,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); } @@ -990,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(); @@ -999,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 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/Netty.kt b/src/main/kotlin/mdnet/base/Netty.kt index 175d732..3b8d9c1 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 @@ -36,33 +37,33 @@ 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) { + 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) 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) diff --git a/src/main/kotlin/mdnet/base/Statistics.kt b/src/main/kotlin/mdnet/base/Statistics.kt new file mode 100644 index 0000000..3d841ef --- /dev/null +++ b/src/main/kotlin/mdnet/base/Statistics.kt @@ -0,0 +1,12 @@ +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, + @field:SerializedName("bytes_on_disk") val bytesOnDisk: Long = 0 +) diff --git a/src/main/kotlin/mdnet/base/Application.kt b/src/main/kotlin/mdnet/base/web/Application.kt similarity index 61% rename from src/main/kotlin/mdnet/base/Application.kt rename to src/main/kotlin/mdnet/base/web/Application.kt index c380398..985132f 100644 --- a/src/main/kotlin/mdnet/base/Application.kt +++ b/src/main/kotlin/mdnet/base/web/Application.kt @@ -1,14 +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 @@ -27,10 +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 import java.util.concurrent.atomic.AtomicReference import javax.crypto.Cipher @@ -39,7 +39,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() @@ -49,16 +49,15 @@ 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) .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 -> @@ -81,47 +80,43 @@ 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?, cached: Boolean): 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 - } - } - .let { - if (cached != null && cached == true) { - it.header("X-Cache", "HIT") - } else { - it.header("X-Cache", "MISS") - } - } + 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 + } + } 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") } @@ -130,20 +125,26 @@ fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSetting snapshot.close() Response(Status.NOT_MODIFIED) - .header("Last-Modified", lastModified) + .header("Last-Modified", lastModified) } else { + statistics.getAndUpdate { + it.copy(cacheHits = it.cacheHits + 1) + } + 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), - true + CipherInputStream(BufferedInputStream(snapshot.getInputStream(0)), getRc4(rc4Bytes)), + snapshot.getLength(0).toString(), snapshot.getString(1), snapshot.getString(2) ) } } else { - statistics.get().cacheMisses.incrementAndGet() + statistics.getAndUpdate { + it.copy(cacheMisses = it.cacheMisses + 1) + } + if (LOGGER.isInfoEnabled) { LOGGER.info("Request for $sanitizedUri missed cache") } @@ -176,8 +177,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 @@ -195,14 +196,14 @@ fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSetting editor.abort() } } - respondWithImage(tee, contentLength, contentType, lastModified, false) + respondWithImage(tee, contentLength, contentType, lastModified) } else { editor?.abort() if (LOGGER.isTraceEnabled) { LOGGER.trace("Request for $sanitizedUri is being served") } - respondWithImage(mdResponse.body.stream, contentLength, contentType, lastModified, false) + respondWithImage(mdResponse.body.stream, contentLength, contentType, lastModified) } } } @@ -212,17 +213,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) + .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 { @@ -231,33 +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 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") return digest.digest(stringToHash.toByteArray()) 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..cd068ca --- /dev/null +++ b/src/main/kotlin/mdnet/base/web/WebUi.kt @@ -0,0 +1,47 @@ +/* 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 +import java.time.Instant + +fun getUiServer( + webSettings: WebSettings, + statistics: AtomicReference, + statsMap: Map +): Http4kServer { + val statsMapLens = Body.auto>().toLens() + + return catchAllHideDetails() + .then(ServerFilters.CatchLensFailure) + .then(addCommonHeaders()) + .then( + routes( + "/api/stats" bind Method.GET to { + statsMapLens(mapOf(Instant.now() to statistics.get()), Response(Status.OK)) + }, + "/api/pastStats" bind Method.GET to { + synchronized(statsMap) { + statsMapLens(statsMap, 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) + } + } + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index b402243..4efaaf9 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,4 +1,5 @@ + ${file-level:-TRACE} diff --git a/src/main/resources/webui/dataReceive.js b/src/main/resources/webui/dataReceive.js new file mode 100644 index 0000000..ab407c8 --- /dev/null +++ b/src/main/resources/webui/dataReceive.js @@ -0,0 +1,705 @@ +let connection; +let theme; +let style; +let port; +let ip; +let refreshRate; +let maxConsoleLines; +let graphTimeFrame; +let showConsoleLatest; +let doAnimations; +let lockDash; +//non-option var +let statRequest; +//stat vars +let hitmiss, + byte, + cached, + req; + +//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) { + $(".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) + } + }); + loadStuff(); + fetch("/api/allStats") + .then(response => async function () { + let respj = JSON.parse(await response.text()); + updateValues(respj); + console.log(respj); + }); + statRequest = setInterval(getStats, refreshRate); +}); + +function loadStuff() { + hitmiss = new Chart(document.getElementById('hitpie').getContext('2d'), { + type: 'doughnut', + data: { + datasets: [{ + data: [0, 0, 0] + }], + labels: [ + 'Hits', + 'Misses', + 'Browser Cached' + ] + }, + options: {} + }); + req = new Chart(document.getElementById('requestsserved').getContext('2d'), { + type: 'line', + data: { + labels: [], + datasets: [{ + label: 'Requests Served', + backgroundColor: "#f00", + borderColor: "#f00", + data: [], + fill: false + }] + }, + options: { + maintainAspectRatio: false, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + } + }); + byte = new Chart(document.getElementById('bytessent').getContext('2d'), { + type: 'line', + data: { + labels: [], + datasets: [{ + label: 'Bytes Sent', + backgroundColor: "#f00", + borderColor: "#f00", + data: [], + fill: false + }] + }, + options: { + maintainAspectRatio: false, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + } + }); + cached = new Chart(document.getElementById('browsercached').getContext('2d'), { + type: 'line', + data: { + labels: [], + datasets: [{ + label: 'Bytes On Disk', + backgroundColor: "#f00", + borderColor: "#f00", + data: [], + fill: false + }] + }, + options: { + maintainAspectRatio: false, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + } + }); +} + +//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, + lock_dashboard: 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; + lockDash = options.lock_dashboard; + $("#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); + $("#lockDash").prop("checked", lockDash) +} + +function resetOptions() { + if (confirm("Do you really want to reset all customizations 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); + dashlayout.destroy(); + localStorage.removeItem('dashState'); + loadDash(); + selectTab('dash', 'dashb'); + let dash = $("#dashboard"); + dashlayout.updateSize(dash.width(), dash.height()); + 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"), + lock_dashboard: $("#lockDash").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); + } + 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)); +} + +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) + $(document.body).children().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) + $(document.body).children().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 getStats() { + fetch("/api/stats") + .then(response => response.json()) + .then(response => { + updateValues(response); + console.log(response); + }); + //TODO: use values and update web info +} + +function updateValues(data) { + for (let key in data) { + if (data.hasOwnProperty(key)) { + let x = data[key]; + hitmiss.data.datasets[0].data[0] = x.cache_hits; + hitmiss.data.datasets[0].data[1] = x.cache_misses; + hitmiss.data.datasets[0].data[2] = x.browser_cached; + + hitmiss.update() + req.data.labels.push(key); + req.data.datasets.forEach((dataset) => { + dataset.data.push(x.requests_served); + }); + req.update() + byte.data.labels.push(key); + byte.data.datasets.forEach((dataset) => { + dataset.data.push(x.bytes_sent); + }); + byte.update() + cached.data.labels.push(key); + cached.data.datasets.forEach((dataset) => { + dataset.data.push(x.bytes_on_disk); + }); + cached.update() + } + } +} + +//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); + + }; + 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/close.png b/src/main/resources/webui/icons/close.png new file mode 100644 index 0000000..8c030e8 Binary files /dev/null and b/src/main/resources/webui/icons/close.png differ diff --git a/src/main/resources/webui/icons/console.png b/src/main/resources/webui/icons/console.png new file mode 100644 index 0000000..560c4e4 Binary files /dev/null and b/src/main/resources/webui/icons/console.png differ diff --git a/src/main/resources/webui/icons/dashboard.png b/src/main/resources/webui/icons/dashboard.png new file mode 100644 index 0000000..e314766 Binary files /dev/null and b/src/main/resources/webui/icons/dashboard.png differ diff --git a/src/main/resources/webui/icons/info.png b/src/main/resources/webui/icons/info.png new file mode 100644 index 0000000..7e6f797 Binary files /dev/null and b/src/main/resources/webui/icons/info.png differ diff --git a/src/main/resources/webui/icons/options.png b/src/main/resources/webui/icons/options.png new file mode 100644 index 0000000..44a20ca Binary files /dev/null and b/src/main/resources/webui/icons/options.png differ diff --git a/src/main/resources/webui/icons/showmore.png b/src/main/resources/webui/icons/showmore.png new file mode 100644 index 0000000..839745f Binary files /dev/null and b/src/main/resources/webui/icons/showmore.png differ diff --git a/src/main/resources/webui/index.html b/src/main/resources/webui/index.html new file mode 100644 index 0000000..6f43fa6 --- /dev/null +++ b/src/main/resources/webui/index.html @@ -0,0 +1,126 @@ + + + + + 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..03c63cb --- /dev/null +++ b/src/main/resources/webui/layout.css @@ -0,0 +1,680 @@ +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; + 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; +} + +#dashboard{ + position: absolute; + margin: 20px; + width: calc(100% - 40px); + height: calc(100% - 140px); + top: 100px; +} + +.numerical_data { + height: 100%; + width: 100%; +} + +#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: 100%;*/ +/* width: 100%;*/ +/*}*/ + +/*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; +} + +.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; + width: 150px; + height: 30px; +} + +.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; + } +} + +/*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; + top: 1px; + 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 new file mode 100644 index 0000000..e3c26b1 --- /dev/null +++ b/src/main/resources/webui/themes/darkTheme.css @@ -0,0 +1,334 @@ +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: #555555; +} + +#gDat { +} + +.line_graph_data { + background-color: #555555; +} + +/*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 { + +} + +/*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/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..0a26f14 --- /dev/null +++ b/src/main/resources/webui/themes/lightTheme.css @@ -0,0 +1,329 @@ +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 { + +} + +/*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/midnightTheme.css b/src/main/resources/webui/themes/midnightTheme.css new file mode 100644 index 0000000..9791d4b --- /dev/null +++ b/src/main/resources/webui/themes/midnightTheme.css @@ -0,0 +1,327 @@ +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 { + +} + +.numerical_data { + background-color: #353535; +} + +.line_graph_data { + background-color: #353535; +} + +/*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 { + +} + +/*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/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..d1b83e1 --- /dev/null +++ b/src/main/resources/webui/themes/softStyle.css @@ -0,0 +1,165 @@ +/*Content holder positions*/ + +#pageBar { +} + +#connection { +} + +#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: 0 0 10px 10px; +} + +#gDat { + border-radius: 10px; +} + +.line_graph_data { + border-radius: 0 0 10px 10px; +} + +/*Console and options positions*/ + +#console { +} + +#buttonBoard { +} + +#liveConsole { + border-radius: 10px; +} + +#console_input { +} + +#console_text { + border-radius: 10px; +} + +/*Web option positions*/ +#dashOptions { +} + +#options { +} + +#apply { +} + +.option { +} + +.optionLabel { + +} + +.optionInput { +} + +/*misc modifications*/ + +.input { + padding-left: 5px; + border-radius: 10px; +} + +.button{ + 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 { +} + +/*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; +} +