Minetrack 5 (#143)

* remove unused #getServer methods, inline #roundToPoint

* replace #safeName regex with incremental ids

* remove legacy #setInterval based #updateMojangServices handling

* add Tooltip class, move faviconSize to css instead of js

* move server id assignment to ServerRegistry

* move printPort logic to formatMinecraftServerAddress, add MINECRAFT_DEFAULT_PORTS

* simplify ping tracking

* rework perc-bar tooltip to not use mousemove event

* begin moving graphing logic to GraphDisplayManager

* begin merge graph point tracking into graphDisplayManager

* centralizing graphing logic into GraphDisplayManager

* properly reset GraphDisplayManager when handling disconnects

* move individual server graph data into ServerGraph class

* constantly run sortServers loop to simplify logic

* inline #updateMojangServices method

* resize performance improvements

* remove legacy bootTime refresh behavior, require manual user refresh

* move class defs to core.js

* remove unused #isGraphDataVisible arg

* remove #toggleControlsDrawer

* dont call #updatePercentageBar in #updateServerStatus calls

* centralize caption handling

* inline #msToTime

* remove hackish seconds handling for timestamps

* reduce #forEach calls with filter/map

* safely fallback to errorMessage if errno/description does not match

* Add /images/missing_favicon.png path instead of putting base64 in js

* remove debug

* cleanup mojang status handling

* move historyPlot instance into GraphDisplayManager

* cleanup checkbox html generation

* cleanup #updateServerStatus

* fix up tooltip styling

* move jquery code out of core.js

* fix add server race condition when initially pinging servers

* send error.placeholder=true for pending pings so the frontend can discard later

* filter placeholder pings sent by the backend

* del assets/images/logo_2014.png

* move graph code into graph.js

* merge pingTracker into ServerRegistry+ServerGraph

* remove todos

* simplify getVisibleGraphData

* fix potential sortServers race condition when adding

* use #show instead of #fadeIn(0)

* remove publicConfig.json, send over socket

* update docs/CHANGELOG.md

* getOrAssign -> getOrCreateId

* dont delete graph controls when disconnected

* early work cleaning up HTML+CSS structures

* cleanup server css elements

* cleanup graph control css elements

* move base CSS color values into @media(prefers-color-scheme: light)

* move CSS magic colors to vars

* reduce duplicated CSS color rules

* inline body text color CSS

* WIP replacing jQuery calls with vanilla JS

* WIP replacing jQuery calls with vanilla JS

* replace getElementsByClass with querySelectorAll

* typeMarker -> serverTypeHTML

* use jQuery slim for remaining flot.js dependency

* merge setAllGraphVisibility into GraphDisplayManager

* break apart element update and redraw logic

* add eslint + parcel bundler

* auto lint assets/js when building

* statically serve favicons/ for faviconOverrides outside of dist/

* only send favicons when changed

* move faviconOverride behavior into entry in servers.json

* add warning to backend server files

* remove .server-favicon-missing class

* add Minetrack 5 migration guide

* add npm run build step to install.sh

* adjust package.json version to 5.0.0

* remove js references from index.html

* move logic and behavior out of site.js

* cleanup ServerRegistry methods

* prevent multiple history graph redraws

* add comments

* cleanup #addServer usage, move to App

* move graph control bindings into GraphDisplayManager

* site.js -> main.js, core.js -> servers.js

* move Tooltip/Caption into util.js

* spacing tweak

* format index.html

* ensure the frontend does not handling updateHistoryGraph events

* prevent versions/record updates if the same value

* avoid empty percbar updates, ensure versions are sorted

* only include main.js ref in index.html

* serve minified copy of font awesome directly

* bundle icons.css into main.css, remove Open Sans 400

* add new SVG logo

* update docs/CHANGELOG.md

* new design, server version grouping

* remove start.sh call from install.sh

* move graph controls into header with new button

* move #handleSettingsToggle back to graph

* fix legacy code behavior of currentVersionIndex applying globally

* fix header text color in light mode

* fix mojang status text color in light mode

* fix toggle settings and checkbox colors

* tweak button hover color

* tweak button hover color

* add new status-overlay to avoid complicated DOM management during loading

* fix initial graph rendering bug

* add comments

* update default graph tick sizes

* prevent #tooltip from overflowing page

* remove localhost spec

* prevent minor connection errors from reshuffling layout

* update CHANGELOG.md

* add message/button for manually loading historical graph on mobile devices

* send isGraphVisible to frontend to prevent alert if logToDatabase: false

* send timestamp data with record

* update docs/CHANGELOG.md

* remove clock icon

* remove 24h peak timestamp

* Only check favicon if present

* safely handle undefined/empty knownVersions in #formatMinecraftVersions

* merge config.versions and minecraft.json into minecraft_versions.json, simplify index matching behavior

* remove localhost url in socket.io config

* stub methods/linkage for FocusManager

* add #isObjectEqual hack, add event proxying to FocusManager

* wip extended stats box

* remove server-type badging

* tweak mojang unstable color

* serve socket.io-client using parcel

* fix incorrect mojang status colors

* remove legacy capitalization design

* redesign focus boxes

* update docs/CHANGELOG.md

* remove localhost ref

* color clock icon

* use background-color for hover effect, remove unused var

* improve stats focus box icons

* change mojang sessions icon to globe

* Add favorites system

* remove focus boxes

* update docs/CHANGELOG.md

* remove focus icons from font

* simplify graph related event binding

* Add Sort By button

* store current sortOption in localStorage

* update docs/CHANGELOG.md

* move magic 0 sortOption to SORT_OPTION_INDEX_DEFAULT

* remove localhost ref

* merge #settings-toggle, #sort-by and .mojang-status CSS

* remove .focus-box CSS

* use sortedServerIds for _lastSortedServers

* tweak --color-blue

* new missing_favicon design to match logo

* edit footer CSS/text, remove github icon

* replace player count diff counter with GROWTH sort option

* italize non-default sort options

* add Only Favorites button to auto sync favorites to the visible graph data

* add icons to graph control buttons

* update docs/CHANGELOG.md

* use * to denote non-default sort option instead

* remove localhost url in socket.io config

* add value highlighting to make sort by easier to read

* remove last remaining uppercase text

* remove serverTypesVisible from config.json

* simplify header CSS, fix spacing with logToDatabase=false

* fix inverted text color on highlighted values

* remove localhost url in socket.io config

* break header into rows on mobile devices

Co-authored-by: Hugo Manrique <contact@hugmanrique.me>
This commit is contained in:
Nick Krecklow
2020-04-19 19:27:59 -05:00
committed by GitHub
parent 8c5e25b259
commit f875361bc7
41 changed files with 10304 additions and 1330 deletions

59
assets/css/icons.css Normal file
View File

@ -0,0 +1,59 @@
@font-face {
font-family: 'icomoon';
src:
url('../fonts/icomoon.ttf?gn52nv') format('truetype'),
url('../fonts/icomoon.woff?gn52nv') format('woff'),
url('../fonts/icomoon.svg?gn52nv#icomoon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
}
[class^="icon-"], [class*=" icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icomoon' !important;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-star:before {
content: "\f005";
}
.icon-star-o:before {
content: "\f006";
}
.icon-lock:before {
content: "\f023";
}
.icon-eye:before {
content: "\f06e";
}
.icon-eye-slash:before {
content: "\f070";
}
.icon-cogs:before {
content: "\f085";
}
.icon-gears:before {
content: "\f085";
}
.icon-globe:before {
content: "\f0ac";
}
.icon-code:before {
content: "\f121";
}
.icon-sort-amount-desc:before {
content: "\f161";
}
.icon-street-view:before {
content: "\f21d";
}

View File

@ -1,16 +1,62 @@
@import url(https://fonts.googleapis.com/css?family=Open+Sans:700,300,400);
@import url(https://fonts.googleapis.com/css?family=Open+Sans:700,300);
@import url(../css/icons.css);
* {
margin: 0;
padding: 0;
}
body {
:root {
--color-dark-gray: #A3A3A3;
--color-gold: #FFD700;
--color-dark-purple: #6c5ce7;
--color-light-purple: #a29bfe;
--color-dark-blue: #0984e3;
--color-light-blue: #74b9ff;
--theme-color-dark: #3B3738;
--theme-color-light: #EBEBEB;
--border-radius: 1px;
}
@media (prefers-color-scheme: light) {
:root {
--color-purple: var(--color-light-purple);
--color-blue: var(--color-light-blue);
--background-color: var(--theme-color-light);
--text-decoration-color: var(--theme-color-dark);
--text-color: #000;
--text-color-inverted: #FFF;
}
body {
background: #212021;
color: #FFF;
font-family: "Open Sans", sans-serif;
font-size: 18px;
font-weight: 300 !important;
}
}
@media (prefers-color-scheme: dark) {
:root {
--color-purple: var(--color-dark-purple);
--color-blue: var(--color-dark-blue);
--background-color: var(--theme-color-dark);
--text-decoration-color: var(--theme-color-light);
--text-color: #FFF;
--text-color-inverted: #000;
}
body {
background: #1c1b1c;
color: #FFF;
}
}
body {
font-family: "Open Sans", sans-serif;
font-size: 18px;
font-weight: 300;
min-width: 800px;
}
/* Page layout */
@ -19,195 +65,287 @@ html, body {
}
a {
cursor: pointer;
cursor: pointer;
color: inherit;
text-decoration: none;
}
#push {
position: relative;
min-height: 100%;
display: none;
position: relative;
min-height: 100%;
}
strong {
font-weight: 700;
}
/* Logo */
.logo-text {
letter-spacing: -3px;
}
/* Header */
#header {
background: #EBEBEB;
color: #3B3738;
overflow: auto;
header {
overflow: auto;
padding: 20px;
}
#header-wrapper {
overflow: auto;
min-width: 850px;
header .column-left {
float: left;
}
#header .column {
display: inline-block;
float: left;
header .column-right {
float: right;
}
#header .column h1 {
margin: -6px 0;
header .logo-image {
--fixed-logo-image-size: 36px;
width: var(--fixed-logo-image-size);
height: var(--fixed-logo-image-size);
margin-right: 5px;
display: inline-block;
}
#header .slogan {
font-size: 20px;
text-align: left;
header .logo-text {
font-size: 48px;
margin: -6px 0;
display: inline-block;
}
#header .subslogan {
font-size: 19px;
header .logo-status {
font-size: 21px;
}
#header a, #footer a {
text-decoration: none;
color: inherit;
border-bottom: 1px dashed #3B3738;
header a {
border-bottom: 1px dashed var(--text-decoration-color);
}
#header a:hover, #footer a:hover {
border-bottom: 1px dashed transparent;
header a:hover {
border-bottom: 1px dashed transparent;
}
#header > h1 {
font-size: 42px;
header .header-button {
color: var(--text-color);
width: 83px;
height: 83px;
text-align: center;
line-height: 20px;
font-size: 14px;
margin-left: -5px;
}
#header > #column-center {
width: 1480px;
margin: 0 auto;
text-align: center;
header .header-button > span:first-of-type {
display: block;
margin-top: 10px;
font-size: 22px;
}
header .header-button-group {
display: inline-block;
}
header .header-button-group:first-of-type {
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
}
header .header-button-group:last-of-type {
border-top-right-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
header .header-button-single {
display: none;
cursor: pointer;
border-radius: var(--border-radius);
margin-right: 20px;
}
header .header-button-single:hover {
background: var(--background-color) !important;
}
/* Footer */
#footer {
font-size: 16px;
text-transform: uppercase;
background: #EBEBEB;
color: #3B3738;
padding: 15px 0;
min-width: 950px;
margin-top: 15px;
footer {
display: none;
background: var(--background-color);
color: var(--text-color);
padding: 10px 0 15px 0;
text-align: center;
margin-top: 15px;
}
#footer a {
font-weight: 700;
border-bottom: none !important;
footer a {
border-bottom: 1px dashed var(--text-decoration-color);
}
#footer a:hover {
border-bottom: 1px dashed #000 !important;
footer a:hover {
border-bottom: 1px dashed transparent;
}
/* Tagline */
#tagline-text {
padding-top: 20px;
text-align: center;
/* Status overly */
#status-overlay {
padding: 20px 0;
background: var(--background-color);
color: var(--text-color);
border-radius: var(--border-radius);
text-align: center;
height: 150px;
width: 350px;
position: fixed;
top: 50%;
left: 50%;
margin-top: -95px;
margin-left: -175px;
}
#status-overlay .logo-image {
--fixed-logo-image-size: 72px;
width: var(--fixed-logo-image-size);
height: var(--fixed-logo-image-size);
}
/* Floating tooltip */
#tooltip {
display: none;
position: absolute;
padding: 5px 8px;
border-radius: var(--border-radius);
background: var(--background-color);
color: var(--text-color);
z-index: 10000;
}
/* Server listing */
.server-container {
overflow: auto;
display: flex;
flex-wrap: wrap;
justify-content: center;
#server-list {
overflow: auto;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.server {
padding: 5px 10px;
margin: 0 5px;
width: 800px;
border: 1px solid transparent;
display: inline-block;
padding: 5px 10px;
margin: 0 5px;
width: 800px;
display: inline-block;
}
.version {
font-size: 12px;
.server .column {
float: left;
}
/*.server:hover {
background: #282828;
border: 1px solid #444;
cursor: pointer;
border-radius: 2px;
}*/
.server > .column > img {
border-radius: 2px;
margin-top: 5px;
.server .column-favicon {
width: 80px;
}
.server > .column {
float: left;
display: inline-block;
.server .column-favicon .server-favicon {
--fixed-server-favicon-size: 64px;
width: var(--fixed-server-favicon-size);
height: var(--fixed-server-favicon-size);
border-radius: var(--border-radius);
margin-top: 5px;
}
.server > .column > .rank {
width: 64px;
padding-top: 4px;
.server .column-favicon .server-rank {
display: block;
width: 64px;
text-align: center;
}
.server > .column > h3 > .type {
padding: 1px 5px;
border-radius: 2px;
border: 1px solid #A09E9E;
font-size: 14px;
margin-bottom: 2px;
.server .column-status {
width: 282px;
}
.server-meta {
font-size: 16px !important;
.server .column-status .server-name {
display: inline-block;
}
/* Charts */
.chart {
height: 100px;
width: 400px;
margin-right: -3px;
margin-bottom: 5px;
.server .column-status .server-is-favorite {
cursor: pointer;
color: var(--color-gold);
}
#tooltip {
display: none;
position: absolute;
padding: 5px;
border-radius: 3px;
background: rgba(0, 0, 0, 0.65);
z-index: 10000;
.server .column-status .server-is-favorite:hover::before {
content: "\f006";
}
/* Existing elements */
h3 {
text-transform: uppercase;
.server .column-status .server-is-not-favorite {
cursor: pointer;
color: var(--background-color);
}
/* Basic classes used randomly */
.color-gray {
color: #C4C4C4;
.server .column-status .server-is-not-favorite:hover {
color: var(--color-gold);
}
.color-dark-gray {
color: #A3A3A3;
.server .column-status .server-error {
display: none;
color: #e74c3c;
}
.color-red {
color: #e74c3c;
.server .column-status .server-label {
color: var(--color-dark-gray);
font-size: 16px;
display: none;
}
.text-uppercase {
text-transform: uppercase;
.server .column-status .server-value {
color: var(--color-dark-gray);
font-size: 16px;
}
.text-center-align {
text-align: center;
.server .column-graph {
float: right;
height: 100px;
width: 400px;
margin-right: -3px;
margin-bottom: 5px;
}
/* Highlighted values */
.server-highlighted-label {
font-size: 18px;
}
.server-highlighted-value {
font-size: 18px;
font-weight: 700;
}
/* Global stats */
.global-stat {
font-weight: 700;
}
/* Sort by */
#sort-by {
background: var(--color-purple);
}
/* Settings toggle */
#settings-toggle {
background: var(--color-blue);
}
/* Historical graph */
#big-graph-mobile-load-request {
background: var(--background-color);
color: var(--text-color);
padding: 10px 0;
text-align: center;
display: none;
width: 100%;
margin-bottom: 10px;
}
#big-graph-mobile-load-request a {
display: inline-block;
}
/* The big graph */
#big-graph, #big-graph-controls, #big-graph-checkboxes {
width: 90%;
margin: 15px auto 0 auto;
}
#big-graph-checkboxes > table {
@ -215,119 +353,105 @@ h3 {
}
#big-graph {
margin-top: 20px;
margin: 0 auto;
}
#big-graph-controls {
margin: 10px auto;
margin: 10px auto;
display: none;
}
#big-graph-controls a {
text-decoration: none;
color: inherit;
text-transform: uppercase;
border-bottom: 1px dashed #FFF;
font-size: 16px;
#big-graph-controls .icon-star {
color: var(--color-gold);
}
#big-graph-controls a:hover {
border-bottom: 1px dashed transparent;
#big-graph-controls-drawer {
background: var(--background-color);
color: var(--text-color);
border-radius: var(--border-radius);
padding-bottom: 10px;
overflow: auto;
display: none;
}
#big-graph-controls-drawer .graph-controls-setall {
text-align: center;
display: block;
margin: 15px 0;
}
#big-graph-checkboxes {
margin: 15px auto 0 auto;
}
/* Basic elements */
.button {
background: #3498db;
border-radius: 2px;
text-shadow: 0 0 0 #000;
width: 85px;
font-size: 16px;
padding: 5px 10px;
margin: 0 auto;
background: var(--color-blue);
border-radius: var(--border-radius);
font-size: 16px;
padding: 5px 10px;
}
.button:hover {
background: #ecf0f1;
color: #3498db;
cursor: pointer;
background: var(--text-color);
color: var(--text-color-inverted);
}
/* Percentage bar */
#perc-bar {
height: 35px;
position: relative;
height: 35px;
position: relative;
overflow-x: hidden;
}
#perc-bar > .perc-bar-part {
height: 100%;
display: inline-block;
position: absolute;
transition: 0.1s all;
}
#perc-bar > .perc-bar-part:hover {
opacity: 0.75;
}
/* Mojang Status */
.mojang-status {
width: 85px;
height: 106px;
display: inline-block;
text-align: center;
line-height: 20px;
font-size: 14px;
}
.mojang-status > strong {
text-transform: uppercase;
}
.mojang-status > i {
margin-top: 20px;
font-size: 22px;
#perc-bar .perc-bar-part {
height: 100%;
display: inline-block;
position: absolute;
}
/* Mojang status colors */
.mojang-status-online {
background: #87D37C;
@media (prefers-color-scheme: light) {
.mojang-status-online {
background: #87D37C;
}
.mojang-status-unstable {
background: #f1c40f;
}
.mojang-status-offline {
background: #DE5749;
}
}
.mojang-status-unstable {
background: #f1c40f;
}
.mojang-status-offline {
background: #DE5749;
}
/* Dark color scheme overrides */
@media (prefers-color-scheme: dark) {
body {
background: #1c1b1c;
}
.mojang-status-online {
background: #66aa5a;
}
#header, #footer {
background: #3B3738;
color: #FFF;
}
.mojang-status-unstable {
background: #cc8a4f;
}
#footer a, #header a:hover {
border-bottom: 1px dashed transparent !important;
}
.mojang-status-offline {
background: #A6453B;
}
}
#footer a:hover, #header a {
border-bottom: 1px dashed #EBEBEB !important;
}
/* Header rows */
@media only screen and (max-width: 1050px) {
header {
padding: 0 !important;
}
.mojang-status-online {
background: #6b9963;
}
.header-possible-row-break {
padding-top: 20px;
width: 100%;
text-align: center;
}
.mojang-status-unstable {
background: #a87b4b;
}
.mojang-status-offline {
background: #A6453B;
}
}
.header-possible-row-break:last-of-type {
margin-bottom: 20px;
}
}

