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

3
.gitignore vendored
View file

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

View file

@ -1,6 +1,6 @@
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
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,5 +1,5 @@
# now listening
A replacement for the old /now page on lastfm profiles!
A replacement for the old /now page on last.fm profiles!
## Usage
https://now.flash.moe/#/{username}
@ -7,13 +7,5 @@ https://now.flash.moe/#/{username}
## 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*).
## Building
To compile the LESS and TypeScript assets you need to have the individual compilers installed,
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.
## Requirements
Server side needs PHP 8.0 or newer.

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
use LastFmApi\Api\AuthApi;
use LastFmApi\Api\UserApi;
define('FNP_API_KEY', __DIR__ . '/../.apikey');
define('FNP_API_URL', 'https://ws.audioscrobbler.com');
function view($object)
{
header('Content-Type: application/json; charset=utf-8');
return json_encode($object, JSON_NUMERIC_CHECK);
define('FNP_FMT_JSON', 'json');
define('FNP_FMT_XML', 'xml');
define('FNP_FMTS', [
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')) {
die(view(['error' => 'Please run "composer install" in the main directory first!']));
$charset = preg_match('#MSIE#i', $_SERVER['HTTP_USER_AGENT'] ?? '') ? '' : 'utf-8';
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')) {
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.']));
$apiKey = trim(file_get_contents(FNP_API_KEY));
$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>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Now Listening</title>
<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://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">
<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://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" type="text/css" />
<link href="/now.css" type="text/css" rel="stylesheet" />
</head>
<body>
<div class="background" id="background"></div>
@ -40,6 +40,6 @@
</div>
</div>
</div>
<script src="/app.js" type="text/javascript"></script>
<script src="/now.js" type="text/javascript"></script>
</body>
</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"
]
}