diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..7b5554a --- /dev/null +++ b/.babelrc @@ -0,0 +1,5 @@ +{ + "plugins": [ + "@babel/plugin-proposal-class-properties" + ] +} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..0ec52d3 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "env": { + "browser": true, + "es6": true, + "jquery": true + }, + "extends": [ + "standard" + ], + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 2018 + }, + "rules": { + }, + "parser": "babel-eslint" +} diff --git a/.gitignore b/.gitignore index e0f3e0d..5985360 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ minetrack.log .idea/ database.sql database.sql-journal -.DS_Store \ No newline at end of file +.DS_Store +dist/ +.cache/ diff --git a/README.md b/README.md index 3824a6a..d2be1ef 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ Minetrack is a Minecraft PC/PE server tracker that lets you focus on what's happening *now*. Built to be lightweight and durable, you can easily adapt it to monitor BungeeCord or server instances. +#### Migrating to Minetrack 5 +See our Minetrack 5 [migration guide](docs/MIGRATING.md). + #### This project is not actively maintained! This project and the offical website are not actively maintained anymore, but you are welcome to run your own instances of Minetrack. I will however review and accept pull-requests, so please share any improvements you are making so everybody can benefit from them. diff --git a/app.js b/app.js index fdaac82..674f0c7 100644 --- a/app.js +++ b/app.js @@ -1,3 +1,8 @@ +/** + * THIS IS LEGACY, UNMAINTAINED CODE + * IT MAY (AND LIKELY DOES) CONTAIN BUGS + * USAGE IS NOT RECOMMENDED + */ var server = require('./lib/server'); var ping = require('./lib/ping'); var logger = require('./lib/logger'); @@ -7,22 +12,44 @@ var db = require('./lib/database'); var config = require('./config.json'); var servers = require('./servers.json'); +var minecraftVersions = require('./minecraft_versions.json'); var networkHistory = []; var connectedClients = 0; -var currentVersionIndex = { - 'PC': 0, - 'PE': 0 -}; - var networkVersions = []; +const lastFavicons = []; + var graphData = []; var highestPlayerCount = {}; var lastGraphPush = []; var graphPeaks = {}; +const serverProtocolVersionIndexes = [] + +function getNextProtocolVersion (server) { + // Minecraft Bedrock Edition does not have protocol versions + if (server.type === 'PE') { + return { + protocolId: 0, + protocolIndex: 0 + } + } + const protocolVersions = minecraftVersions[server.type] + let nextProtocolVersion = serverProtocolVersionIndexes[server.name] + if (typeof nextProtocolVersion === 'undefined' || nextProtocolVersion + 1 >= protocolVersions.length) { + nextProtocolVersion = 0 + } else { + nextProtocolVersion++ + } + serverProtocolVersionIndexes[server.name] = nextProtocolVersion + return { + protocolId: protocolVersions[nextProtocolVersion].protocolId, + protocolIndex: nextProtocolVersion + } +} + function pingAll() { for (var i = 0; i < servers.length; i++) { // Make sure we lock our scope. @@ -32,7 +59,7 @@ function pingAll() { network.color = util.stringToColor(network.name); } - var attemptedVersion = config.versions[network.type][currentVersionIndex[network.type]]; + const attemptedVersion = getNextProtocolVersion(network) ping.ping(network.ip, network.port, network.type, config.rates.connectTimeout, function(err, res) { // Handle our ping results, if it succeeded. if (err) { @@ -40,27 +67,14 @@ function pingAll() { } // If we have favicon override specified, use it. - if (res && config.faviconOverride && config.faviconOverride[network.name]) { - res.favicon = config.faviconOverride[network.name]; + if (network.favicon) { + res.favicon = network.favicon } handlePing(network, res, err, attemptedVersion); - }, attemptedVersion); + }, attemptedVersion.protocolId); })(servers[i]); } - - currentVersionIndex['PC']++; - currentVersionIndex['PE']++; - - if (currentVersionIndex['PC'] >= config.versions['PC'].length) { - // Loop around - currentVersionIndex['PC'] = 0; - } - - if (currentVersionIndex['PE'] >= config.versions['PE'].length) { - // Loop around - currentVersionIndex['PE'] = 0; - } } // This is where the result of a ping is feed. @@ -76,18 +90,31 @@ function handlePing(network, res, err, attemptedVersion) { networkVersions[network.name] = []; } + const serverVersionHistory = networkVersions[network.name] + // If the result version matches the attempted version, the version is supported - var _networkVersions = networkVersions[network.name]; + if (res && res.version !== undefined) { + const indexOf = serverVersionHistory.indexOf(attemptedVersion.protocolIndex) + + // Test indexOf to avoid inserting previously recorded protocolIndex values + if (res.version === attemptedVersion.protocolId && indexOf === -1) { + serverVersionHistory.push(attemptedVersion.protocolIndex) + } else if (res.version !== attemptedVersion.protocolId && indexOf >= 0) { + serverVersionHistory.splice(indexOf, 1) + } + } + + const timestamp = util.getCurrentTimeMs() + if (res) { - if (res.version == attemptedVersion) { - if (_networkVersions.indexOf(res.version) == -1) { - _networkVersions.push(res.version); - } - } else { - // Mismatch, so remove the version from the supported version list - var index = _networkVersions.indexOf(attemptedVersion); - if (index != -1) { - _networkVersions.splice(index, 1); + const recordData = highestPlayerCount[network.ip] + + // Validate that we have logToDatabase enabled otherwise in memory pings + // will create a record that's only valid for the runtime duration. + if (config.logToDatabase && (!recordData || res.players.online > recordData.playerCount)) { + highestPlayerCount[network.ip] = { + playerCount: res.players.online, + timestamp: timestamp } } } @@ -96,20 +123,25 @@ function handlePing(network, res, err, attemptedVersion) { var networkSnapshot = { info: { name: network.name, - timestamp: util.getCurrentTimeMs(), + timestamp: timestamp, type: network.type }, - versions: _networkVersions, - record: highestPlayerCount[network.ip] + versions: serverVersionHistory, + recordData: highestPlayerCount[network.ip] }; if (res) { networkSnapshot.result = res; - // Validate that we have logToDatabase enabled otherwise in memory pings - // will create a record that's only valid for the runtime duration. - if (config.logToDatabase && res.players.online > highestPlayerCount[network.ip]) { - highestPlayerCount[network.ip] = res.players.online; + // Only emit updated favicons + // Favicons will otherwise be explicitly emitted during the handshake process + if (res.favicon) { + const lastFavicon = lastFavicons[network.name] + if (lastFavicon !== res.favicon) { + lastFavicons[network.name] = res.favicon + networkSnapshot.favicon = res.favicon // Send updated favicon directly on object + } + delete res.favicon // Never store favicons in memory outside lastFavicons } } else if (err) { networkSnapshot.error = err; @@ -121,18 +153,15 @@ function handlePing(network, res, err, attemptedVersion) { // Remove our previous data that we don't need anymore. for (var i = 0; i < _networkHistory.length; i++) { + delete _networkHistory[i].versions delete _networkHistory[i].info; - - if (_networkHistory[i].result) { - delete _networkHistory[i].result.favicon; - } } _networkHistory.push({ error: err, result: res, - versions: _networkVersions, - timestamp: util.getCurrentTimeMs(), + versions: serverVersionHistory, + timestamp: timestamp, info: { ip: network.ip, port: network.port, @@ -148,18 +177,16 @@ function handlePing(network, res, err, attemptedVersion) { // Log it to the database if needed. if (config.logToDatabase) { - db.log(network.ip, util.getCurrentTimeMs(), res ? res.players.online : 0); + db.log(network.ip, timestamp, res ? res.players.online : 0); } - // Push it to our graphs. - var timeMs = util.getCurrentTimeMs(); // The same mechanic from trimUselessPings is seen here. // If we dropped the ping, then to avoid destroying the graph, ignore it. // However if it's been too long since the last successful ping, we'll send it anyways. if (config.logToDatabase) { - if (!lastGraphPush[network.ip] || (timeMs - lastGraphPush[network.ip] >= 60 * 1000 && res) || timeMs - lastGraphPush[network.ip] >= 70 * 1000) { - lastGraphPush[network.ip] = timeMs; + if (!lastGraphPush[network.ip] || (timestamp - lastGraphPush[network.ip] >= 60 * 1000 && res) || timestamp - lastGraphPush[network.ip] >= 70 * 1000) { + lastGraphPush[network.ip] = timestamp; // Don't have too much data! util.trimOldPings(graphData); @@ -168,14 +195,14 @@ function handlePing(network, res, err, attemptedVersion) { graphData[network.name] = []; } - graphData[network.name].push([timeMs, res ? res.players.online : 0]); + graphData[network.name].push([timestamp, res ? res.players.online : 0]); // Send the update. server.io.sockets.emit('updateHistoryGraph', { ip: network.ip, name: network.name, players: (res ? res.players.online : 0), - timestamp: timeMs + timestamp: timestamp }); } @@ -234,10 +261,6 @@ function startServices() { logger.log('info', '%s connected, total clients: %d', util.getRemoteAddr(client.request), connectedClients); - // We send the boot time (also sent in publicConfig.json) to the frontend to validate they have the same config. - // If so, they'll send back "requestListing" event, otherwise they will pull the new config and retry. - client.emit('bootTime', util.getBootTime()); - // Attach our listeners. client.on('disconnect', function() { connectedClients -= 1; @@ -257,41 +280,54 @@ function startServices() { } }); - client.on('requestListing', function() { - // Send them our previous data, so they have somewhere to start. - client.emit('updateMojangServices', mojang.toMessage()); + const minecraftVersionNames = {} + Object.keys(minecraftVersions).forEach(function (key) { + minecraftVersionNames[key] = minecraftVersions[key].map(version => version.name) + }) - // Remap our associative array into just an array. - var networkHistoryKeys = Object.keys(networkHistory); - - networkHistoryKeys.sort(); - - // Send each individually, this should look cleaner than waiting for one big array to transfer. - for (var i = 0; i < servers.length; i++) { - var server = servers[i]; - - if (!(server.name in networkHistory) || networkHistory[server.name].length < 1) { - // This server hasn't been ping'd yet. Send a hacky placeholder. - client.emit('add', [[{ - error: { - description: 'Waiting' - }, - result: null, - timestamp: util.getCurrentTimeMs(), - info: { - ip: server.ip, - port: server.port, - type: server.type, - name: server.name - } - }]]); - } else { - client.emit('add', [networkHistory[networkHistoryKeys[i]]]); - } - } - - client.emit('syncComplete'); + // Send configuration data for rendering the page + client.emit('setPublicConfig', { + graphDuration: config.graphDuration, + servers: servers, + minecraftVersions: minecraftVersionNames, + isGraphVisible: config.logToDatabase }); + + // Send them our previous data, so they have somewhere to start. + client.emit('updateMojangServices', mojang.toMessage()); + + // Send each individually, this should look cleaner than waiting for one big array to transfer. + for (var i = 0; i < servers.length; i++) { + var server = servers[i]; + + if (!(server.name in networkHistory) || networkHistory[server.name].length < 1) { + // This server hasn't been ping'd yet. Send a hacky placeholder. + client.emit('add', [[{ + error: { + description: 'Waiting...', + placeholder: true + }, + result: null, + timestamp: util.getCurrentTimeMs(), + info: { + ip: server.ip, + port: server.port, + type: server.type, + name: server.name + } + }]]); + } else { + // Append the lastFavicon to the last ping entry + const serverHistory = networkHistory[server.name]; + const lastFavicon = lastFavicons[server.name]; + if (lastFavicon) { + serverHistory[serverHistory.length - 1].favicon = lastFavicon + } + client.emit('add', [serverHistory]) + } + } + + client.emit('syncComplete'); }); startMainLoop(); @@ -332,10 +368,13 @@ if (config.logToDatabase) { } (function(server) { - db.getTotalRecord(server.ip, function(record) { - logger.log('info', 'Computed total record %s (%d)', server.ip, record); + db.getTotalRecord(server.ip, function(playerCount, timestamp) { + logger.log('info', 'Computed total record %s (%d) @ %d', server.ip, playerCount, timestamp); - highestPlayerCount[server.ip] = record; + highestPlayerCount[server.ip] = { + playerCount: playerCount, + timestamp: timestamp + }; completedQueries += 1; diff --git a/assets/css/icons.css b/assets/css/icons.css new file mode 100644 index 0000000..a9b0e38 --- /dev/null +++ b/assets/css/icons.css @@ -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"; +} diff --git a/assets/css/main.css b/assets/css/main.css index 076bccc..d802598 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -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; - } -} \ No newline at end of file + .header-possible-row-break:last-of-type { + margin-bottom: 20px; + } +} diff --git a/assets/fonts/icomoon.svg b/assets/fonts/icomoon.svg new file mode 100644 index 0000000..041aa3d --- /dev/null +++ b/assets/fonts/icomoon.svg @@ -0,0 +1,20 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/fonts/icomoon.ttf b/assets/fonts/icomoon.ttf new file mode 100644 index 0000000..f8d62d2 Binary files /dev/null and b/assets/fonts/icomoon.ttf differ diff --git a/assets/fonts/icomoon.woff b/assets/fonts/icomoon.woff new file mode 100644 index 0000000..fd37104 Binary files /dev/null and b/assets/fonts/icomoon.woff differ diff --git a/assets/html/index.html b/assets/html/index.html index 4d2eb9b..6ec540c 100644 --- a/assets/html/index.html +++ b/assets/html/index.html @@ -2,98 +2,97 @@ - + - - + - + - Minetrack + - + Minetrack - + -
+ -
+
-