20
assets/fonts/icomoon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/fonts/icomoon.ttf Normal file

Binary file not shown.

BIN
assets/fonts/icomoon.woff Normal file

Binary file not shown.

View File

@ -2,98 +2,97 @@
<html>
<head>
<head>
<link rel="stylesheet" type="text/css" href="css/main.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.6.3/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="../css/main.css">
<link rel="icon" type="image/png" href="/images/compass.png">
<link rel="icon" type="image/svg+xml" href="../images/logo.svg">
<title>Minetrack</title>
<meta charset="UTF-8">
</head>
<title>Minetrack</title>
<body>
</head>
<div id="tooltip"></div>
<body>
<div id="push">
<div id="tooltip"></div>
<div id="header">
<div id="status-overlay">
<img class="logo-image" src="../images/logo.svg">
<h1 class="logo-text">Minetrack</h1>
<div id="status-text">Connecting...</div>
</div>
<div id="header-wrapper">
<div id="push">
<div class="column" style="padding: 20px;">
<img src="/images/compass.png" width="28" height="28" style="display: inline-block; margin-right: 5px;">
<h1 style="display: inline-block;" class="text-uppercase">Minetrack</h1>
<p class="subslogan text-uppercase">Watching <span id="stat_totalPlayers">0</span> players on <span id="stat_networks">0</span> networks.</p>
</div>
<div id="mojang-status-list" class="column" style="float: right;">
<div class="mojang-status" title="Sessions" id="mojang-status_Sessions"><i class="fa fa-gamepad"></i><br /><strong>Sessions</strong><p class="mojang-status-text" id="mojang-status-text_Sessions">...</p></div><div class="mojang-status" title="Skins" id="mojang-status_Skins"><i class="fa fa-user"></i><br /><strong>Skins</strong><p class="mojang-status-text" id="mojang-status-text_Skins">...</p></div><div class="mojang-status" title="Auth" id="mojang-status_Auth"><i class="fa fa-key"></i><br /><strong>Auth</strong><p class="mojang-status-text" id="mojang-status-text_Auth">...</p></div><div class="mojang-status" title="API" id="mojang-status_API"><i class="fa fa-wrench"></i><br /><strong>API</strong><p class="mojang-status-text" id="mojang-status-text_API">...</p></div>
</div>
</div>
<div id="perc-bar"></div>
<header>
<div class="header-possible-row-break column-left">
<img class="logo-image" src="../images/logo.svg">
<h1 class="logo-text">Minetrack</h1>
<p class="logo-status">Counting <span class="global-stat" id="stat_totalPlayers">0</span> players on <span class="global-stat" id="stat_networks">0</span> Minecraft servers.</p>
</div>
<div id="perc-bar"></div>
<div id="tagline-text">Connecting...</div>
<div id="big-graph"></div>
<div id="big-graph-controls" style="display: none;">
<span style="text-align: center; display: block;">
<a onclick="toggleControlsDrawer();">Click to toggle graph controls</a>
</span>
<div id="big-graph-controls-drawer" style="display: none;">
<div id="big-graph-checkboxes"></div>
<br />
<span style="text-align: center; display: block; margin-bottom: 15px;">
<span onclick="setAllGraphVisibility(true);" class="button">Show All</span>
<span onclick="setAllGraphVisibility(false);" class="button">Hide All</span>
</span>
<div class="header-possible-row-break column-right">
<div id="sort-by" class="header-button header-button-single"><span class="icon-sort-amount-desc"></span> Sort By<br><strong id="sort-by-text">...</strong></div>
<div id="settings-toggle" class="header-button header-button-single"><span class="icon-gears"></span> Graph Controls</div>
<div class="header-button header-button-group" title="Sessions" id="mojang-status_Sessions">
<span class="icon-globe"></span><strong>Sessions</strong>
<p class="mojang-status-text" id="mojang-status-text_Sessions">...</p>
</div>
<div class="header-button header-button-group" title="Skins" id="mojang-status_Skins">
<span class="icon-street-view"></span><strong>Skins</strong>
<p class="mojang-status-text" id="mojang-status-text_Skins">...</p>
</div>
<div class="header-button header-button-group" title="Auth" id="mojang-status_Auth">
<span class="icon-lock"></span><strong>Auth</strong>
<p class="mojang-status-text" id="mojang-status-text_Auth">...</p>
</div>
<div class="header-button header-button-group" title="API" id="mojang-status_API">
<span class="icon-code"></span><strong>API</strong>
<p class="mojang-status-text" id="mojang-status-text_API">...</p>
</div>
</div>
</header>
<div id="server-container-list" class="server-container"></div>
</div>
<div id="footer">
<span style="padding-left: 20px;">Website powered by <a href="https://github.com/Cryptkeeper/Minetrack"><i class="fa fa-github"></i> Minetrack</a>.</span>
<div id="big-graph-mobile-load-request">
<strong>On a mobile device?</strong>
<p>Minetrack has skipped automatically loading the historical graph to help save data and power.</p>
<br>
<a id="big-graph-mobile-load-request-button" class="button">Load Historical Graph</a>
</div>
<!-- External JS assets -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/flot/0.8.3/jquery.flot.min.js"></script>
<div id="big-graph"></div>
<!-- Internal JS assets -->
<script src="js/util.js"></script>
<script src="js/graph.js"></script>
<script src="js/site.js"></script>
<div id="big-graph-controls">
<div id="big-graph-controls-drawer">
<div id="big-graph-checkboxes"></div>
<script src="publicConfig.json"></script>
<span class="graph-controls-setall">
<a minetrack-show-type="all" class="button graph-controls-show"><span class="icon-eye"></span> Show All</a>
<a minetrack-show-type="none" class="button graph-controls-show"><span class="icon-eye-slash"></span> Hide All</a>
<a minetrack-show-type="favorites" class="button graph-controls-show"><span class="icon-star"></span> Only Favorites</a>
</span>
</div>
</div>
</body>
<div id="server-list"></div>
</html>
</div>
<footer id="footer">
<span class="icon-code"></span> Powered by open source software - <a href="https://github.com/Cryptkeeper/Minetrack">make it your own!</a>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/flot/0.8.3/jquery.flot.min.js"></script>
<script src="../js/main.js" defer></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

