TS, Less -> JS, CSS

This commit is contained in:
flash 2021-05-20 17:38:25 +02:00
parent b70f0a891d
commit 8f503a6b10
22 changed files with 515 additions and 811 deletions

1
.gitattributes vendored
View file

@ -1,2 +1 @@
# Auto detect text files and perform LF normalization
* text=auto * text=auto

3
.gitignore vendored
View file

@ -1,6 +1,3 @@
node_modules/*
vendor/*
public/app.*
.apikey .apikey
[Tt]humbs.db [Tt]humbs.db
desktop.ini desktop.ini

View file

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016 Julian van de Groep <https://flash.moe> Copyright (c) 2016-2021 flashwave <me@flash.moe>
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -1,5 +1,5 @@
# now listening # now listening
A replacement for the old /now page on lastfm profiles! A replacement for the old /now page on last.fm profiles!
## Usage ## Usage
https://now.flash.moe/#/{username} https://now.flash.moe/#/{username}
@ -7,13 +7,5 @@ https://now.flash.moe/#/{username}
## Configuration ## Configuration
Create an application [here](https://www.last.fm/api/account/create), take the api and place it in a file called `.apikey` in the root (*not the public dir*). Create an application [here](https://www.last.fm/api/account/create), take the api and place it in a file called `.apikey` in the root (*not the public dir*).
## Building ## Requirements
To compile the LESS and TypeScript assets you need to have the individual compilers installed, Server side needs PHP 8.0 or newer.
both are available through yarn and can be installed with the following command:
`yarn global add less typescript`.
After that just run `build.sh`.
The server side uses a PHP script with dependencies to fetch the data without exposing the API key.
To install these dependencies you're going to need [composer](https://getcomposer.org/).
After installing composer you can simply run `composer install` in the root directory.

View file

@ -1,19 +0,0 @@
#!/bin/sh
# delete old files, using find to avoid errors
echo "=> Cleanup"
rm -v ./public/app.js
rm -v ./public/app.css
# styles
echo
echo "=> LESS"
lessc --verbose ./src/less/app.less ./public/app.css
# scripts
echo
echo "=> TypeScript"
tsc \
-p ./src/typescript/tsconfig.json \
--listFiles \
--listEmittedFiles

View file

@ -1,5 +0,0 @@
{
"require": {
"matto1990/lastfm-api": "^1.2"
}
}

70
composer.lock generated
View file

@ -1,70 +0,0 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "0447259d6060720f3c189f0fdca5c123",
"content-hash": "94d9635fa82a3525da82ed39e8471a8b",
"packages": [
{
"name": "matto1990/lastfm-api",
"version": "v1.2",
"source": {
"type": "git",
"url": "https://github.com/matto1990/PHP-Last.fm-API.git",
"reference": "1cf9a8947bf756beb876d5f8e2af398058805e08"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/matto1990/PHP-Last.fm-API/zipball/1cf9a8947bf756beb876d5f8e2af398058805e08",
"reference": "1cf9a8947bf756beb876d5f8e2af398058805e08",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"php": ">=5.3.3",
"phpunit/phpunit": "~4.8"
},
"type": "library",
"autoload": {
"psr-4": {
"LastFmApi\\": "src/lastfmapi/",
"Tests\\": "tests"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marcos",
"email": "devilcius@gmail.com"
},
{
"name": "Matt",
"email": "matt@oakes.ws"
}
],
"description": "Last.fm webservice client",
"homepage": "https://github.com/matto1990/PHP-Last.fm-API",
"keywords": [
"api",
"last.fm",
"webservice client"
],
"time": "2016-03-17 16:41:24"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}

3
public/get-xml.php Normal file
View file

@ -0,0 +1,3 @@
<?php
$format = 'xml';
require __DIR__ . '/get.php';

View file

@ -1,31 +1,148 @@
<?php <?php
use LastFmApi\Api\AuthApi; define('FNP_API_KEY', __DIR__ . '/../.apikey');
use LastFmApi\Api\UserApi; define('FNP_API_URL', 'https://ws.audioscrobbler.com');
function view($object) define('FNP_FMT_JSON', 'json');
{ define('FNP_FMT_XML', 'xml');
header('Content-Type: application/json; charset=utf-8'); define('FNP_FMTS', [
return json_encode($object, JSON_NUMERIC_CHECK); FNP_FMT_JSON => 'application/json',
FNP_FMT_XML => 'application/xml',
]);
if(!isset($format))
$format = (string)(filter_input(INPUT_GET, 'f') ?? 'json');
$pretty = !empty($_GET['p']);
if($format !== 'json' && $format !== 'xml') {
http_response_code(400);
return;
} }
if (!file_exists('../vendor/autoload.php')) { $charset = preg_match('#MSIE#i', $_SERVER['HTTP_USER_AGENT'] ?? '') ? '' : 'utf-8';
die(view(['error' => 'Please run "composer install" in the main directory first!']));
header('Content-Type: ' . FNP_FMTS[$format] . (empty($charset) ? '' : ('; charset=' . $charset)));
function errorResponse(string $text, int $code = -1): void {
global $format, $charset;
http_response_code(500);
switch($format) {
case FNP_FMT_JSON:
echo json_encode(['error' => $text, 'code' => $code]);
break;
// Has strange formatting, but XML mode exists for backwards compatibility with an unreleased variation of get.php.
case FNP_FMT_XML:
$document = new DOMDocument('1.0', $charset);
$tracks = $document->appendChild($document->createElement('Tracks'));
$track = $tracks->appendChild($document->createElement('Track'));
$track->setAttribute('error', $text);
$track->setAttribute('code', $code);
echo $document->saveXML();
break;
}
exit;
} }
require_once '../vendor/autoload.php'; if(!is_file(FNP_API_KEY))
errorResponse('API key file missing! Create a file called ".apikey" in the root directory and place your last.fm api key in there.');
if (!file_exists('../.apikey')) { $apiKey = trim(file_get_contents(FNP_API_KEY));
die(view(['error' => 'API key file missing! Create a file called ".apikey" in the root directory and place your last.fm api key in there.'])); $userName = (string)filter_input(INPUT_GET, 'u', FILTER_SANITIZE_STRING);
$curl = curl_init(FNP_API_URL . '/2.0/?method=user.getrecenttracks&format=json&limit=1&api_key=' . $apiKey . '&user=' . rawurlencode($userName));
curl_setopt_array($curl, [
CURLOPT_AUTOREFERER => false,
CURLOPT_COOKIESESSION => false,
CURLOPT_VERBOSE => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TCP_NODELAY => true,
CURLOPT_HEADER => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TCP_FASTOPEN => true,
CURLOPT_VERBOSE => false,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_TIMEOUT => 2,
CURLOPT_USERAGENT => 'NowListening v2 (' . $_SERVER['HTTP_HOST'] . ')',
]);
$response = json_decode(curl_exec($curl));
curl_close($curl);
if($response === null)
errorResponse('API request failed.');
if(isset($response->message))
errorResponse($response->message, $response->error ?? -1);
switch($format) {
case FNP_FMT_JSON:
$tracks = [];
if(!empty($response->recenttracks?->track))
foreach($response->recenttracks->track as $recentTrack) {
$tracks[] = $track = new stdClass;
$track->name = $recentTrack->name;
if(!empty($recentTrack->{"@attr"}?->nowplaying))
$track->nowplaying = true;
$track->mbid = $recentTrack->mbid;
$track->url = $recentTrack->url;
$track->date = $recentTrack->date?->uts ?? '';
$track->streamable = $recentTrack->streamable;
$track->artist = new stdClass;
$track->artist->name = $recentTrack->artist?->{"#text"} ?? '';
$track->artist->mbid = $recentTrack->artist?->mbid ?? '';
$track->album = new stdClass;
$track->album->name = $recentTrack->album?->{"#text"};
$track->album->mbid = $recentTrack->album?->mbid ?? '';
$track->images = new stdClass;
foreach($recentTrack->image as $trackImage)
$track->images->{$trackImage->size} = $trackImage->{"#text"};
}
echo json_encode($tracks, $pretty ? JSON_PRETTY_PRINT : 0);
break;
case FNP_FMT_XML:
$document = new DOMDocument('1.0', $charset);
$document->formatOutput = $pretty;
$tracks = $document->appendChild($document->createElement('Tracks'));
if(!empty($response->recenttracks?->track))
foreach($response->recenttracks->track as $recentTrack) {
$elem = $tracks->appendChild($document->createElement('Track'));
$elem->appendChild($document->createElement('Name', htmlspecialchars($recentTrack->name)));
$elem->appendChild($document->createElement('Mbid', $recentTrack->mbid));
$elem->appendChild($document->createElement('Url', htmlspecialchars($recentTrack->url)));
$elem->appendChild($document->createElement('Date', empty($recentTrack->date?->uts) ? '' : date('c', $recentTrack->date->uts)));
$elem->appendChild($document->createElement('Streamable', empty($recentTrack->streamable) ? '0' : '1'));
$elem->appendChild($document->createElement('IsPlaying', empty($recentTrack->{"@attr"}?->nowplaying) ? '0' : '1'));
$artist = $elem->appendChild($document->createElement('Artist'));
$artist->appendChild($document->createElement('Name', htmlspecialchars($recentTrack->artist?->{"#text"} ?? '')));
$artist->appendChild($document->createElement('Mbid', $recentTrack->artist?->mbid ?? ''));
$album = $elem->appendChild($document->createElement('Album'));
$album->appendChild($document->createElement('Name', htmlspecialchars($recentTrack->album?->{"#text"} ?? '')));
$album->appendChild($document->createElement('Mbid', $recentTrack->album?->mbid ?? ''));
$images = $elem->appendChild($document->createElement('Images'));
foreach($recentTrack->image as $trackImage) {
$name = $trackImage->size;
if($name === 'extralarge')
$name = 'ExtraLarge';
else
$name = ucfirst($name);
$images->appendChild($document->createElement($name, htmlspecialchars($trackImage->{"#text"})));
}
}
echo $document->saveXML();
break;
} }
$api_key = trim(file_get_contents('../.apikey'));
$auth = new AuthApi('setsession', ['apiKey' => $api_key]);
$user = new UserApi($auth);
$now = $user->getRecentTracks(['user' => (isset($_GET['u']) ? $_GET['u'] : ''), 'limit' => '1']);
if ($now === false) {
$now = ['error' => 'User not found.'];
}
echo view($now);

View file

@ -1,13 +1,13 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Now Listening</title> <title>Now Listening</title>
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no" />
<link href="https://fonts.googleapis.com/css?family=Exo+2:400,400italic,200,200italic" rel="stylesheet" type="text/css"> <link href="https://fonts.googleapis.com/css?family=Exo+2:400,400italic,200,200italic" rel="stylesheet" type="text/css" />
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" type="text/css" />
<link href="/app.css" type="text/css" rel="stylesheet"> <link href="/now.css" type="text/css" rel="stylesheet" />
</head> </head>
<body> <body>
<div class="background" id="background"></div> <div class="background" id="background"></div>
@ -40,6 +40,6 @@
</div> </div>
</div> </div>
</div> </div>
<script src="/app.js" type="text/javascript"></script> <script src="/now.js" type="text/javascript"></script>
</body> </body>
</html> </html>

176
public/now.css Normal file
View file

@ -0,0 +1,176 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
width: 100%;
}
a {
color: inherit;
text-decoration: none;
}
.hidden {
display: none !important;
}
.container {
background: url('resources/grid.png') transparent;
height: 100%;
width: 100%;
color: #fff;
font: 12px/20px 'Exo 2', sans-serif;
display: flex;
align-items: center;
justify-content: center;
}
@media (max-width: 600px) {
.container__index,
.container__user {
width: 100%;
height: 100%;
}
}
.container__index {
text-align: center;
background: rgba(17, 17, 17, 0.8);
box-shadow: 0 1px 4px #111;
padding: 6px 10px;
display: flex;
flex-flow: column;
justify-content: center;
}
.container__user {
display: flex;
flex-flow: row;
box-shadow: 0 1px 4px #111;
}
.background {
background: none no-repeat center / cover #000;
z-index: -1;
position: absolute;
height: 100%;
width: 100%;
transition: background-color 2.1s;
}
.index__title {
font-weight: 200;
font-style: italic;
font-size: 3em;
line-height: 1.5em;
}
.index__form {
margin: 5px;
display: flex;
justify-content: center;
box-shadow: 0 1px 4px #111;
}
.index__username,
.index__submit {
border: 1px solid #111;
color: #fff;
padding: 2px;
font-size: 1.5em;
height: 30px;
}
.index__username {
background: rgba(17, 17, 17, 0.5);
border-right: 0;
font-family: 'Exo 2', sans-serif;
width: 100%;
}
.index__submit {
border-left: 0;
background: #111;
width: 30px;
}
.index__dev {
font-size: 1.1em;
font-weight: 200;
}
.cover {
flex-shrink: 0;
flex-grow: 0;
}
@media (max-width: 600px) {
.cover {
display: none;
}
}
.cover__image {
background: none no-repeat center / cover rgba(17, 17, 17, 0.8);
width: 300px;
height: 300px;
}
.info {
flex-grow: 1;
flex-shrink: 0;
background: rgba(17, 17, 17, 0.8);
padding: 6px 8px;
display: flex;
flex-flow: column;
}
@media (min-width: 601px) {
.info {
width: 300px;
height: 300px;
}
}
.info__title {
font-size: 3em;
line-height: 1.2em;
font-style: italic;
font-weight: 200;
}
.info__artist {
font-size: 1.5em;
line-height: 1.2em;
font-weight: 200;
}
.info__flags {
line-height: 1.5em;
font-size: 2em;
flex-grow: 1;
}
.info__user {
align-self: end;
display: inline-flex;
width: 100%;
}
.info__user-back {
cursor: pointer;
}
.info__user-name {
flex-grow: 1;
text-align: right;
}

184
public/now.js Normal file
View file

@ -0,0 +1,184 @@
var fnp = {
defaultCoverFnp: '/resources/no-cover.png',
defaultCoverLfm: 'https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png',
watch: {
userName: null,
interval: 0,
},
ui: {
mode: '',
},
};
fnp.goIndex = function() {
location.hash = '';
};
fnp.switchUser = function(userName) {
location.hash = '#/' + encodeURIComponent((userName ?? '').toString());
};
fnp.updateHash = function() {
var userName = decodeURIComponent(location.hash.substring(2));
if(userName.length > 0) {
this.watch.start(userName, function(response) {
if(response.error) {
this.goIndex();
alert(response.error);
return;
}
var cover = response[0].images.extralarge;
console.log(cover);
if(cover.length < 1 || cover === this.defaultCoverLfm)
cover = this.defaultCoverFnp;
console.log(cover);
this.ui.setInfo(cover, response[0].name, response[0].artist.name, userName, response[0].nowplaying || false);
this.ui.goUser();
}.bind(this));
} else {
this.watch.stop();
this.ui.goIndex();
this.ui.setBackgroundFade();
}
};
fnp.watch.start = function(userName, callback) {
this.stop();
this.userName = userName = (userName || '').toString();
this.fetch(function(response) {
if(response.error)
this.stop();
callback(response);
}.bind(this));
};
fnp.watch.stop = function() {
if(this.interval !== 0) {
clearInterval(this.interval);
this.interval = 0;
}
this.userName = null;
};
fnp.watch.fetch = function(callback) {
if(typeof callback !== 'function')
return;
var xhr = new XMLHttpRequest;
xhr.onreadystatechange = function() {
if(xhr.readyState !== 4)
return;
callback(JSON.parse(xhr.responseText));
};
xhr.open('GET', '/get.php?u=' + encodeURIComponent(this.userName));
xhr.send();
};
fnp.ui.setMode = function(modeName) {
modeName = (modeName || '').toString();
if(this.mode === modeName)
return;
this.mode = modeName;
for(var i = 0; i < this.modes.length; ++i) {
var mode = this.modes[i];
mode.elem.classList[mode.name === modeName ? 'remove' : 'add']('hidden');
}
};
fnp.ui.goIndex = function() {
this.setMode('index');
};
fnp.ui.goUser = function() {
this.setMode('user');
};
fnp.ui.setBackground = function(bgPath) {
if(bgPath === '::fade') {
this.setBackgroundFade();
return;
}
if(this.iColourFade !== 0) {
clearInterval(this.iColourFade);
this.iColourFade = 0;
}
this.eBg.style.backgroundColor = null;
this.eBg.style.backgroundImage = 'url(\'' + (bgPath || '').toString() + '\')';
};
fnp.ui.setBackgroundFade = function() {
if(this.iColourFade !== 0)
return;
this.eBg.style.backgroundImage = null;
var fader = function() {
var colour = Math.floor(Math.random() * 0xFFFFFF).toString(16);
if(colour.length !== 6 && colour.length !== 3)
colour = '000000'.substring(colour.length) + colour;
this.eBg.style.backgroundColor = '#' + colour;
}.bind(this);
this.iColourFade = setInterval(fader, 2000);
fader();
};
fnp.ui.setInfo = function(cover, title, artist, user, now) {
this.setBackground(cover);
this.eInfoCover.style.backgroundImage = 'url(\'' + (cover || '').toString() + '\')';
this.eInfoTitle.textContent = (title || '').toString();
this.eInfoArtist.textContent = (artist || '').toString();
this.eInfoUser.textContent = (user || '').toString();
this.eInfoFlags.innerHTML = '';
if(now) {
var nowIcon = document.createElement('span');
nowIcon.className = 'fa fa-music';
nowIcon.title = 'Now playing';
this.eInfoFlags.appendChild(nowIcon);
}
};
window.onload = function() {
fnp.ui.eIndex = document.getElementById('index');
fnp.ui.eUser = document.getElementById('user');
fnp.ui.eBg = document.getElementById('background');
fnp.ui.eFormUser = document.getElementById('username');
fnp.ui.eFormSend = document.getElementById('submit');
fnp.ui.eInfoCover = document.getElementById('np_cover');
fnp.ui.eInfoTitle = document.getElementById('np_title');
fnp.ui.eInfoArtist = document.getElementById('np_artist');
fnp.ui.eInfoUser = document.getElementById('np_user');
fnp.ui.eInfoBack = document.getElementById('np_back');
fnp.ui.eInfoFlags = document.getElementById('np_flags');
fnp.ui.iColourFade = 0;
fnp.ui.modes = [
{ name: 'index', elem: fnp.ui.eIndex },
{ name: 'user', elem: fnp.ui.eUser },
];
fnp.ui.eInfoBack.onclick = function() {
fnp.goIndex();
};
window.onhashchange = function() {
fnp.updateHash();
};
fnp.ui.eFormSend.onclick = function() {
fnp.switchUser(fnp.ui.eFormUser.value);
};
fnp.ui.eFormUser.onkeydown = function(ev) {
if(typeof ev.key === 'undefined')
ev.key = ev.keyCode === 13 ? 'Enter' : '?';
if(ev.key === 'Enter' || ev.key === 'NumpadEnter')
fnp.switchUser(fnp.ui.eFormUser.value);
};
fnp.updateHash();
};

View file

@ -1,174 +0,0 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
width: 100%;
}
a {
color: inherit;
text-decoration: none;
}
.hidden {
display: none !important;
}
.container {
background: url('resources/grid.png') transparent;
height: 100%;
width: 100%;
color: #fff;
font: 12px/20px "Exo 2", sans-serif;
display: flex;
align-items: center;
justify-content: center;
@media (max-width: 600px) {
&__index,
&__user {
width: 100%;
height: 100%;
}
}
&__index {
text-align: center;
background: fade(#111, 80%);
box-shadow: 0 1px 4px #111;
padding: 6px 10px;
display: flex;
flex-flow: column;
justify-content: center;
}
&__user {
display: flex;
flex-flow: row;
box-shadow: 0 1px 4px #111;
}
}
.background {
background: none no-repeat center / cover #000;
z-index: -1;
position: absolute;
height: 100%;
width: 100%;
transition: background-color 2.1s;
}
.index {
&__title {
font-weight: 200;
font-style: italic;
font-size: 3em;
line-height: 1.5em;
}
&__form {
margin: 5px;
display: flex;
justify-content: center;
box-shadow: 0 1px 4px #111;
}
&__username,
&__submit {
border: 1px solid #111;
color: #fff;
padding: 2px;
font-size: 1.5em;
height: 30px;
}
&__username {
background: fade(#111, 50%);
border-right: 0;
font-family: "Exo 2", sans-serif;
width: 100%;
}
&__submit {
border-left: 0;
background: #111;
width: 30px;
}
&__dev {
font-size: 1.1em;
font-weight: 200;
}
}
.cover {
flex-shrink: 0;
flex-grow: 0;
@media (max-width: 600px) {
display: none;
}
&__image {
background: none no-repeat center / cover fade(#111, 80%);
width: 300px;
height: 300px;
}
}
.info {
flex-grow: 1;
flex-shrink: 0;
background: fade(#111, 80%);
padding: 6px 8px;
display: flex;
flex-flow: column;
@media (min-width: 601px) {
width: 300px;
height: 300px;
}
&__title {
font-size: 3em;
line-height: 1.2em;
font-style: italic;
font-weight: 200;
}
&__artist {
font-size: 1.5em;
line-height: 1.2em;
font-weight: 200;
}
&__flags {
line-height: 1.5em;
font-size: 2em;
flex-grow: 1; // fill space
}
&__user {
align-self: end;
display: inline-flex;
width: 100%;
&-back {
cursor: pointer;
}
&-name {
flex-grow: 1;
text-align: right;
}
}
}

View file

@ -1,137 +0,0 @@
namespace NP
{
export class AJAX
{
// XMLHTTPRequest container
private Request: XMLHttpRequest;
private Callbacks: any;
private Headers: any;
private URL: string;
private Send: string = null;
private Asynchronous: boolean = true;
// Prepares the XMLHttpRequest and stuff
constructor(async: boolean = true) {
this.Request = new XMLHttpRequest();
this.Callbacks = new Object();
this.Headers = new Object();
this.Asynchronous = async;
}
// Start
public Start(method: HTTPMethod, avoidCache: boolean = false): void {
// Open the connection
this.Request.open(HTTPMethod[method], this.URL + (avoidCache ? "?no-cache=" + Date.now() : ""), this.Asynchronous);
// Set headers
this.PrepareHeaders();
// Watch the ready state
this.Request.onreadystatechange = () => {
// Only invoke when complete
if (this.Request.readyState === 4) {
// Check if a callback if present
if ((typeof this.Callbacks[this.Request.status]).toLowerCase() === 'function') {
this.Callbacks[this.Request.status]();
} else { // Else check if there's a generic fallback present
if ((typeof this.Callbacks['0']).toLowerCase() === 'function') {
// Call that
this.Callbacks['0']();
}
}
}
}
this.Request.send(this.Send);
}
// Stop
public Stop(): void {
this.Request = null;
}
// Add post data
public SetSend(data: any): void {
// Storage array
var store: Array<string> = new Array<string>();
// Iterate over the object and them in the array with an equals sign inbetween
for (var item in data) {
store.push(encodeURIComponent(item) + "=" + encodeURIComponent(data[item]));
}
// Assign to send
this.Send = store.join('&');
}
// Set raw post
public SetRawSend(data: string): void {
this.Send = data;
}
// Get response
public Response(): string {
return this.Request.responseText;
}
// Get all headers
public ResponseHeaders(): string {
return this.Request.getAllResponseHeaders();
}
// Get a header
public ResponseHeader(name: string): string {
return this.Request.getResponseHeader(name);
}
// Set charset
public ContentType(type: string, charset: string = null): void {
this.AddHeader('Content-Type', type + ';charset=' + (charset ? charset : 'utf-8'));
}
// Add a header
public AddHeader(name: string, value: string): void {
// Attempt to remove a previous instance
this.RemoveHeader(name);
// Add the new header
this.Headers[name] = value;
}
// Remove a header
public RemoveHeader(name: string): void {
if ((typeof this.Headers[name]).toLowerCase() !== 'undefined') {
delete this.Headers[name];
}
}
// Prepare Request headers
public PrepareHeaders(): void {
for (var header in this.Headers) {
this.Request.setRequestHeader(header, this.Headers[header]);
}
}
// Adds a callback
public AddCallback(status: number, callback: Function): void {
// Attempt to remove previous instances
this.RemoveCallback(status);
// Add the new callback
this.Callbacks[status] = callback;
}
// Delete a callback
public RemoveCallback(status: number): void {
// Delete the callback if present
if ((typeof this.Callbacks[status]).toLowerCase() === 'function') {
delete this.Callbacks[status];
}
}
// Sets the URL
public SetUrl(url: string): void {
this.URL = url;
}
}
}

View file

@ -1,8 +0,0 @@
namespace NP
{
export enum Background
{
IMAGE,
COLOURFADE
}
}

View file

@ -1,134 +0,0 @@
namespace NP
{
export class DOM
{
// Block Element Modifier class name generator
public static BEM(block: string, element: string = null, modifiers: string[] = [], firstModifierOnly: boolean = false): string
{
var className: string = "";
if (firstModifierOnly && modifiers.length === 0) {
return null;
}
className += block;
if (element !== null) {
className += "__" + element;
}
var baseName: string = className;
for (var _i in modifiers) {
if (firstModifierOnly) {
return baseName + "--" + modifiers[_i];
}
className += " " + baseName + "--" + modifiers[_i];
}
return className;
}
// Shorthand for creating an element with a class and string
public static Element(name: string, className: string = null, id: string = null): HTMLElement {
var element = document.createElement(name);
if (className !== null) {
element.className = className;
}
if (id !== null) {
element.id = id;
}
return element;
}
// Shorthand for textnode
public static Text(text: string): Text {
return document.createTextNode(text);
}
// Shorthand for getElementById (i'm lazy)
public static ID(id: string): HTMLElement {
return document.getElementById(id);
}
// Shorthand for removing an element
public static Remove(element: HTMLElement): void {
element.parentNode.removeChild(element);
}
// Shorthand for the first element of getElementsByClassName
public static Class(className: string): NodeListOf<Element> {
return <NodeListOf<Element>>document.getElementsByClassName(className);
}
// Shorthand for prepending
public static Prepend(target: HTMLElement, element: HTMLElement | Text): void {
if (target.children.length) {
target.insertBefore(element, target.firstChild);
} else {
this.Append(target, element);
}
}
// Shorthand for appending
public static Append(target: HTMLElement, element: HTMLElement | Text): void {
target.appendChild(element);
}
// Getting all classes of an element
public static ClassNames(target: HTMLElement): string[] {
var className: string = target.className,
classes: string[] = [];
if (className.length > 1) {
classes = className.split(' ');
}
return classes;
}
// Adding classes to an element
public static AddClass(target: HTMLElement, classes: string[]): void {
for (var _i in classes) {
var current: string[] = this.ClassNames(target),
index: number = current.indexOf(classes[_i]);
if (index >= 0) {
continue;
}
current.push(classes[_i]);
target.className = current.join(' ');
}
}
// Removing classes
public static RemoveClass(target: HTMLElement, classes: string[]): void {
for (var _i in classes) {
var current: string[] = this.ClassNames(target),
index: number = current.indexOf(classes[_i]);
if (index < 0) {
continue;
}
current.splice(index, 1);
target.className = current.join(' ');
}
}
public static Clone(subject: HTMLElement): HTMLElement {
return (<HTMLElement>subject.cloneNode(true));
}
public static Query(query: string): NodeListOf<Element> {
return document.querySelectorAll(query);
}
}
}

View file

@ -1,11 +0,0 @@
namespace NP
{
export enum HTTPMethod
{
GET,
HEAD,
POST,
PUT,
DELETE
}
}

View file

@ -1,4 +0,0 @@
window.addEventListener("load", () => {
NP.UI.RegisterHooks();
NP.UI.Update();
});

View file

@ -1,8 +0,0 @@
namespace NP
{
export enum Mode
{
INDEX,
USER
}
}

View file

@ -1,127 +0,0 @@
namespace NP
{
export class UI
{
private static IndexElem: HTMLDivElement = <HTMLDivElement>DOM.ID('index');
private static UserElem: HTMLDivElement = <HTMLDivElement>DOM.ID('user');
private static BGElem: HTMLDivElement = <HTMLDivElement>DOM.ID('background');
private static FormUsername: HTMLInputElement = <HTMLInputElement>DOM.ID('username');
private static FormSubmit: HTMLButtonElement = <HTMLButtonElement>DOM.ID('submit');
private static InfoCover: HTMLDivElement = <HTMLDivElement>DOM.ID('np_cover');
private static InfoTitle: HTMLDivElement = <HTMLDivElement>DOM.ID('np_title');
private static InfoArtist: HTMLDivElement = <HTMLDivElement>DOM.ID('np_artist');
private static InfoUser: HTMLDivElement = <HTMLDivElement>DOM.ID('np_user');
private static InfoBack: HTMLDivElement = <HTMLDivElement>DOM.ID('np_back');
private static InfoFlags: HTMLDivElement = <HTMLDivElement>DOM.ID('np_flags');
private static ColourFadeInterval: number = null;
public static Mode(mode: Mode): void {
switch (mode) {
case Mode.INDEX:
DOM.AddClass(this.UserElem, ['hidden']);
DOM.RemoveClass(this.IndexElem, ['hidden']);
break;
case Mode.USER:
DOM.AddClass(this.IndexElem, ['hidden']);
DOM.RemoveClass(this.UserElem, ['hidden']);
break;
}
}
public static Background(mode: Background, property: string = null): void {
switch (mode) {
case Background.COLOURFADE:
if (this.ColourFadeInterval !== null) {
break;
}
this.BGElem.style.backgroundImage = null;
var fader: Function = () => {
var colour: string = Math.floor(Math.random() * 16777215).toString(16);
if (colour.length !== 6 && colour.length !== 3) {
colour = "000000".substring(colour.length) + colour;
}
UI.BGElem.style.backgroundColor = "#" + colour;
};
this.ColourFadeInterval = setInterval(fader, 2000);
fader.call(this);
break;
case Background.IMAGE:
if (property === null) {
break;
}
if (this.ColourFadeInterval !== null) {
clearInterval(this.ColourFadeInterval);
this.ColourFadeInterval = null;
}
this.BGElem.style.backgroundColor = null;
this.BGElem.style.backgroundImage = "url('{0}')".replace('{0}', property);
break;
}
}
public static SetInfo(cover: string, title: string, artist: string, user: string, now: boolean = false): void {
this.Background(Background.IMAGE, cover);
this.InfoCover.style.backgroundImage = "url('{0}')".replace('{0}', cover);
this.InfoTitle.innerText = title;
this.InfoArtist.innerText = artist;
this.InfoUser.innerText = user;
this.InfoFlags.innerHTML = '';
if (now) {
var nowIcon: HTMLSpanElement = DOM.Element('span', 'fa fa-music');
nowIcon.title = 'Now playing';
this.InfoFlags.appendChild(nowIcon);
}
}
public static Update(): void {
var user: string = location.hash.substring(2);
if (user.length > 0) {
Watcher.Start(user);
UI.Mode(Mode.USER);
} else {
Watcher.Stop();
UI.Mode(Mode.INDEX);
UI.Background(Background.COLOURFADE);
}
}
public static RegisterHooks(): void {
UI.InfoBack.addEventListener('click', () => {
location.hash = '';
});
var enter: Function = () => {
location.hash = '#/' + UI.FormUsername.value;
};
window.addEventListener('hashchange', () => {
UI.Update();
});
UI.FormSubmit.addEventListener('click', () => {
enter.call(this);
});
UI.FormUsername.addEventListener('keydown', (ev) => {
if (ev.keyCode === 13) {
enter.call(this);
}
});
}
}
}

View file

@ -1,56 +0,0 @@
namespace NP
{
export class Watcher
{
private static Fetcher: AJAX = null;
private static CheckIntervalId: number = null;
private static CheckTimeout: number = 15 * 1000;
private static GetPath: string = "/get.php";
private static User: string = null;
public static Start(user: string): void {
if (this.Fetcher !== null) {
this.Stop();
}
this.User = user;
this.Fetcher = new AJAX;
this.Fetcher.AddCallback(200, () => {
var data: any = JSON.parse(Watcher.Fetcher.Response());
console.log(data);
if ((typeof data.error).toLowerCase() !== 'undefined') {
Watcher.Stop();
UI.Mode(Mode.INDEX);
NP.UI.Background(NP.Background.COLOURFADE);
alert(data.error);
return;
}
UI.Mode(Mode.USER);
var image: string = data[0].images.large.length < 1 ? '/resources/no-cover.png' : data[0].images.large.replace('/174s/', '/300s/'),
now: boolean = (typeof data[0].nowplaying).toLowerCase() === 'undefined' ? false : data[0].nowplaying;
UI.SetInfo(image, data[0].name, data[0].artist.name, this.User, now);
});
this.Check();
this.CheckIntervalId = setInterval(this.Check, this.CheckTimeout);
}
public static Stop(): void {
clearInterval(this.CheckIntervalId);
this.CheckIntervalId = null;
this.Fetcher = null;
this.User = null;
}
public static Check(): void {
Watcher.Fetcher.SetUrl(Watcher.GetPath + "?u=" + Watcher.User);
Watcher.Fetcher.Start(HTTPMethod.GET);
}
}
}

View file

@ -1,11 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"noImplicitAny": false,
"removeComments": true,
"outFile": "../../public/app.js"
},
"filesGlob": [
"**/*.ts"
]
}