8
assets/images/logo.svg Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 500 500" width="500" height="500" xmlns="http://www.w3.org/2000/svg">
<circle style="fill: rgb(85, 218, 149);" cx="250" cy="250" r="250"/>
<path d="M 0 250 C 0 111.929 111.929 0 250 0 C 369.688 0 469.731 84.108 494.25 196.443 L 300.624 389.846 L 200.592 290.249 L 68.654 422.087 C 26.106 377.264 0 316.681 0 250 Z" style="fill: rgb(178, 255, 161);"/>
<path d="M 191.114 130.467 L 217.723 185.377 L 164.504 185.377 L 191.114 130.467 Z" style="fill: rgb(41, 41, 41);" transform="matrix(-1, 0.000057, -0.000057, -1, 391.12207, 407.396851)"/>
<rect x="100" y="81.861" width="200" height="147.724" style="fill: rgb(41, 41, 41);" rx="7.498" ry="7.498"/>
<path d="M 185.523 118.251 L 185.523 200.341 L 163.003 200.341 L 163.003 156.881 C 163.003 151.601 163.116 146.771 163.343 142.391 C 161.956 144.078 160.233 145.838 158.173 147.671 L 148.853 155.361 L 137.343 141.211 L 165.533 118.251 L 185.523 118.251 ZM 263.346 159.401 C 263.346 173.928 260.912 184.571 256.046 191.331 C 251.179 198.084 243.729 201.461 233.696 201.461 C 223.889 201.461 216.496 197.934 211.516 190.881 C 206.536 183.821 204.046 173.328 204.046 159.401 C 204.046 144.808 206.479 134.104 211.346 127.291 C 216.212 120.478 223.662 117.071 233.696 117.071 C 243.469 117.071 250.852 120.618 255.846 127.711 C 260.846 134.804 263.346 145.368 263.346 159.401 Z M 226.116 159.401 C 226.116 168.688 226.696 175.071 227.856 178.551 C 229.016 182.038 230.962 183.781 233.696 183.781 C 236.469 183.781 238.426 181.981 239.566 178.381 C 240.706 174.788 241.276 168.461 241.276 159.401 C 241.276 150.308 240.696 143.938 239.536 140.291 C 238.376 136.638 236.429 134.811 233.696 134.811 C 230.962 134.811 229.016 136.571 227.856 140.091 C 226.696 143.611 226.116 150.048 226.116 159.401 Z" transform="matrix(1, 0, 0, 1, 0, 0)" style="fill: rgb(255, 255, 255); white-space: pre;"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 500 500" width="500" height="500" xmlns="http://www.w3.org/2000/svg">
<circle style="fill: rgb(227, 106, 87);" cx="250" cy="250" r="250"/>
<path d="M 5.7 172.087 C 5.7 310.158 117.629 422.087 255.7 422.087 C 375.388 422.087 475.431 337.979 499.95 225.644 L 306.324 32.241 L 206.292 131.838 L 74.354 0 C 31.806 44.823 5.7 105.406 5.7 172.087 Z" style="fill: rgb(222, 222, 222);" transform="matrix(-1, 0, 0, -1, 505.700409, 422.087006)"/>
<path d="M 191.114 130.467 L 217.723 185.377 L 164.504 185.377 L 191.114 130.467 Z" style="fill: rgb(41, 41, 41);" transform="matrix(-1, 0.000057, -0.000057, -1, 491.12207, 407.396851)"/>
<rect x="200" y="81.861" width="200" height="147.724" style="fill: rgb(41, 41, 41);" rx="7.498" ry="7.498"/>
<path d="M 304.775 171.28 L 287.535 171.28 L 287.535 166.45 C 287.535 162.777 288.285 159.677 289.785 157.15 C 291.285 154.623 293.942 152.163 297.755 149.77 C 300.788 147.863 302.972 146.14 304.305 144.6 C 305.632 143.067 306.295 141.307 306.295 139.32 C 306.295 137.753 305.585 136.51 304.165 135.59 C 302.738 134.67 300.885 134.21 298.605 134.21 C 292.952 134.21 286.345 136.213 278.785 140.22 L 270.975 124.95 C 280.295 119.637 290.028 116.98 300.175 116.98 C 308.522 116.98 315.072 118.813 319.825 122.48 C 324.578 126.147 326.955 131.143 326.955 137.47 C 326.955 142.003 325.898 145.927 323.785 149.24 C 321.672 152.553 318.295 155.687 313.655 158.64 C 309.722 161.187 307.258 163.05 306.265 164.23 C 305.272 165.41 304.775 166.803 304.775 168.41 L 304.775 171.28 Z M 285.125 190.93 C 285.125 187.483 286.125 184.827 288.125 182.96 C 290.132 181.087 293.082 180.15 296.975 180.15 C 300.715 180.15 303.588 181.093 305.595 182.98 C 307.595 184.873 308.595 187.523 308.595 190.93 C 308.595 194.337 307.558 196.977 305.485 198.85 C 303.405 200.717 300.568 201.65 296.975 201.65 C 293.268 201.65 290.365 200.723 288.265 198.87 C 286.172 197.017 285.125 194.37 285.125 190.93 Z" transform="matrix(1, 0, 0, 1, 0, 0)" style="fill: rgb(255, 255, 255); white-space: pre;"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

140
assets/js/app.js Normal file
View File

@ -0,0 +1,140 @@
import { ServerRegistry } from './servers'
import { SortController } from './sort'
import { GraphDisplayManager } from './graph'
import { MojangUpdater } from './mojang'
import { PercentageBar } from './percbar'
import { FavoritesManager } from './favorites'
import { Tooltip, Caption, formatNumber } from './util'
export class App {
publicConfig
constructor () {
this.tooltip = new Tooltip()
this.caption = new Caption()
this.serverRegistry = new ServerRegistry(this)
this.sortController = new SortController(this)
this.graphDisplayManager = new GraphDisplayManager(this)
this.mojangUpdater = new MojangUpdater()
this.percentageBar = new PercentageBar(this)
this.favoritesManager = new FavoritesManager(this)
this._taskIds = []
}
setPageReady (isReady) {
document.getElementById('push').style.display = isReady ? 'block' : 'none'
document.getElementById('footer').style.display = isReady ? 'block' : 'none'
document.getElementById('status-overlay').style.display = isReady ? 'none' : 'block'
}
setPublicConfig (publicConfig) {
this.publicConfig = publicConfig
this.serverRegistry.assignServers(publicConfig.servers)
// Start repeating frontend tasks once it has received enough data to be considered active
// This simplifies management logic at the cost of each task needing to safely handle empty data
this.initTasks()
}
handleSyncComplete () {
this.caption.hide()
// Load favorites since all servers are registered
this.favoritesManager.loadLocalStorage()
// Run a single bulk server sort instead of per-add event since there may be multiple
this.sortController.show()
this.percentageBar.redraw()
}
initTasks () {
this._taskIds.push(setInterval(this.sortController.sortServers, 5000))
this._taskIds.push(setInterval(this.updateGlobalStats, 1000))
this._taskIds.push(setInterval(this.percentageBar.redraw, 1000))
}
handleDisconnect () {
this.tooltip.hide()
// Reset individual tracker elements to flush any held data
this.serverRegistry.reset()
this.sortController.reset()
this.graphDisplayManager.reset()
this.mojangUpdater.reset()
this.percentageBar.reset()
// Undefine publicConfig, resynced during the connection handshake
this.publicConfig = undefined
// Clear all task ids, if any
this._taskIds.forEach(clearInterval)
this._taskIds = []
// Reset hidden values created by #updateGlobalStats
this._lastTotalPlayerCount = undefined
this._lastServerRegistrationCount = undefined
// Reset modified DOM structures
document.getElementById('stat_totalPlayers').innerText = 0
document.getElementById('stat_networks').innerText = 0
// Modify page state to display loading overlay
this.caption.set('Lost connection!')
this.setPageReady(false)
}
getTotalPlayerCount () {
return this.serverRegistry.getServerRegistrations()
.map(serverRegistration => serverRegistration.playerCount)
.reduce((sum, current) => sum + current, 0)
}
addServer = (pings) => {
// Even if the backend has never pinged the server, the frontend is promised a placeholder object.
// result = undefined
// error = defined with "Waiting" description
// info = safely defined with configured data
const latestPing = pings[pings.length - 1]
const serverRegistration = this.serverRegistry.createServerRegistration(latestPing.info.name)
serverRegistration.initServerStatus(latestPing)
// Push the historical data into the graph
// This will trim and format the data so it is ready for the graph to render once init
serverRegistration.addGraphPoints(pings)
// Create the plot instance internally with the restructured and cleaned data
serverRegistration.buildPlotInstance()
// Handle the last known state (if any) as an incoming update
// This triggers the main update pipeline and enables centralized update handling
serverRegistration.updateServerStatus(latestPing, true, this.publicConfig.minecraftVersions)
// Allow the ServerRegistration to bind any DOM events with app instance context
serverRegistration.initEventListeners()
}
updateGlobalStats = () => {
// Only redraw when needed
// These operations are relatively cheap, but the site already does too much rendering
const totalPlayerCount = this.getTotalPlayerCount()
if (totalPlayerCount !== this._lastTotalPlayerCount) {
this._lastTotalPlayerCount = totalPlayerCount
document.getElementById('stat_totalPlayers').innerText = formatNumber(totalPlayerCount)
}
// Only redraw when needed
// These operations are relatively cheap, but the site already does too much rendering
const serverRegistrationCount = this.serverRegistry.getServerRegistrations().length
if (serverRegistrationCount !== this._lastServerRegistrationCount) {
this._lastServerRegistrationCount = serverRegistrationCount
document.getElementById('stat_networks').innerText = serverRegistrationCount
}
}
}

69
assets/js/favorites.js Normal file
View File

@ -0,0 +1,69 @@
export const FAVORITE_SERVERS_STORAGE_KEY = 'minetrack_favorite_servers'
export class FavoritesManager {
constructor (app) {
this._app = app
}
loadLocalStorage () {
if (typeof localStorage !== 'undefined') {
let serverNames = localStorage.getItem(FAVORITE_SERVERS_STORAGE_KEY)
if (serverNames) {
serverNames = JSON.parse(serverNames)
for (let i = 0; i < serverNames.length; i++) {
const serverRegistration = this._app.serverRegistry.getServerRegistration(serverNames[i])
// The serverName may not exist in the backend configuration anymore
// Ensure serverRegistration is defined before mutating data or considering valid
if (serverRegistration) {
serverRegistration.isFavorite = true
// Update icon since by default it is unfavorited
document.getElementById('favorite-toggle_' + serverRegistration.serverId).setAttribute('class', this.getIconClass(serverRegistration.isFavorite))
}
}
}
}
}
updateLocalStorage () {
if (typeof localStorage !== 'undefined') {
// Mutate the serverIds array into server names for storage use
const serverNames = this._app.serverRegistry.getServerRegistrations()
.filter(serverRegistration => serverRegistration.isFavorite)
.map(serverRegistration => serverRegistration.data.name)
if (serverNames.length > 0) {
// Only save if the array contains data, otherwise clear the item
localStorage.setItem(FAVORITE_SERVERS_STORAGE_KEY, JSON.stringify(serverNames))
} else {
localStorage.removeItem(FAVORITE_SERVERS_STORAGE_KEY)
}
}
}
handleFavoriteButtonClick = (serverRegistration) => {
serverRegistration.isFavorite = !serverRegistration.isFavorite
// Update the displayed favorite icon
document.getElementById('favorite-toggle_' + serverRegistration.serverId).setAttribute('class', this.getIconClass(serverRegistration.isFavorite))
// Request the app controller instantly re-sort the server listing
// This handles the favorite sorting logic internally
this._app.sortController.sortServers()
this._app.graphDisplayManager.handleServerIsFavoriteUpdate(serverRegistration)
// Write an updated settings payload
this.updateLocalStorage()
}
getIconClass (isFavorite) {
if (isFavorite) {
return 'icon-star server-is-favorite'
} else {
return 'icon-star-o server-is-not-favorite'
}
}
}

View File

@ -1,130 +1,370 @@
// Used by the individual server entries
var smallChartOptions = {
series: {
shadowSize: 0
},
xaxis: {
font: {
color: "#E3E3E3"
},
show: false
},
yaxis: {
minTickSize: 75,
tickDecimals: 0,
show: true,
tickLength: 10,
tickFormatter: function(value) {
return formatNumber(value);
},
font: {
color: "#E3E3E3"
},
labelWidth: -10
},
grid: {
hoverable: true,
color: "#696969"
},
colors: [
"#E9E581"
]
};
import { formatNumber, formatTimestamp, isMobileBrowser } from './util'
// Used by the one chart to rule them all
var bigChartOptions = {
series: {
shadowSize: 0
import { FAVORITE_SERVERS_STORAGE_KEY } from './favorites'
export const HISTORY_GRAPH_OPTIONS = {
series: {
shadowSize: 0
},
xaxis: {
font: {
color: '#E3E3E3'
},
xaxis: {
font: {
color: "#E3E3E3"
},
show: false
show: false
},
yaxis: {
show: true,
tickSize: 5000,
tickLength: 10,
tickFormatter: formatNumber,
font: {
color: '#E3E3E3'
},
yaxis: {
show: true,
tickSize: 2000,
tickLength: 10,
tickFormatter: function(value) {
return formatNumber(value);
},
font: {
color: "#E3E3E3"
},
labelWidth: -5,
min: 0
},
grid: {
hoverable: true,
color: "#696969"
},
legend: {
show: false
labelWidth: -5,
min: 0
},
grid: {
hoverable: true,
color: '#696969'
},
legend: {
show: false
}
}
const HIDDEN_SERVERS_STORAGE_KEY = 'minetrack_hidden_servers'
const SHOW_FAVORITES_STORAGE_KEY = 'minetrack_show_favorites'
export class GraphDisplayManager {
// Only emit graph data request if not on mobile due to graph data size
isVisible = !isMobileBrowser()
constructor (app) {
this._app = app
this._graphData = []
this._hasLoadedSettings = false
this._initEventListenersOnce = false
this._showOnlyFavorites = false
}
addGraphPoint (serverId, timestamp, playerCount) {
if (!this._hasLoadedSettings) {
// _hasLoadedSettings is controlled by #setGraphData
// It will only be true once the context has been loaded and initial payload received
// #addGraphPoint should not be called prior to that since it means the data is racing
// and the application has received updates prior to the initial state
return
}
};
function toggleControlsDrawer() {
var div = $('#big-graph-controls-drawer');
// Trim any outdated entries by filtering the array into a new array
const startTimestamp = new Date().getTime()
const newGraphData = this._graphData[serverId].filter(point => startTimestamp - point[0] <= this._app.publicConfig.graphDuration)
div.css('display', div.css('display') !== 'none' ? 'none' : 'block');
}
// Push the new data from the method call request
newGraphData.push([timestamp, playerCount])
function saveGraphControls(displayedServers) {
if (typeof(localStorage)) {
var json = JSON.stringify(displayedServers);
this._graphData[serverId] = newGraphData
}
localStorage.setItem('displayedServers', json);
}
}
loadLocalStorage () {
if (typeof localStorage !== 'undefined') {
const showOnlyFavorites = localStorage.getItem(SHOW_FAVORITES_STORAGE_KEY)
if (showOnlyFavorites) {
this._showOnlyFavorites = true
}
function loadGraphControls() {
if (typeof(localStorage)) {
var item = localStorage.getItem('displayedServers');
// If only favorites mode is active, use the stored favorite servers data instead
let serverNames
if (this._showOnlyFavorites) {
serverNames = localStorage.getItem(FAVORITE_SERVERS_STORAGE_KEY)
} else {
serverNames = localStorage.getItem(HIDDEN_SERVERS_STORAGE_KEY)
}
if (item) {
return JSON.parse(item);
}
}
}
if (serverNames) {
serverNames = JSON.parse(serverNames)
function resetGraphControls() {
if (typeof(localStorage)) {
localStorage.removeItem('displayedServers');
}
}
// Called by flot.js when they hover over a data point.
function handlePlotHover(event, pos, item) {
if (item) {
var text = getTimestamp(item.datapoint[0] / 1000) + '\
<br />\
' + formatNumber(item.datapoint[1]) + ' Players';
if (item.series && item.series.label) {
text = item.series.label + '<br />' + text;
// Iterate over all active serverRegistrations
// This merges saved state with current state to prevent desyncs
for (const serverRegistration of this._app.serverRegistry.getServerRegistrations()) {
// isVisible will be true if showOnlyFavorites && contained in FAVORITE_SERVERS_STORAGE_KEY
// OR, if it is NOT contains within HIDDEN_SERVERS_STORAGE_KEY
// Checks between FAVORITE/HIDDEN keys are mutually exclusive
if (this._showOnlyFavorites) {
serverRegistration.isVisible = serverNames.indexOf(serverRegistration.data.name) >= 0
} else {
serverRegistration.isVisible = serverNames.indexOf(serverRegistration.data.name) < 0
}
}
}
}
}
renderTooltip(item.pageX + 5, item.pageY + 5, text);
updateLocalStorage () {
if (typeof localStorage !== 'undefined') {
// Mutate the serverIds array into server names for storage use
const serverNames = this._app.serverRegistry.getServerRegistrations()
.filter(serverRegistration => !serverRegistration.isVisible)
.map(serverRegistration => serverRegistration.data.name)
// Only store if the array contains data, otherwise clear the item
// If showOnlyFavorites is true, do NOT store serverNames since the state will be auto managed instead
if (serverNames.length > 0 && !this._showOnlyFavorites) {
localStorage.setItem(HIDDEN_SERVERS_STORAGE_KEY, JSON.stringify(serverNames))
} else {
localStorage.removeItem(HIDDEN_SERVERS_STORAGE_KEY)
}
// Only store SHOW_FAVORITES_STORAGE_KEY if true
if (this._showOnlyFavorites) {
localStorage.setItem(SHOW_FAVORITES_STORAGE_KEY, true)
} else {
localStorage.removeItem(SHOW_FAVORITES_STORAGE_KEY)
}
}
}
// Converts the backend data into the schema used by flot.js
getVisibleGraphData () {
return Object.keys(this._graphData)
.map(Number)
.map(serverId => this._app.serverRegistry.getServerRegistration(serverId))
.filter(serverRegistration => serverRegistration !== undefined && serverRegistration.isVisible)
.map(serverRegistration => {
return {
data: this._graphData[serverRegistration.serverId],
yaxis: 1,
label: serverRegistration.data.name,
color: serverRegistration.data.color
}
})
}
buildPlotInstance (graphData) {
// Lazy load settings from localStorage, if any and if enabled
if (!this._hasLoadedSettings) {
this._hasLoadedSettings = true
this.loadLocalStorage()
}
// Remap the incoming data from being string (serverName) keyed into serverId keys
for (const serverName of Object.keys(graphData)) {
const serverRegistration = this._app.serverRegistry.getServerRegistration(serverName)
this._graphData[serverRegistration.serverId] = graphData[serverName]
}
// Explicitly define a height so flot.js can rescale the Y axis
document.getElementById('big-graph').style.height = '400px'
this._plotInstance = $.plot('#big-graph', this.getVisibleGraphData(), HISTORY_GRAPH_OPTIONS)
// Show the settings-toggle element
document.getElementById('settings-toggle').style.display = 'inline-block'
}
// requestRedraw allows usages to request a redraw that may be performed, or cancelled, sometime later
// This allows multiple rapid, but individual updates, to clump into a single redraw instead
requestRedraw () {
if (this._redrawRequestTimeout) {
clearTimeout(this._redrawRequestTimeout)
}
// Schedule new delayed redraw call
// This can be cancelled by #requestRedraw, #redraw and #reset
this._redrawRequestTimeout = setTimeout(this.redraw, 1000)
}
redraw = () => {
// Use drawing as a hint to update settings
// This may cause unnessecary localStorage updates, but its a rare and harmless outcome
this.updateLocalStorage()
// Fire calls to the provided graph instance
// This allows flot.js to manage redrawing and creates a helper method to reduce code duplication
this._plotInstance.setData(this.getVisibleGraphData())
this._plotInstance.setupGrid()
this._plotInstance.draw()
// undefine value so #clearTimeout is not called
// This is safe even if #redraw is manually called since it removes the pending work
if (this._redrawRequestTimeout) {
clearTimeout(this._redrawRequestTimeout)
}
this._redrawRequestTimeout = undefined
}
requestResize () {
// Only resize when _plotInstance is defined
// Set a timeout to resize after resize events have not been fired for some duration of time
// This prevents burning CPU time for multiple, rapid resize events
if (this._plotInstance) {
if (this._resizeRequestTimeout) {
clearTimeout(this._resizeRequestTimeout)
}
// Schedule new delayed resize call
// This can be cancelled by #requestResize, #resize and #reset
this._resizeRequestTimeout = setTimeout(this.resize, 200)
}
}
resize = () => {
if (this._plotInstance) {
this._plotInstance.resize()
this._plotInstance.setupGrid()
this._plotInstance.draw()
}
// undefine value so #clearTimeout is not called
// This is safe even if #resize is manually called since it removes the pending work
if (this._resizeRequestTimeout) {
clearTimeout(this._resizeRequestTimeout)
}
this._resizeRequestTimeout = undefined
}
// Called by flot.js when they hover over a data point.
handlePlotHover = (event, pos, item) => {
if (!item) {
this._app.tooltip.hide()
} else {
hideTooltip();
let text = formatNumber(item.datapoint[1]) + ' Players<br>' + formatTimestamp(item.datapoint[0])
// Prefix text with the series label when possible
if (item.series && item.series.label) {
text = '<strong>' + item.series.label + '</strong><br>' + text
}
this._app.tooltip.set(item.pageX, item.pageY, 10, 10, text)
}
}
initEventListeners () {
if (!this._initEventListenersOnce) {
this._initEventListenersOnce = true
// These listeners should only be init once since they attach to persistent elements
document.getElementById('settings-toggle').addEventListener('click', this.handleSettingsToggle, false)
document.querySelectorAll('.graph-controls-show').forEach((element) => {
element.addEventListener('click', this.handleShowButtonClick, false)
})
}
$('#big-graph').bind('plothover', this.handlePlotHover)
// These listeners should be bound each #initEventListeners call since they are for newly created elements
document.querySelectorAll('.graph-control').forEach((element) => {
element.addEventListener('click', this.handleServerButtonClick, false)
})
}
handleServerButtonClick = (event) => {
const serverId = parseInt(event.target.getAttribute('minetrack-server-id'))
const serverRegistration = this._app.serverRegistry.getServerRegistration(serverId)
if (serverRegistration.isVisible !== event.target.checked) {
serverRegistration.isVisible = event.target.checked
// Any manual changes automatically disables "Only Favorites" mode
// Otherwise the auto management might overwrite their manual changes
this._showOnlyFavorites = false
this.redraw()
}
}
handleShowButtonClick = (event) => {
const showType = event.target.getAttribute('minetrack-show-type')
// If set to "Only Favorites", set internal state so that
// visible graphData is automatically updating when a ServerRegistration's #isVisible changes
// This is also saved and loaded by #loadLocalStorage & #updateLocalStorage
this._showOnlyFavorites = showType === 'favorites'
let redraw = false
this._app.serverRegistry.getServerRegistrations().forEach(function (serverRegistration) {
let isVisible
if (showType === 'all') {
isVisible = true
} else if (showType === 'none') {
isVisible = false
} else if (showType === 'favorites') {
isVisible = serverRegistration.isFavorite
}
if (serverRegistration.isVisible !== isVisible) {
serverRegistration.isVisible = isVisible
redraw = true
}
})
if (redraw) {
this.redraw()
this.updateCheckboxes()
}
}
handleSettingsToggle = () => {
const element = document.getElementById('big-graph-controls-drawer')
if (element.style.display !== 'block') {
element.style.display = 'block'
} else {
element.style.display = 'none'
}
}
handleServerIsFavoriteUpdate = (serverRegistration) => {
// When in "Only Favorites" mode, visibility is dependent on favorite status
// Redraw and update elements as needed
if (this._showOnlyFavorites && serverRegistration.isVisible !== serverRegistration.isFavorite) {
serverRegistration.isVisible = serverRegistration.isFavorite
this.redraw()
this.updateCheckboxes()
}
}
updateCheckboxes () {
document.querySelectorAll('.graph-control').forEach((checkbox) => {
const serverId = parseInt(checkbox.getAttribute('minetrack-server-id'))
const serverRegistration = this._app.serverRegistry.getServerRegistration(serverId)
checkbox.checked = serverRegistration.isVisible
})
}
reset () {
this._graphData = []
this._plotInstance = undefined
this._hasLoadedSettings = false
// Fire #clearTimeout if the timeout is currently defined
if (this._resizeRequestTimeout) {
clearTimeout(this._resizeRequestTimeout)
this._resizeRequestTimeout = undefined
}
if (this._redrawRequestTimeout) {
clearTimeout(this._redrawRequestTimeout)
this._redrawRequestTimeout = undefined
}
// Reset modified DOM structures
document.getElementById('big-graph-checkboxes').innerHTML = ''
document.getElementById('big-graph-controls').style.display = 'none'
document.getElementById('settings-toggle').style.display = 'none'
const graphElement = document.getElementById('big-graph')
graphElement.innerHTML = ''
graphElement.removeAttribute('style')
}
}
// Converts the backend data into the schema used by flot.js
function convertGraphData(rawData) {
var data = [];
var keys = Object.keys(rawData);
for (var i = 0; i < keys.length; i++) {
data.push({
data: rawData[keys[i]],
yaxis: 1,
label: keys[i],
color: getServerColor(keys[i])
});
}
return data;
}

159
assets/js/main.js Normal file
View File

@ -0,0 +1,159 @@
import { App } from './app'
import io from 'socket.io-client'
const app = new App()
document.addEventListener('DOMContentLoaded', function () {
const socket = io.connect({
reconnect: true,
reconnectDelay: 1000,
reconnectionAttempts: 10
})
// The backend will automatically push data once connected
socket.on('connect', function () {
app.caption.set('Loading...')
})
socket.on('disconnect', function () {
app.handleDisconnect()
// Reset modified DOM structures
document.getElementById('big-graph-mobile-load-request').style.display = 'none'
})
socket.on('historyGraph', function (data) {
// Consider the graph visible since a payload has been received
// This is used for the manual graph load request behavior
app.graphDisplayManager.isVisible = true
app.graphDisplayManager.buildPlotInstance(data)
// Build checkbox elements for graph controls
let lastRowCounter = 0
let controlsHTML = ''
Object.keys(data).sort().forEach(function (serverName) {
const serverRegistration = app.serverRegistry.getServerRegistration(serverName)
controlsHTML += '<td>' +
'<input type="checkbox" class="graph-control" minetrack-server-id="' + serverRegistration.serverId + '" ' + (serverRegistration.isVisible ? 'checked' : '') + '>' +
' ' + serverName +
'</input></td>'
// Occasionally break table rows using a magic number
if (++lastRowCounter % 6 === 0) {
controlsHTML += '</tr><tr>'
}
})
// Apply generated HTML and show controls
document.getElementById('big-graph-checkboxes').innerHTML = '<table><tr>' +
controlsHTML +
'</tr></table>'
document.getElementById('big-graph-controls').style.display = 'block'
// Bind click event for updating graph data
app.graphDisplayManager.initEventListeners()
})
socket.on('updateHistoryGraph', function (data) {
// Skip any incoming updates if the graph is disabled
// The backend shouldn't send these anyways
if (!app.graphDisplayManager.isVisible) {
return
}
const serverRegistration = app.serverRegistry.getServerRegistration(data.name)
if (serverRegistration) {
app.graphDisplayManager.addGraphPoint(serverRegistration.serverId, data.timestamp, data.players)
// Only redraw the graph if not mutating hidden data
if (serverRegistration.isVisible) {
app.graphDisplayManager.requestRedraw()
}
}
})
socket.on('add', function (data) {
data.forEach(app.addServer)
})
socket.on('update', function (data) {
// The backend may send "update" events prior to receiving all "add" events
// A server has only been added once it's ServerRegistration is defined
// Checking undefined protects from this race condition
const serverRegistration = app.serverRegistry.getServerRegistration(data.info.name)
if (serverRegistration) {
serverRegistration.updateServerStatus(data, false, app.publicConfig.minecraftVersions)
}
})
socket.on('updateMojangServices', function (data) {
Object.values(data).forEach(app.mojangUpdater.updateServiceStatus)
})
socket.on('setPublicConfig', function (data) {
app.setPublicConfig(data)
// Display the main page component
// Called here instead of syncComplete so the DOM can be drawn prior to the graphs being drawn
// Otherwise flot.js will cause visual alignment bugs
app.setPageReady(true)
// Allow the graphDisplayManager to control whether or not the historical graph is loaded
// Defer to isGraphVisible from the publicConfig to understand if the frontend will ever receive a graph payload
if (data.isGraphVisible) {
if (app.graphDisplayManager.isVisible) {
socket.emit('requestHistoryGraph')
} else {
document.getElementById('big-graph-mobile-load-request').style.display = 'block'
}
}
})
// Fired once the backend has sent all requested data
socket.on('syncComplete', function () {
app.handleSyncComplete()
})
socket.on('updatePeak', function (data) {
const serverRegistration = app.serverRegistry.getServerRegistration(data.name)
if (serverRegistration) {
serverRegistration.updateServerPeak(data.timestamp, data.players)
}
})
socket.on('peaks', function (data) {
Object.keys(data).forEach(function (serverName) {
const serverRegistration = app.serverRegistry.getServerRegistration(serverName)
if (serverRegistration) {
const graphData = data[serverName]
// [0] and [1] indexes correspond to flot.js' graphing data structure
serverRegistration.updateServerPeak(graphData[0], graphData[1])
}
})
})
window.addEventListener('resize', function () {
app.percentageBar.redraw()
// Delegate to GraphDisplayManager which can check if the resize is necessary
app.graphDisplayManager.requestResize()
}, false)
document.getElementById('big-graph-mobile-load-request-button').addEventListener('click', function () {
// Send a graph data request to the backend
socket.emit('requestHistoryGraph')
// Hide the activation link to avoid multiple requests
document.getElementById('big-graph-mobile-load-request').style.display = 'none'
}, false)
}, false)

20
assets/js/mojang.js Normal file
View File

@ -0,0 +1,20 @@
const MOJANG_STATUS_BASE_CLASS = 'header-button header-button-group'
export class MojangUpdater {
updateServiceStatus (status) {
// HACK: ensure mojang-status is added for alignment, replace existing class to swap status color
document.getElementById('mojang-status_' + status.name).setAttribute('class', MOJANG_STATUS_BASE_CLASS + ' mojang-status-' + status.title.toLowerCase())
document.getElementById('mojang-status-text_' + status.name).innerText = status.title
}
reset () {
// Strip any mojang-status-* color classes from all mojang-status classes
document.querySelectorAll('.mojang-status').forEach(function (element) {
element.setAttribute('class', MOJANG_STATUS_BASE_CLASS)
})
document.querySelectorAll('.mojang-status-text').forEach(function (element) {
element.innerText = '...'
})
}
}

72
assets/js/percbar.js Normal file
View File

@ -0,0 +1,72 @@
import { formatNumber, formatPercent } from './util'
export class PercentageBar {
constructor (app) {
this._app = app
this._parent = document.getElementById('perc-bar')
}
redraw = () => {
const serverRegistrations = this._app.serverRegistry.getServerRegistrations().sort(function (a, b) {
return a.playerCount - b.playerCount
})
const totalPlayers = this._app.getTotalPlayerCount()
let leftPadding = 0
for (const serverRegistration of serverRegistrations) {
const width = Math.round((serverRegistration.playerCount / totalPlayers) * this._parent.offsetWidth)
// Update position/width
// leftPadding is a sum of previous iterations width value
const div = document.getElementById('perc-bar-part_' + serverRegistration.serverId) || this.createPart(serverRegistration)
// Only redraw if needed
if (div.style.width !== width + 'px' || div.style.left !== leftPadding + 'px') {
div.style.width = width + 'px'
div.style.left = leftPadding + 'px'
}
leftPadding += width
}
}
createPart (serverRegistration) {
const div = document.createElement('div')
div.id = 'perc-bar-part_' + serverRegistration.serverId
div.style.background = serverRegistration.data.color
div.setAttribute('class', 'perc-bar-part')
div.setAttribute('minetrack-server-id', serverRegistration.serverId)
this._parent.appendChild(div)
// Define events once during creation
div.addEventListener('mouseover', this.handleMouseOver, false)
div.addEventListener('mouseout', this.handleMouseOut, false)
return div
}
handleMouseOver = (event) => {
const serverId = parseInt(event.target.getAttribute('minetrack-server-id'))
const serverRegistration = this._app.serverRegistry.getServerRegistration(serverId)
this._app.tooltip.set(event.target.offsetLeft, event.target.offsetTop, 10, this._parent.offsetTop + this._parent.offsetHeight + 10,
(typeof serverRegistration.rankIndex !== 'undefined' ? '#' + (serverRegistration.rankIndex + 1) + ' ' : '') +
serverRegistration.data.name +
'<br>' + formatNumber(serverRegistration.playerCount) + ' Players<br>' +
'<strong>' + formatPercent(serverRegistration.playerCount, this._app.getTotalPlayerCount()) + '</strong>')
}
handleMouseOut = () => {
this._app.tooltip.hide()
}
reset () {
// Reset modified DOM elements
this._parent.innerHTML = ''
}
}

304
assets/js/servers.js Normal file
View File

@ -0,0 +1,304 @@
import { formatNumber, formatTimestamp, formatDate, formatMinecraftServerAddress, formatMinecraftVersions, isArrayEqual, isObjectEqual } from './util'
import MISSING_FAVICON from '../images/missing_favicon.svg'
export const SERVER_GRAPH_OPTIONS = {
series: {
shadowSize: 0
},
xaxis: {
font: {
color: '#E3E3E3'
},
show: false
},
yaxis: {
minTickSize: 100,
tickDecimals: 0,
show: true,
tickLength: 10,
tickFormatter: formatNumber,
font: {
color: '#E3E3E3'
},
labelWidth: -10
},
grid: {
hoverable: true,
color: '#696969'
},
colors: [
'#E9E581'
]
}
export class ServerRegistry {
constructor (app) {
this._app = app
this._serverIdsByName = []
this._serverDataById = []
this._registeredServers = []
}
assignServers (servers) {
for (let i = 0; i < servers.length; i++) {
const data = servers[i]
this._serverIdsByName[data.name] = i
this._serverDataById[i] = data
}
}
createServerRegistration (serverName) {
const serverId = this._serverIdsByName[serverName]
const serverData = this._serverDataById[serverId]
const serverRegistration = new ServerRegistration(this._app, serverId, serverData)
this._registeredServers[serverId] = serverRegistration
return serverRegistration
}
getServerRegistration (serverKey) {
if (typeof serverKey === 'string') {
const serverId = this._serverIdsByName[serverKey]
return this._registeredServers[serverId]
} else if (typeof serverKey === 'number') {
return this._registeredServers[serverKey]
}
}
getServerRankBy (serverRegistration, x, sort) {
const records = Object.values(this._registeredServers)
.map(x)
.filter(val => val !== undefined)
// Invalidate any results that do not account for all serverRegistrations
if (records.length === this._registeredServers.length) {
records.sort(sort)
// Pull matching data from target serverRegistration
// Assume indexOf cannot be -1 or val undefined since they have been pre-tested in the map call above
const val = x(serverRegistration)
const indexOf = records.indexOf(val)
return indexOf + 1
}
}
getServerRegistrations = () => Object.values(this._registeredServers)
reset () {
this._serverIdsByName = []
this._serverDataById = []
this._registeredServers = []
// Reset modified DOM structures
document.getElementById('server-list').innerHTML = ''
}
}
const SERVER_GRAPH_DATA_MAX_LENGTH = 72
export class ServerRegistration {
playerCount = 0
isVisible = true
isFavorite = false
rankIndex
lastRecordData
lastVersions = []
lastPeakData
constructor (app, serverId, data) {
this._app = app
this.serverId = serverId
this.data = data
this._graphData = []
this._failedSequentialPings = 0
}
addGraphPoints (points) {
// Test if the first point contains error.placeholder === true
// This is sent by the backend when the server hasn't been pinged yet
// These points will be disregarded to prevent the graph starting at 0 player count
points = points.filter(point => !point.error || !point.error.placeholder)
// The backend should never return more data elements than the max
// but trim the data result regardless for safety and performance purposes
if (points.length > SERVER_GRAPH_DATA_MAX_LENGTH) {
points.slice(points.length - SERVER_GRAPH_DATA_MAX_LENGTH, points.length)
}
this._graphData = points.map(point => point.result ? [point.timestamp, point.result.players.online] : [point.timestamp, 0])
}
buildPlotInstance () {
this._plotInstance = $.plot('#chart_' + this.serverId, [this._graphData], SERVER_GRAPH_OPTIONS)
}
handlePing (payload, pushToGraph) {
if (payload.result) {
this.playerCount = payload.result.players.online
if (pushToGraph) {
// Only update graph for successful pings
// This intentionally pauses the server graph when pings begin to fail
this._graphData.push([payload.info.timestamp, this.playerCount])
// Trim graphData to within the max length by shifting out the leading elements
if (this._graphData.length > SERVER_GRAPH_DATA_MAX_LENGTH) {
this._graphData.shift()
}
this.redraw()
}
// Reset failed ping counter to ensure the next connection error
// doesn't instantly retrigger a layout change
this._failedSequentialPings = 0
} else {
// Attempt to retain a copy of the cached playerCount for up to N failed pings
// This prevents minor connection issues from constantly reshuffling the layout
if (++this._failedSequentialPings > 5) {
this.playerCount = 0
}
}
}
redraw () {
// Redraw the plot instance
this._plotInstance.setData([this._graphData])
this._plotInstance.setupGrid()
this._plotInstance.draw()
}
updateServerRankIndex (rankIndex) {
this.rankIndex = rankIndex
document.getElementById('ranking_' + this.serverId).innerText = '#' + (rankIndex + 1)
}
updateServerPeak (time, playerCount) {
const peakLabelElement = document.getElementById('peak_' + this.serverId)
// Always set label once any peak data has been received
peakLabelElement.style.display = 'block'
const peakValueElement = document.getElementById('peak-value_' + this.serverId)
peakValueElement.innerText = formatNumber(playerCount)
peakLabelElement.title = 'At ' + formatTimestamp(time)
this.lastPeakData = {
timestamp: time,
playerCount: playerCount
}
}
updateServerStatus (ping, isInitialUpdate, minecraftVersions) {
// Only pushToGraph when initialUpdate === false
// Otherwise the ping value is pushed into the graphData when already present
this.handlePing(ping, !isInitialUpdate)
// Compare against a cached value to avoid empty updates
// Allow undefined ping.versions inside the if statement for text reset handling
if (ping.versions && !isArrayEqual(ping.versions, this.lastVersions)) {
this.lastVersions = ping.versions
const versionsElement = document.getElementById('version_' + this.serverId)
versionsElement.style.display = 'block'
versionsElement.innerText = formatMinecraftVersions(ping.versions, minecraftVersions[ping.info.type]) || ''
}
// Compare against a cached value to avoid empty updates
if (ping.recordData !== undefined && !isObjectEqual(ping.recordData, this.lastRecordData, ['playerCount', 'timestamp'])) {
this.lastRecordData = ping.recordData
// Always set label once any record data has been received
const recordLabelElement = document.getElementById('record_' + this.serverId)
recordLabelElement.style.display = 'block'
const recordValueElement = document.getElementById('record-value_' + this.serverId)
const recordData = ping.recordData
// Safely handle legacy recordData that may not include the timestamp payload
if (recordData.timestamp !== -1) {
recordValueElement.innerHTML = formatNumber(recordData.playerCount) + ' (' + formatDate(recordData.timestamp) + ')'
recordLabelElement.title = 'At ' + formatDate(recordData.timestamp) + ' ' + formatTimestamp(recordData.timestamp)
} else {
recordValueElement.innerText = formatNumber(recordData.playerCount)
}
}
const playerCountLabelElement = document.getElementById('player-count_' + this.serverId)
const errorElement = document.getElementById('error_' + this.serverId)
if (ping.error) {
// Hide any visible player-count and show the error element
playerCountLabelElement.style.display = 'none'
errorElement.style.display = 'block'
// Attempt to find an error cause from documented options
errorElement.innerText = ping.error.description || ping.error.errno || 'Unknown error'
} else if (ping.result) {
// Ensure the player-count element is visible and hide the error element
playerCountLabelElement.style.display = 'block'
errorElement.style.display = 'none'
document.getElementById('player-count-value_' + this.serverId).innerText = formatNumber(ping.result.players.online)
// An updated favicon has been sent, update the src
// Ignore calls from 'add' events since they will have explicitly manually handled the favicon update
if (!isInitialUpdate && ping.favicon) {
document.getElementById('favicon_' + this.serverId).setAttribute('src', ping.favicon)
}
}
}
initServerStatus (latestPing) {
const peakHourDuration = Math.floor(this._app.publicConfig.graphDuration / (60 * 60 * 1000)) + 'h Peak: '
const serverElement = document.createElement('div')
serverElement.id = 'container_' + this.serverId
serverElement.innerHTML = '<div class="column column-favicon">' +
'<img class="server-favicon" src="' + (latestPing.favicon || MISSING_FAVICON) + '" id="favicon_' + this.serverId + '" title="' + this.data.name + '\n' + formatMinecraftServerAddress(this.data.ip, this.data.port) + '">' +
'<span class="server-rank" id="ranking_' + this.serverId + '"></span>' +
'</div>' +
'<div class="column column-status">' +
'<h3 class="server-name"><span class="' + this._app.favoritesManager.getIconClass(this.isFavorite) + '" id="favorite-toggle_' + this.serverId + '"></span> ' + this.data.name + '</h3>' +
'<span class="server-error" id="error_' + this.serverId + '"></span>' +
'<span class="server-label" id="player-count_' + this.serverId + '">Players: <span class="server-value" id="player-count-value_' + this.serverId + '"></span></span>' +
'<span class="server-label" id="peak_' + this.serverId + '">' + peakHourDuration + '<span class="server-value" id="peak-value_' + this.serverId + '">-</span></span>' +
'<span class="server-label" id="record_' + this.serverId + '">Record: <span class="server-value" id="record-value_' + this.serverId + '">-</span></span>' +
'<span class="server-label" id="version_' + this.serverId + '"></span>' +
'</div>' +
'<div class="column column-graph" id="chart_' + this.serverId + '"></div>'
serverElement.setAttribute('class', 'server')
document.getElementById('server-list').appendChild(serverElement)
}
updateHighlightedValue (selectedCategory) {
['player-count', 'peak', 'record'].forEach((category) => {
const labelElement = document.getElementById(category + '_' + this.serverId)
const valueElement = document.getElementById(category + '-value_' + this.serverId)
if (selectedCategory && category === selectedCategory) {
labelElement.setAttribute('class', 'server-highlighted-label')
valueElement.setAttribute('class', 'server-highlighted-value')
} else {
labelElement.setAttribute('class', 'server-label')
valueElement.setAttribute('class', 'server-value')
}
})
}
initEventListeners () {
$('#chart_' + this.serverId).bind('plothover', this._app.graphDisplayManager.handlePlotHover)
document.getElementById('favorite-toggle_' + this.serverId).addEventListener('click', () => {
this._app.favoritesManager.handleFavoriteButtonClick(this)
}, false)
}
}

View File

@ -1,538 +0,0 @@
var graphs = [];
var lastPlayerEntries = [];
var historyPlot;
var displayedGraphData;
var hiddenGraphData = [];
var isConnected = false;
var mojangServicesUpdater;
var sortServersTask;
var currentServerHover;
var faviconSize = 64;
function updateServerStatus(lastEntry) {
var info = lastEntry.info;
var div = $('#status_' + safeName(info.name));
var versionDiv = $('#version_' + safeName(info.name));
if (lastEntry.versions) {
var versions = '';
for (var i = 0; i < lastEntry.versions.length; i++) {
if (!lastEntry.versions[i]) continue;
versions += '<span class="version">' + publicConfig.minecraftVersions[lastEntry.info.type][lastEntry.versions[i]] + '</span>&nbsp;';
}
versionDiv.html(versions);
} else {
versionDiv.html('');
}
if (lastEntry.result) {
var result = lastEntry.result;
var newStatus = 'Players: <span style="font-weight: 500;">' + formatNumber(result.players.online) + '</span>';
var listing = graphs[lastEntry.info.name].listing;
if (listing.length > 0) {
newStatus += '<span class="color-gray"> (';
var playerDifference = listing[listing.length - 1][1] - listing[0][1];
if (playerDifference >= 0) {
newStatus += '+';
}
newStatus += playerDifference + ')</span>';
}
lastPlayerEntries[info.name] = result.players.online;
div.html(newStatus);
} else {
var newStatus = '<span class="color-red">';
if (findErrorMessage(lastEntry.error)) {
newStatus += findErrorMessage(lastEntry.error);
} else {
newStatus += 'Failed to ping!';
}
div.html(newStatus + '</span>');
}
var keys = Object.keys(lastPlayerEntries);
var totalPlayers = 0;
for (var i = 0; i < keys.length; i++) {
totalPlayers += lastPlayerEntries[keys[i]];
}
$("#stat_totalPlayers").text(formatNumber(totalPlayers));
$("#stat_networks").text(formatNumber(keys.length));
if (lastEntry.record) {
$('#record_' + safeName(info.name)).html('Record: ' + formatNumber(lastEntry.record));
}
updatePercentageBar();
}
function sortServers() {
var serverNames = [];
var keys = Object.keys(lastPlayerEntries);
for (var i = 0; i < keys.length; i++) {
serverNames.push(keys[i]);
}
serverNames.sort(function(a, b) {
return (lastPlayerEntries[b] || 0) - (lastPlayerEntries[a] || 0);
});
for (var i = 0; i < serverNames.length; i++) {
$('#container_' + safeName(serverNames[i])).appendTo('#server-container-list');
$('#ranking_' + safeName(serverNames[i])).text('#' + (i + 1));
}
}
function updatePercentageBar() {
var keys = Object.keys(lastPlayerEntries);
keys.sort(function(a, b) {
return lastPlayerEntries[a] - lastPlayerEntries[b];
});
var totalPlayers = getCurrentTotalPlayers();
var parent = $('#perc-bar');
var leftPadding = 0;
for (var i = 0; i < keys.length; i++) {
(function(pos, server, length) {
var safeNameCopy = safeName(server);
var playerCount = lastPlayerEntries[server];
var div = $('#perc_bar_part_' + safeNameCopy);
// Setup the base
if (!div.length) {
$('<div/>', {
id: 'perc_bar_part_' + safeNameCopy,
class: 'perc-bar-part',
html: '',
style: 'background: ' + getServerColor(server) + ';'
}).appendTo(parent);
div = $('#perc_bar_part_' + safeNameCopy);
div.mouseover(function(e) {
currentServerHover = server;
});
div.mouseout(function(e) {
hideTooltip();
currentServerHover = undefined;
});
}
// Update our position/width
var width = (playerCount / totalPlayers) * parent.width();
div.css({
width: width + 'px',
left: leftPadding + 'px'
});
leftPadding += width;
})(i, keys[i], keys.length);
}
}
function getCurrentTotalPlayers() {
var totalPlayers = 0;
var keys = Object.keys(lastPlayerEntries);
for (var i = 0; i < keys.length; i++) totalPlayers += lastPlayerEntries[keys[i]]
return totalPlayers;
}
function setAllGraphVisibility(visible) {
if (visible) {
var keys = Object.keys(hiddenGraphData);
for (var i = 0; i < keys.length; i++) {
displayedGraphData[keys[i]] = hiddenGraphData[keys[i]];
}
hiddenGraphData = [];
} else {
var keys = Object.keys(displayedGraphData);
for (var i = 0; i < keys.length; i++) {
hiddenGraphData[keys[i]] = displayedGraphData[keys[i]];
}
displayedGraphData = [];
}
$('.graph-control').each(function(index, item) {
item.checked = visible;
});
historyPlot.setData(convertGraphData(displayedGraphData));
historyPlot.setupGrid();
historyPlot.draw();
// Update our localStorage
if (visible) {
resetGraphControls();
} else {
saveGraphControls(Object.keys(displayedGraphData));
}
}
function validateBootTime(bootTime, socket) {
$('#tagline-text').text('Validating...');
console.log('Remote bootTime is ' + bootTime + ', local is ' + publicConfig.bootTime);
if (bootTime === publicConfig.bootTime) {
$('#tagline-text').text('Loading...');
socket.emit('requestListing');
if (!isMobileBrowser()) socket.emit('requestHistoryGraph');
isConnected = true;
// Start any special updating tasks.
mojangServicesUpdater = setInterval(updateMojangServices, 1000);
sortServersTask = setInterval(sortServers, 10000);
} else {
$('#tagline-text').text('Updating...');
$.getScript('/publicConfig.json', function(data, textStatus, xhr) {
if (xhr.status === 200) {
validateBootTime(publicConfig.bootTime, socket);
} else {
showCaption('Failed to update! Refresh?');
}
});
}
}
function printPort(port) {
if(port == undefined || port == 25565) {
return "";
} else {
return ":" + port;
}
}
function updateServerPeak(name, time, playerCount) {
var safeNameCopy = safeName(name);
// hack: strip the AM/PM suffix
// Javascript doesn't have a nice way to format Dates with AM/PM, so we'll append it manually
var timestamp = getTimestamp(time / 1000).split(':');
var end = timestamp.pop().split(' ')[1];
timestamp = timestamp.join(':');
// end may be undefined for other timezones/24 hour times
if (end) {
timestamp += ' ' + end;
}
var timeLabel = msToTime(publicConfig.graphDuration);
$('#peak_' + safeNameCopy).html(timeLabel + ' Peak: ' + formatNumber(playerCount) + ' @ ' + timestamp);
}
$(document).ready(function() {
var socket = io.connect({
reconnect: true,
reconnectDelay: 1000,
reconnectionAttempts: 10
});
socket.on('bootTime', function(bootTime) {
validateBootTime(bootTime, socket);
});
socket.on('disconnect', function() {
if (mojangServicesUpdater) clearInterval(mojangServicesUpdater);
if (sortServersTask) clearInterval(sortServersTask);
lastMojangServiceUpdate = undefined;
showCaption('Disconnected! Refresh?');
lastPlayerEntries = {};
graphs = {};
$('#server-container-list').html('');
$('#big-graph').html('');
$('#big-graph-checkboxes').html('');
$('#big-graph-controls').css('display', 'none');
$('#perc-bar').html('');
$('.mojang-status').css('background', 'transparent');
$('.mojang-status-text').text('...');
$("#stat_totalPlayers").text(0);
$("#stat_networks").text(0);
isConnected = false;
});
socket.on('historyGraph', function(rawData) {
var shownServers = loadGraphControls();
if (shownServers) {
var keys = Object.keys(rawData);
hiddenGraphData = [];
displayedGraphData = [];
for (var i = 0; i < keys.length; i++) {
var name = keys[i];
if (shownServers.indexOf(name) !== -1) {
displayedGraphData[name] = rawData[name];
} else {
hiddenGraphData[name] = rawData[name];
}
}
} else {
displayedGraphData = rawData;
}
$('#big-graph').css('height', '400px');
historyPlot = $.plot('#big-graph', convertGraphData(displayedGraphData), bigChartOptions);
$('#big-graph').bind('plothover', handlePlotHover);
var keys = Object.keys(rawData);
var sinceBreak = 0;
var html = '<table><tr>';
keys.sort();
for (var i = 0; i < keys.length; i++) {
var checkedString = '';
if (displayedGraphData[keys[i]]) {
checkedString = 'checked=checked';
}
html += '<td><input type="checkbox" class="graph-control" id="graph-controls" data-target-network="' + keys[i] + '" ' + checkedString + '> ' + keys[i] + '</input></td>';
if (sinceBreak >= 7) {
sinceBreak = 0;
html += '</tr><tr>';
} else {
sinceBreak++;
}
}
$('#big-graph-checkboxes').append(html + '</tr></table>');
$('#big-graph-controls').css('display', 'block');
});
socket.on('updateHistoryGraph', function(rawData) {
// Prevent race conditions.
if (!displayedGraphData || !hiddenGraphData) {
return;
}
// If it's not in our display group, use the hidden group instead.
var targetGraphData = displayedGraphData[rawData.name] ? displayedGraphData : hiddenGraphData;
trimOldPings(targetGraphData, publicConfig.graphDuration);
targetGraphData[rawData.name].push([rawData.timestamp, rawData.players]);
// Redraw if we need to.
if (displayedGraphData[rawData.name]) {
historyPlot.setData(convertGraphData(displayedGraphData));
historyPlot.setupGrid();
historyPlot.draw();
}
});
socket.on('add', function(servers) {
for (var i = 0; i < servers.length; i++) {
var history = servers[i];
var listing = [];
for (var x = 0; x < history.length; x++) {
var point = history[x];
if (point.result) {
listing.push([point.timestamp, point.result.players.online]);
} else if (point.error) {
listing.push([point.timestamp, 0]);
}
}
var lastEntry = history[history.length - 1];
var info = lastEntry.info;
if (lastEntry.error) {
lastPlayerEntries[info.name] = 0;
} else if (lastEntry.result) {
lastPlayerEntries[info.name] = lastEntry.result.players.online;
}
var typeString = publicConfig.serverTypesVisible ? '<span class="type">' + info.type + '</span>' : '';
var safeNameCopy = safeName(info.name);
$('<div/>', {
id: 'container_' + safeNameCopy,
class: 'server',
'server-id': safeNameCopy,
html: '<div id="server-' + safeNameCopy + '" class="column" style="width: 80px;">\
<img id="favicon_' + safeNameCopy + '" title="' + info.name + '\n' + info.ip + printPort(info.port) + '" height="' + faviconSize + '" width="' + faviconSize + '">\
<br />\
<p class="text-center-align rank" id="ranking_' + safeNameCopy + '"></p>\
</div>\
<div class="column" style="width: 282px;">\
<h3>' + info.name + '&nbsp;' + typeString + '</h3>\
<span id="status_' + safeNameCopy + '">Waiting</span>\
<div id="version_' + safeNameCopy + '" class="color-dark-gray server-meta versions"><span class="version"></span></div>\
<span id="peak_' + safeNameCopy + '" class="color-dark-gray server-meta"></span>\
<br><span id="record_' + safeNameCopy + '" class="color-dark-gray server-meta"></span>\
</div>\
<div class="column" style="float: right;">\
<div class="chart" id="chart_' + safeNameCopy + '"></div>\
</div>'
}).appendTo("#server-container-list");
var favicon = MISSING_FAVICON_BASE64;
if (lastEntry.result && lastEntry.result.favicon) {
favicon = lastEntry.result.favicon;
}
$('#favicon_' + safeName(info.name)).attr('src', favicon);
graphs[lastEntry.info.name] = {
listing: listing,
plot: $.plot('#chart_' + safeNameCopy, [listing], smallChartOptions)
};
updateServerStatus(lastEntry);
$('#chart_' + safeNameCopy).bind('plothover', handlePlotHover);
}
sortServers();
updatePercentageBar();
});
socket.on('update', function(update) {
// Prevent weird race conditions.
if (!graphs[update.info.name]) {
return;
}
// We have a new favicon, update the old one.
if (update.result && update.result.favicon) {
$('#favicon_' + safeName(update.info.name)).attr('src', update.result.favicon);
}
var graph = graphs[update.info.name];
updateServerStatus(update);
if (update.result) {
graph.listing.push([update.info.timestamp, update.result ? update.result.players.online : 0]);
if (graph.listing.length > 72) {
graph.listing.shift();
}
graph.plot.setData([graph.listing]);
graph.plot.setupGrid();
graph.plot.draw();
}
});
socket.on('updateMojangServices', function(data) {
if (isConnected) {
updateMojangServices(data);
}
});
socket.on('syncComplete', function() {
hideCaption();
});
socket.on('updatePeak', function(data) {
updateServerPeak(data.name, data.timestamp, data.players);
});
socket.on('peaks', function(data) {
var keys = Object.keys(data);
for (var i = 0; i < keys.length; i++) {
var val = data[keys[i]];
updateServerPeak(keys[i], val[0], val[1]);
}
});
$(document).on('click', '.graph-control', function(e) {
var serverIp = $(this).attr('data-target-network');
// Restore it, or delete it - either works.
if (!this.checked) {
hiddenGraphData[serverIp] = displayedGraphData[serverIp];
delete displayedGraphData[serverIp];
} else {
displayedGraphData[serverIp] = hiddenGraphData[serverIp];
delete hiddenGraphData[serverIp];
}
// Redraw the graph
historyPlot.setData(convertGraphData(displayedGraphData));
historyPlot.setupGrid();
historyPlot.draw();
// Update our localStorage
if (Object.keys(hiddenGraphData).length === 0) {
resetGraphControls();
} else {
saveGraphControls(Object.keys(displayedGraphData));
}
});
$(document).on('mousemove', function(e) {
if (currentServerHover) {
var totalPlayers = getCurrentTotalPlayers();
var playerCount = lastPlayerEntries[currentServerHover];
renderTooltip(e.pageX + 10, e.pageY + 10, '<strong>' + currentServerHover + '</strong>: ' + roundToPoint(playerCount / totalPlayers * 100, 10) + '% of ' + formatNumber(totalPlayers) + ' tracked players.<br />(' + formatNumber(playerCount) + ' online.)');
}
});
$(window).on('resize', function() {
updatePercentageBar();
if (historyPlot) {
historyPlot.resize();
historyPlot.setupGrid();
historyPlot.draw();
}
});
});

185
assets/js/sort.js Normal file
View File

@ -0,0 +1,185 @@
import { isArrayEqual } from './util'
const SORT_OPTIONS = [
{
getName: () => 'Players',
sortFunc: (a, b) => b.playerCount - a.playerCount,
highlightedValue: 'player-count'
},
{
getName: (app) => {
return Math.floor(app.publicConfig.graphDuration / (60 * 60 * 1000)) + 'h Peak'
},
sortFunc: (a, b) => {
if (!a.lastPeakData && !b.lastPeakData) {
return 0
} else if (a.lastPeakData && !b.lastPeakData) {
return -1
} else if (b.lastPeakData && !a.lastPeakData) {
return 1
}
return b.lastPeakData.playerCount - a.lastPeakData.playerCount
},
testFunc: (app) => {
// Require at least one ServerRegistration to have a lastPeakData value defined
for (const serverRegistration of app.serverRegistry.getServerRegistrations()) {
if (serverRegistration.lastPeakData) {
return true
}
}
return false
},
highlightedValue: 'peak'
},
{
getName: () => 'Record',
sortFunc: (a, b) => {
if (!a.lastRecordData && !b.lastRecordData) {
return 0
} else if (a.lastRecordData && !b.lastRecordData) {
return -1
} else if (b.lastRecordData && !a.lastRecordData) {
return 1
}
return b.lastRecordData.playerCount - a.lastRecordData.playerCount
},
testFunc: (app) => {
// Require at least one ServerRegistration to have a lastRecordData value defined
for (const serverRegistration of app.serverRegistry.getServerRegistrations()) {
if (serverRegistration.lastRecordData) {
return true
}
}
return false
},
highlightedValue: 'record'
}
]
const SORT_OPTION_INDEX_DEFAULT = 0
const SORT_OPTION_INDEX_STORAGE_KEY = 'minetrack_sort_option_index'
export class SortController {
constructor (app) {
this._app = app
this._buttonElement = document.getElementById('sort-by')
this._textElement = document.getElementById('sort-by-text')
this._sortOptionIndex = SORT_OPTION_INDEX_DEFAULT
}
reset () {
this._lastSortedServers = undefined
// Reset modified DOM structures
this._buttonElement.style.display = 'none'
this._textElement.innerText = '...'
// Remove bound DOM event listeners
this._buttonElement.removeEventListener('click', this.handleSortByButtonClick)
}
loadLocalStorage () {
if (typeof localStorage !== 'undefined') {
const sortOptionIndex = localStorage.getItem(SORT_OPTION_INDEX_STORAGE_KEY)
if (sortOptionIndex) {
this._sortOptionIndex = parseInt(sortOptionIndex)
}
}
}
updateLocalStorage () {
if (typeof localStorage !== 'undefined') {
if (this._sortOptionIndex !== SORT_OPTION_INDEX_DEFAULT) {
localStorage.setItem(SORT_OPTION_INDEX_STORAGE_KEY, this._sortOptionIndex)
} else {
localStorage.removeItem(SORT_OPTION_INDEX_STORAGE_KEY)
}
}
}
show () {
// Load the saved option selection, if any
this.loadLocalStorage()
this.updateSortOption()
// Bind DOM event listeners
// This is removed by #reset to avoid multiple listeners
this._buttonElement.addEventListener('click', this.handleSortByButtonClick)
// Show #sort-by element
this._buttonElement.style.display = 'inline-block'
}
handleSortByButtonClick = () => {
while (true) {
// Increment to the next sort option, wrap around if needed
this._sortOptionIndex = (this._sortOptionIndex + 1) % SORT_OPTIONS.length
// Only break if the sortOption is supported
// This can technically cause an infinite loop, but never should assuming
// at least one sortOption does not implement the test OR always returns true
const sortOption = SORT_OPTIONS[this._sortOptionIndex]
if (!sortOption.testFunc || sortOption.testFunc(this._app)) {
break
}
}
// Redraw the button and sort the servers
this.updateSortOption()
// Save the updated option selection
this.updateLocalStorage()
}
updateSortOption = () => {
const sortOption = SORT_OPTIONS[this._sortOptionIndex]
// Pass app instance so sortOption names can be dynamically generated
this._textElement.innerText = sortOption.getName(this._app)
// Update all servers highlighted values
for (const serverRegistration of this._app.serverRegistry.getServerRegistrations()) {
serverRegistration.updateHighlightedValue(sortOption.highlightedValue)
}
this.sortServers()
}
sortServers = () => {
const sortOption = SORT_OPTIONS[this._sortOptionIndex]
const sortedServers = this._app.serverRegistry.getServerRegistrations().sort((a, b) => {
if (a.isFavorite && !b.isFavorite) {
return -1
} else if (b.isFavorite && !a.isFavorite) {
return 1
}
return sortOption.sortFunc(a, b)
})
// Test if sortedServers has changed from the previous listing
// This avoids DOM updates and graphs being redrawn
const sortedServerIds = sortedServers.map(server => server.serverId)
if (isArrayEqual(sortedServerIds, this._lastSortedServers)) {
return
}
this._lastSortedServers = sortedServerIds
// Sort a ServerRegistration list by the sortOption ONLY
// This is used to determine the ServerRegistration's rankIndex without #isFavorite skewing values
const rankIndexSort = this._app.serverRegistry.getServerRegistrations().sort(sortOption.sortFunc)
// Update the DOM structure
sortedServers.forEach(function (serverRegistration) {
$('#container_' + serverRegistration.serverId).appendTo('#server-list')
// Set the ServerRegistration's rankIndex to its indexOf the normal sort
serverRegistration.updateServerRankIndex(rankIndexSort.indexOf(serverRegistration))
})
}
}

View File

@ -1,189 +1,160 @@
var MISSING_FAVICON_BASE64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAJNUlEQVR4XtVbe4xU5RX/nXNnH+W1pS8iXaFVwRgaU7ph597ZxZKmD2maGtNSRNFiWyEQLSVaVKRStCgUqo2VVkiR0tiA0ldqW1IbEyzsfDOr2waJ1oYKQpcoWkFeBWe55zTfsEt3l537mLmzbO+/93zfOed3z+s737mEKj86fXodDh+eAse5UlQnkOplAowhohFQHV5kT3RSVU8wcEiJ9rDqHhDtwrFjL9BLLxWqKSJVY3Ntbv4oHOc6X+RqApqZub4cPgKcYqAdRNsAbKFsdn85+wStSQwAnTSpFg0NN/gicx0iN2lBBVD1feM4zjoUCpupo6MrCR4VA6BNTcNQU3OrqC5k5rFJCBW2hwCdrPowiH5CxpwKo6+aBajrzhTm1ax6cSVClLtWgNeY6A7KZn9V7h5lWYCm043KvImAT5XLOMl1CjxDqdQc2rHj9bj7xgZAXfeLQrSRgffFZVZNevH9t7imZg61tf0xDp9YAPiZzApWXRKHwWDS2kDJqsspl1selW8kAHTGDEcOHFjPzF+LunFvOgEOQuQvTGRA9DJ8fy+I3kY+fwLTpjFOnRoG5g8BGA+iSSIyBUTTGGgsk986NmYBARK2PhQAq7x2dm4l4NqwzfooLXIEjmNdZQuy2RcI0DjrLa163mQBrofvf5Ud54Mx129FY+Ms2rrVrygL+On0hjhfXoA3GFgBYEOlKapH8O4aY46I3MNE46ICIUQ/dbLZW8oGwM9kvseq90RhKCI+E61BV9d91NHxnyhr4tIUgRg16k4RWRK1uhTgQceYknGrpAtoJnMNVH8bQ8h7yZj7Y9CXTaotLZeLyFMMXBlpE6Ivl6oVBgTA5nlh3hUn1QnQxcBMMuY3kYSqkEg97z2iup6JZkfY6ih8fzK1t+/rTzsgAOJ5z5ZT5Aw6CACJ532fgTvCQPBFjJPPt/QPxucBYMtbEG0J27DU+8EGwcrhe97qKCAA+AYZs6G37H0AsAcbqat7pdLafrBBUGsJrvvzMHcQon8z80TaufNIDwh9AXDdxSBaVe7X71f8DH5MAHJhgVGAlY4xd58HgE0xMmrUawxcVNK8bapjXi7AdxioCQNq0C3BZoeurl3sOHUBLnqMC4Vx1NFx1NKcswDNZG6G6uOBSqmuolzuLvW8awV4ckiC4Hn3Agg7C9xNxqzsA8AZ1zVBnZxihVcoXNpT5AxVEIqWPHLknqCKUYB9jjGXnAOgu4e3N8SkbyNjHu1NM2RByGTmQnVdoD5EGcpmTdEF1PNsUHggwPePMPOHB6rthyIIRSsYPrwz6AAlwKOOMbcVATiTTj/nMF9VEgCih5xs9vZS74ciCP7ZAunbJXXy/Ved9vbLyPbt5fDhdwIPF0TNlM0+H2RSQw0Ee5QG8NcQt24kTadbwbwjIG0cZGMujnKeH2og+J73r5Cmyg2kmcwCqK4N8P/NTj5/fVjO73k/lEDwPe8XbBsqJX1XV5HveQ8z8K0Aom9SLvejqAB0B9UhUSeo5y0E8MOS+gO/tjX000T0hQAFP03GPBsHgKECgqbTnwHzMwHuvZvOuG67QzSlpIK+f8lA5+gogFxod9Dm5olwnH8EZIK3rAu8zMAVJRUSaaB8/lgUhQeiuZAgqOfZu4u3AyzgFPmuuz+w0WiME6W9PBRTpO0aAQjsT4YDUFdXQ9u3nynXAi5kdihe3NbWngySveou0Jv5YLuDptNjwPxGoAuEBkFgAhnzz0ot4EJYgmYyH4Pq7oAa582qpcGhEBPCWvsC7LYu8BADi0oKTLSQstlHkrKAwbQEdd0lILK3VAM+agshdd35IPpxSTNRfcLJ5W5MGoDBKJbCijzbHyT1vAyAtoBA0ekYU7UJkGoFRm1qqpFU6k1mfm+Adc+iYvNgxIijIXdtnyBj/lYNK6iWJYSVwUVdUqmxPR2h7QA+GWAFaxxjSjYXkgAmaUvwM5mNrDonQKc9jjETzwKQydwJ1WKXdKCnOH5y8mRjtYcWkwLBlsDFSTLAVoID66T6iJPLLewBYLyo7uNebfLzVhHNo2x2fRJfu9opUjOZpVANvqlWdSmXy5+7FzjT3NzmOI4NiKUQO8DHj0+othVUGhO0tXU0fN/eAjcEWHSxH2jf/+9ixPNuArAp5AsP3gxAmZcvvuuuZaIFIXosJmNW9wXApo3a2r1BPTQROc2p1Mepra3kGTtJF4kbEwDYOLY0yJVF5B1OpcZRW9vxPgB0B8PbobomSAkBXuS6ujRt3346SWVL7RUHhCjyCNEKJ5td2kPb93bYTl2I/J2ZxweCoPoE53I3RekURxEqjCYpEAQ4xIXC5T0Xo+dZQLcVfAmqvwwTSoCq1wa9ZUgEBNWbKZf7We99S43I/ImAz0YBgY1Z/P9gCT7RTiebvSp0RKZoBS0tY0V1F6t+IBQE6w5Ec5OaCQzjV44lCHCYHWcy7dx5oP/+pcfkWlo+LyK/DyyOunez52oGvkLGvBKmQBLvfc9bzoCdA4j2qF5DudzvBiIOHJVV110Gou9G4SK+/y6nUivR0PAgbdv2bpQ1cWk0nR4lRPeD6FYGOMp6Ae5zjFlWijZ0Vtj3vMcYmBeFmaXp/pvjAYwe/XhSQFjFwTxPgLtizi4+5hgzP0j2UADssDQ6OzcDmBEVhG4gbD/eTm5tLmdYWu0X9rypAlwHkdnMPCIWf5HNnM/PDmvphwJQDIp2XL6zc20cS+gtrACvQ/U5Bp4vjsur7gfRIRQKp1FbqyAaCZH3g+hSAFco4Knvt5YxIV5ka4cf2Bh70qt8XL5PLnbdZUK0LEpgjPO1kqLt/mFiKeVyJaddImeBUkKp604XkU3lfp2klO2/T7HKE7mR8vk/x+ERyQX6b6hTp16khcJGYv5cHGbVolXgDyTydcrnD8XlURYAPUy6ixLbVv9IXMZJ0Ivvv8rMiyiXe7rc/SoCoBggp02rx+nT84V5UaUzxlGVEBF7ofsDjB69vtJUWzEA56yhqakGtbWzfKJbSLUl6UBZ/HVWZIfDvA51dU8lcWFrZU8MgD7ZorV1HHx/pgJXK+AyMCzq1+2XPk8SYEh1G4ieJGMOlrNPRYVQpQztBQXq65ugevb3eWCCqI4hYATsL/RFP9ITqnq8+Ps8sx1z3QORF1Ff35HUly6lx38BC3SpK3sT2hIAAAAASUVORK5CYII=";
export class Tooltip {
constructor () {
this._div = document.getElementById('tooltip')
}
var tooltip = $('#tooltip');
set (x, y, offsetX, offsetY, html) {
this._div.innerHTML = html
var lastMojangServiceUpdate;
var publicConfig;
// Assign display: block so that the offsetWidth is valid
this._div.style.display = 'block'
function showCaption(html) {
var tagline = $('#tagline-text');
tagline.stop(true, false);
tagline.html(html);
tagline.slideDown(100);
}
// Prevent the div from overflowing the page width
const tooltipWidth = this._div.offsetWidth
function hideCaption() {
var tagline = $('#tagline-text');
tagline.stop(true, false);
tagline.slideUp(100);
}
function setPublicConfig(json) {
publicConfig = json;
$('#server-container-list').html('');
}
function getServerByField(id, value) {
for (var i = 0; i < publicConfig.servers.length; i++) {
var entry = publicConfig.servers[i];
if (entry[id] === value) {
return entry;
}
}
}
function getServerByIp(ip) {
return getServerByField('ip', ip);
}
function getServerByName(name) {
return getServerByField('name', name);
}
function getServerColor(name) {
var server = getServerByName(name);
return server ? server.color : stringToColor(name);
}
// Generate (and set) the HTML that displays Mojang status.
// If nothing is passed, re-render the last update.
// If something is passed, update and then re-render.
function updateMojangServices(currentUpdate) {
if (currentUpdate) {
lastMojangServiceUpdate = currentUpdate;
// 1.2 is a magic number used to pad the offset to ensure the tooltip
// never gets close or surpasses the page's X width
if (x + offsetX + (tooltipWidth * 1.2) > window.innerWidth) {
x -= tooltipWidth
offsetX *= -1
}
if (!lastMojangServiceUpdate) {
return;
this._div.style.top = (y + offsetY) + 'px'
this._div.style.left = (x + offsetX) + 'px'
}
hide = () => {
this._div.style.display = 'none'
}
}
export class Caption {
constructor () {
this._div = document.getElementById('status-text')
}
set (text) {
this._div.innerText = text
this._div.style.display = 'block'
}
hide () {
this._div.style.display = 'none'
}
}
// Minecraft Java Edition default server port: 25565
// Minecraft Bedrock Edition default server port: 19132
const MINECRAFT_DEFAULT_PORTS = [25565, 19132]
export function formatMinecraftServerAddress (ip, port) {
if (port && !MINECRAFT_DEFAULT_PORTS.includes(port)) {
return ip + ':' + port
}
return ip
}
// Detect gaps in versions by matching their indexes to knownVersions
export function formatMinecraftVersions (versions, knownVersions) {
if (!versions || !versions.length || !knownVersions || !knownVersions.length) {
return
}
let currentVersionGroup = []
const versionGroups = []
for (let i = 0; i < versions.length; i++) {
const versionIndex = versions[i]
// Look for value mismatch between the previous index
// Require i > 0 since lastVersionIndex is undefined for i === 0
if (i > 0 && versions[i] - 1 !== versionIndex - 1) {
versionGroups.push(currentVersionGroup)
currentVersionGroup = []
}
var keys = Object.keys(lastMojangServiceUpdate);
currentVersionGroup.push(versionIndex)
}
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var status = lastMojangServiceUpdate[key];
// Ensure the last versionGroup is always pushed
if (currentVersionGroup.length > 0) {
versionGroups.push(currentVersionGroup)
}
// hack: ensure mojang-status is added for alignment, replace existing class to swap status color
$('#mojang-status_' + status.name).attr('class', 'mojang-status mojang-status-' + status.title.toLowerCase());
$('#mojang-status-text_' + status.name).text(status.title);
if (versionGroups.length === 0) {
return
}
// Remap individual versionGroups values into named versions
return versionGroups.map(versionGroup => {
const startVersion = knownVersions[versionGroup[0]]
if (versionGroup.length === 1) {
// A versionGroup may contain a single version, only return its name
// This is a cosmetic catch to avoid version labels like 1.0-1.0
return startVersion
} else {
const endVersion = knownVersions[versionGroup[versionGroup.length - 1]]
return startVersion + '-' + endVersion
}
}).join(', ')
}
function findErrorMessage(error) {
if (error.description) {
return error.description;
} else if (error.errno) {
return error.errno;
export function formatTimestamp (millis) {
const date = new Date(0)
date.setUTCSeconds(millis / 1000)
return date.toLocaleTimeString()
}
export function formatDate (millis) {
const date = new Date(0)
date.setUTCSeconds(millis / 1000)
return date.toLocaleDateString()
}
export function formatPercent (x, over) {
const val = Math.round((x / over) * 100 * 10) / 10
return val + '%'
}
export function formatNumber (x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
export function isArrayEqual (a, b) {
if (typeof a === 'undefined' || typeof a !== typeof b) {
return false
}
if (a.length !== b.length) {
return false
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false
}
}
return true
}
function getTimestamp(ms, timeOnly) {
var date = new Date(0);
export function isObjectEqual (a, b, props) {
if (typeof a === 'undefined' || typeof a !== typeof b) {
return false
}
for (let i = 0; i < props.length; i++) {
const prop = props[i]
date.setUTCSeconds(ms);
return date.toLocaleTimeString();
}
function safeName(name) {
return name.replace(/ /g, '');
}
function renderTooltip(x, y, html) {
tooltip.html(html).css({
top: y,
left: x
}).fadeIn(0);
}
function hideTooltip() {
tooltip.hide();
}
function formatNumber(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
if (typeof a[prop] === 'undefined' || typeof a[prop] !== typeof b[prop] || a[prop] !== b[prop]) {
return false
}
}
return true
}
// From http://detectmobilebrowsers.com/
function isMobileBrowser() {
export function isMobileBrowser () {
var check = false;
(function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera);
return check;
}
function trimOldPings(data, graphDuration) {
var keys = Object.keys(data);
var timeMs = new Date().getTime();
for (var x = 0; x < keys.length; x++) {
var listing = data[keys[x]];
var toSplice = [];
for (var i = 0; i < listing.length; i++) {
var entry = listing[i];
if (timeMs - entry[0] > graphDuration) {
toSplice.push(i);
}
}
for (var i = 0; i < toSplice.length; i++) {
listing.splice(toSplice[i], 1);
}
}
}
function stringToColor(base) {
var hash;
for (var i = base.length - 1, hash = 0; i >= 0; i--) {
hash = base.charCodeAt(i) + ((hash << 5) - hash);
}
color = Math.floor(Math.abs((Math.sin(hash) * 10000) % 1 * 16777216)).toString(16);
return '#' + Array(6 - color.length + 1).join('0') + color;
}
function roundToPoint(val, scale) {
return Math.round(val * scale) / scale;
}
function msToTime(timer) {
var milliseconds = timer % 1000;
timer = (timer - milliseconds) / 1000;
var seconds = timer % 60;
timer = (timer - seconds) / 60;
var minutes = timer % 60;
var hours = (timer - minutes) / 60;
var days = Math.floor(hours / 24);
hours -= days * 24;
var string = '';
// hack: only format days if >1, if === 1 it will format as "24h" instead
if (days > 1) {
string += days + 'd';
} else if (days === 1) {
hours += 24;
}
if (hours > 0) {
string += hours + 'h';
}
if (minutes > 0) {
string += minutes + 'm';
}
if (seconds > 0) {
string += seconds + 's';
}
return string;
// eslint-disable-next-line no-useless-escape
(function (a) { if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4)))check = true })(navigator.userAgent || navigator.vendor || window.opera)
return check
}