big update with fixes and playlist/queuing capabilities #20
|
@ -316,6 +316,8 @@ private void GenerateSeriesDetailsFragment(Series series) {
|
||||||
data-sslug='{series.SlugEncoded}'
|
data-sslug='{series.SlugEncoded}'
|
||||||
data-cover='/cover/sm/{series.SlugEncoded}.jpg'
|
data-cover='/cover/sm/{series.SlugEncoded}.jpg'
|
||||||
data-series='{series.TitleEncoded}'
|
data-series='{series.TitleEncoded}'
|
||||||
|
data-length='{episode.LengthDisplay}'
|
||||||
|
data-size='{episode.FileSize}'
|
||||||
data-file='{episode.FileNameEncoded}'>
|
data-file='{episode.FileNameEncoded}'>
|
||||||
<label>{episode.TitleEncoded}</label>
|
<label>{episode.TitleEncoded}</label>
|
||||||
<aside>
|
<aside>
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">
|
||||||
|
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 332 B |
216
site/index.html
216
site/index.html
|
@ -96,222 +96,6 @@
|
||||||
<h2>Playlist</h2>
|
<h2>Playlist</h2>
|
||||||
<h3>Up Next:</h3>
|
<h3>Up Next:</h3>
|
||||||
<ol>
|
<ol>
|
||||||
<li class='episode' title='Guest - Madame Lazonga'
|
|
||||||
data-slug='1940-07-31'
|
|
||||||
data-sslug='abbott-and-costello'
|
|
||||||
data-cover='/cover/sm/abbott-and-costello.jpg'
|
|
||||||
data-series='Abbott and Costello'
|
|
||||||
data-file='otr/abbott-and-costello/1940-07-31.mp3'>
|
|
||||||
<label>Guest - Madame Lazonga</label>
|
|
||||||
<aside>
|
|
||||||
<span>Abbot and Costello</span>
|
|
||||||
<span>29mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='Bank Robbery with Marlene Dietrich'
|
|
||||||
data-slug='1942-10-15'
|
|
||||||
data-sslug='abbott-and-costello'
|
|
||||||
data-cover='/cover/sm/abbott-and-costello.jpg'
|
|
||||||
data-series='Abbott and Costello'
|
|
||||||
data-file='otr/abbott-and-costello/1942-10-15.mp3'>
|
|
||||||
<label>Bank Robbery with Marlene Dietrich</label>
|
|
||||||
<aside>
|
|
||||||
<span>Abbot and Costello</span>
|
|
||||||
<span>30mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='Morgan Accused of Stealing an Aztec Necklace'
|
|
||||||
data-slug='00001'
|
|
||||||
data-sslug='afloat-with-henry-morgan'
|
|
||||||
data-cover='/cover/sm/afloat-with-henry-morgan.jpg'
|
|
||||||
data-series='Afloat with Henry Morgan'
|
|
||||||
data-file='otr/afloat-with-henry-morgan/00001.mp3'>
|
|
||||||
<label>Morgan Accused of Stealing an Aztec Necklace</label>
|
|
||||||
<aside>
|
|
||||||
<span>Afloat with Henry Morgan</span>
|
|
||||||
<span>12mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='Kitty Wants The Necklace'
|
|
||||||
data-slug='00002'
|
|
||||||
data-sslug='afloat-with-henry-morgan'
|
|
||||||
data-cover='/cover/sm/afloat-with-henry-morgan.jpg'
|
|
||||||
data-series='Afloat with Henry Morgan'
|
|
||||||
data-file='otr/afloat-with-henry-morgan/00002.mp3'>
|
|
||||||
<label>Kitty Wants The Necklace</label>
|
|
||||||
<aside>
|
|
||||||
<span>Afloat with Henry Morgan</span>
|
|
||||||
<span>12mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='World Series Game 2 - Yankees at Cubs - John Harrington Pat Flannagan'
|
|
||||||
data-slug='1938-10-06'
|
|
||||||
data-sslug='classic-baseball-mlb'
|
|
||||||
data-cover='/cover/sm/classic-baseball-mlb.jpg'
|
|
||||||
data-series='Classic Baseball MLB'
|
|
||||||
data-file='otr/classic-baseball-mlb/1938-10-06.mp3'>
|
|
||||||
<label>World Series Game 2 - Yankees at Cubs - John Harrington Pat Flannagan</label>
|
|
||||||
<aside>
|
|
||||||
<span>This is a Ridiculously Long Series Name</span>
|
|
||||||
<span>141mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='World Series Game 4 - Cubs at Yankees'
|
|
||||||
data-slug='1938-10-09'
|
|
||||||
data-sslug='classic-baseball-mlb'
|
|
||||||
data-cover='/cover/sm/classic-baseball-mlb.jpg'
|
|
||||||
data-series='Classic Baseball MLB'
|
|
||||||
data-file='otr/classic-baseball-mlb/1938-10-09.mp3'>
|
|
||||||
<label>World Series Game 4 - Cubs at Yankees</label>
|
|
||||||
<aside>
|
|
||||||
<span>This is a Ridiculously Long Series Name</span>
|
|
||||||
<span>136mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='Guest - Madame Lazonga'
|
|
||||||
data-slug='1940-07-31'
|
|
||||||
data-sslug='abbott-and-costello'
|
|
||||||
data-cover='/cover/sm/abbott-and-costello.jpg'
|
|
||||||
data-series='Abbott and Costello'
|
|
||||||
data-file='otr/abbott-and-costello/1940-07-31.mp3'>
|
|
||||||
<label>Guest - Madame Lazonga</label>
|
|
||||||
<aside>
|
|
||||||
<span>Abbot and Costello</span>
|
|
||||||
<span>29mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='Bank Robbery with Marlene Dietrich'
|
|
||||||
data-slug='1942-10-15'
|
|
||||||
data-sslug='abbott-and-costello'
|
|
||||||
data-cover='/cover/sm/abbott-and-costello.jpg'
|
|
||||||
data-series='Abbott and Costello'
|
|
||||||
data-file='otr/abbott-and-costello/1942-10-15.mp3'>
|
|
||||||
<label>Bank Robbery with Marlene Dietrich</label>
|
|
||||||
<aside>
|
|
||||||
<span>Abbot and Costello</span>
|
|
||||||
<span>30mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='Morgan Accused of Stealing an Aztec Necklace'
|
|
||||||
data-slug='00001'
|
|
||||||
data-sslug='afloat-with-henry-morgan'
|
|
||||||
data-cover='/cover/sm/afloat-with-henry-morgan.jpg'
|
|
||||||
data-series='Afloat with Henry Morgan'
|
|
||||||
data-file='otr/afloat-with-henry-morgan/00001.mp3'>
|
|
||||||
<label>Morgan Accused of Stealing an Aztec Necklace</label>
|
|
||||||
<aside>
|
|
||||||
<span>Afloat with Henry Morgan</span>
|
|
||||||
<span>12mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='Kitty Wants The Necklace'
|
|
||||||
data-slug='00002'
|
|
||||||
data-sslug='afloat-with-henry-morgan'
|
|
||||||
data-cover='/cover/sm/afloat-with-henry-morgan.jpg'
|
|
||||||
data-series='Afloat with Henry Morgan'
|
|
||||||
data-file='otr/afloat-with-henry-morgan/00002.mp3'>
|
|
||||||
<label>Kitty Wants The Necklace</label>
|
|
||||||
<aside>
|
|
||||||
<span>Afloat with Henry Morgan</span>
|
|
||||||
<span>12mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='World Series Game 2 - Yankees at Cubs - John Harrington Pat Flannagan'
|
|
||||||
data-slug='1938-10-06'
|
|
||||||
data-sslug='classic-baseball-mlb'
|
|
||||||
data-cover='/cover/sm/classic-baseball-mlb.jpg'
|
|
||||||
data-series='Classic Baseball MLB'
|
|
||||||
data-file='otr/classic-baseball-mlb/1938-10-06.mp3'>
|
|
||||||
<label>World Series Game 2 - Yankees at Cubs - John Harrington Pat Flannagan</label>
|
|
||||||
<aside>
|
|
||||||
<span>This is a Ridiculously Long Series Name</span>
|
|
||||||
<span>141mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='World Series Game 4 - Cubs at Yankees'
|
|
||||||
data-slug='1938-10-09'
|
|
||||||
data-sslug='classic-baseball-mlb'
|
|
||||||
data-cover='/cover/sm/classic-baseball-mlb.jpg'
|
|
||||||
data-series='Classic Baseball MLB'
|
|
||||||
data-file='otr/classic-baseball-mlb/1938-10-09.mp3'>
|
|
||||||
<label>World Series Game 4 - Cubs at Yankees</label>
|
|
||||||
<aside>
|
|
||||||
<span>This is a Ridiculously Long Series Name</span>
|
|
||||||
<span>136mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='Guest - Madame Lazonga'
|
|
||||||
data-slug='1940-07-31'
|
|
||||||
data-sslug='abbott-and-costello'
|
|
||||||
data-cover='/cover/sm/abbott-and-costello.jpg'
|
|
||||||
data-series='Abbott and Costello'
|
|
||||||
data-file='otr/abbott-and-costello/1940-07-31.mp3'>
|
|
||||||
<label>Guest - Madame Lazonga</label>
|
|
||||||
<aside>
|
|
||||||
<span>Abbot and Costello</span>
|
|
||||||
<span>29mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='Bank Robbery with Marlene Dietrich'
|
|
||||||
data-slug='1942-10-15'
|
|
||||||
data-sslug='abbott-and-costello'
|
|
||||||
data-cover='/cover/sm/abbott-and-costello.jpg'
|
|
||||||
data-series='Abbott and Costello'
|
|
||||||
data-file='otr/abbott-and-costello/1942-10-15.mp3'>
|
|
||||||
<label>Bank Robbery with Marlene Dietrich</label>
|
|
||||||
<aside>
|
|
||||||
<span>Abbot and Costello</span>
|
|
||||||
<span>30mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='Morgan Accused of Stealing an Aztec Necklace'
|
|
||||||
data-slug='00001'
|
|
||||||
data-sslug='afloat-with-henry-morgan'
|
|
||||||
data-cover='/cover/sm/afloat-with-henry-morgan.jpg'
|
|
||||||
data-series='Afloat with Henry Morgan'
|
|
||||||
data-file='otr/afloat-with-henry-morgan/00001.mp3'>
|
|
||||||
<label>Morgan Accused of Stealing an Aztec Necklace</label>
|
|
||||||
<aside>
|
|
||||||
<span>Afloat with Henry Morgan</span>
|
|
||||||
<span>12mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='Kitty Wants The Necklace'
|
|
||||||
data-slug='00002'
|
|
||||||
data-sslug='afloat-with-henry-morgan'
|
|
||||||
data-cover='/cover/sm/afloat-with-henry-morgan.jpg'
|
|
||||||
data-series='Afloat with Henry Morgan'
|
|
||||||
data-file='otr/afloat-with-henry-morgan/00002.mp3'>
|
|
||||||
<label>Kitty Wants The Necklace</label>
|
|
||||||
<aside>
|
|
||||||
<span>Afloat with Henry Morgan</span>
|
|
||||||
<span>12mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='World Series Game 2 - Yankees at Cubs - John Harrington Pat Flannagan'
|
|
||||||
data-slug='1938-10-06'
|
|
||||||
data-sslug='classic-baseball-mlb'
|
|
||||||
data-cover='/cover/sm/classic-baseball-mlb.jpg'
|
|
||||||
data-series='Classic Baseball MLB'
|
|
||||||
data-file='otr/classic-baseball-mlb/1938-10-06.mp3'>
|
|
||||||
<label>World Series Game 2 - Yankees at Cubs - John Harrington Pat Flannagan</label>
|
|
||||||
<aside>
|
|
||||||
<span>This is a Ridiculously Long Series Name</span>
|
|
||||||
<span>141mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
<li class='episode' title='World Series Game 4 - Cubs at Yankees'
|
|
||||||
data-slug='1938-10-09'
|
|
||||||
data-sslug='classic-baseball-mlb'
|
|
||||||
data-cover='/cover/sm/classic-baseball-mlb.jpg'
|
|
||||||
data-series='Classic Baseball MLB'
|
|
||||||
data-file='otr/classic-baseball-mlb/1938-10-09.mp3'>
|
|
||||||
<label>World Series Game 4 - Cubs at Yankees</label>
|
|
||||||
<aside>
|
|
||||||
<span>This is a Ridiculously Long Series Name</span>
|
|
||||||
<span>136mins</span>
|
|
||||||
</aside>
|
|
||||||
</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
|
|
|
@ -39,20 +39,21 @@ var Player = /** @class */ (function () {
|
||||||
function Player(playlist) {
|
function Player(playlist) {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
this.playlist = playlist;
|
this.playlist = playlist;
|
||||||
var controls = document.getElementById('controls');
|
this.playlist.setPlayEpisodeHandler(function (episode) { return _this.playEpisode(episode); });
|
||||||
var timeVolume = document.getElementById('timeVolume');
|
var controls = getOrThrow(document.getElementById('controls'));
|
||||||
this.nowPlaying = document.getElementById('nowPlaying');
|
var timeVolume = getOrThrow(document.getElementById('timeVolume'));
|
||||||
this.rewButton = controls.getElementsByTagName('button').item(0);
|
this.nowPlaying = getOrThrow(document.getElementById('nowPlaying'));
|
||||||
this.playButton = controls.getElementsByTagName('button').item(1);
|
this.rewButton = getOrThrow(controls.getElementsByTagName('button').item(0));
|
||||||
this.playButtonPath = this.playButton.getElementsByTagName('path').item(0);
|
this.playButton = getOrThrow(controls.getElementsByTagName('button').item(1));
|
||||||
this.ffwButton = controls.getElementsByTagName('button').item(2);
|
this.playButtonPath = getOrThrow(this.playButton.getElementsByTagName('path').item(0));
|
||||||
this.skipButton = controls.getElementsByTagName('button').item(3);
|
this.ffwButton = getOrThrow(controls.getElementsByTagName('button').item(2));
|
||||||
this.cover = this.nowPlaying.getElementsByTagName('img').item(0);
|
this.skipButton = getOrThrow(controls.getElementsByTagName('button').item(3));
|
||||||
this.seriesName = this.nowPlaying.getElementsByTagName('span').item(0);
|
this.cover = getOrThrow(this.nowPlaying.getElementsByTagName('img').item(0));
|
||||||
this.episodeName = this.nowPlaying.getElementsByTagName('span').item(1);
|
this.seriesName = getOrThrow(this.nowPlaying.getElementsByTagName('span').item(0));
|
||||||
this.timeDisplay = timeVolume.getElementsByTagName('span').item(0);
|
this.episodeName = getOrThrow(this.nowPlaying.getElementsByTagName('span').item(1));
|
||||||
this.volumeSlider = timeVolume.getElementsByTagName('input').item(0);
|
this.timeDisplay = getOrThrow(timeVolume.getElementsByTagName('span').item(0));
|
||||||
this.progress = document.getElementById('progress');
|
this.volumeSlider = getOrThrow(timeVolume.getElementsByTagName('input').item(0));
|
||||||
|
this.progress = getOrThrow(document.getElementById('progress'));
|
||||||
this.ticker = null;
|
this.ticker = null;
|
||||||
this.howl = null;
|
this.howl = null;
|
||||||
this.episode = null;
|
this.episode = null;
|
||||||
|
@ -83,23 +84,23 @@ var Player = /** @class */ (function () {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// set up playlist changed handler
|
// set up playlist changed handler
|
||||||
this.playlist.setPlaylistChangedHandler(function () {
|
this.playlist.addPlaylistChangedHandler(function () {
|
||||||
_this.skipButton.disabled = !_this.playlist.hasNextEpisode();
|
_this.skipButton.disabled = !_this.playlist.hasNextEpisode();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Player.prototype.setErrorUI = function (message) {
|
Player.prototype.currentEpisode = function () {
|
||||||
this.stopPlaybackAndResetUi();
|
return this.episode;
|
||||||
this.seriesName.innerHTML = 'Error playing episode';
|
|
||||||
this.episodeName.innerHTML = message;
|
|
||||||
this.nowPlaying.classList.add('error');
|
|
||||||
};
|
};
|
||||||
Player.prototype.playEpisode = function (episode) {
|
Player.prototype.playEpisode = function (episode, paused) {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
|
if (paused === void 0) { paused = false; }
|
||||||
this.stopPlaybackAndResetUi();
|
this.stopPlaybackAndResetUi();
|
||||||
this.setPauseButtonUI();
|
this.setPauseButtonUI();
|
||||||
this.episode = episode;
|
this.episode = episode;
|
||||||
|
this.playlist.removeEpisode(episode);
|
||||||
this.updateNowPlayingUI(true);
|
this.updateNowPlayingUI(true);
|
||||||
fetch("/api/r/".concat(episode.file)).then(function (res) { return __awaiter(_this, void 0, void 0, function () {
|
void fetch("/api/r/".concat(episode.file))
|
||||||
|
.then(function (res) { return __awaiter(_this, void 0, void 0, function () {
|
||||||
var link;
|
var link;
|
||||||
var _this = this;
|
var _this = this;
|
||||||
return __generator(this, function (_a) {
|
return __generator(this, function (_a) {
|
||||||
|
@ -115,15 +116,17 @@ var Player = /** @class */ (function () {
|
||||||
this.howl = new Howl({
|
this.howl = new Howl({
|
||||||
src: "".concat(link.url, "?Authorization=").concat(link.token),
|
src: "".concat(link.url, "?Authorization=").concat(link.token),
|
||||||
html5: true,
|
html5: true,
|
||||||
autoplay: true,
|
autoplay: !paused,
|
||||||
volume: this.getVolume(),
|
volume: this.getVolume(),
|
||||||
onload: function () {
|
onload: function () {
|
||||||
_this.updateTimeUI();
|
_this.updateTimeUI();
|
||||||
_this.sendMediaSessionMetadata();
|
_this.sendMediaSessionMetadata();
|
||||||
},
|
|
||||||
onplay: function () {
|
|
||||||
_this.updateNowPlayingUI();
|
_this.updateNowPlayingUI();
|
||||||
_this.sendMediaSessionMetadata();
|
_this.sendMediaSessionMetadata();
|
||||||
|
if (paused)
|
||||||
|
_this.setPlayButtonUI();
|
||||||
|
},
|
||||||
|
onplay: function () {
|
||||||
_this.setPauseButtonUI();
|
_this.setPauseButtonUI();
|
||||||
_this.startTicker();
|
_this.startTicker();
|
||||||
},
|
},
|
||||||
|
@ -137,7 +140,16 @@ var Player = /** @class */ (function () {
|
||||||
return [2 /*return*/];
|
return [2 /*return*/];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}); });
|
}); })
|
||||||
|
.catch(function () {
|
||||||
|
_this.setErrorUI('Fetch episode error');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Player.prototype.setErrorUI = function (message) {
|
||||||
|
this.stopPlaybackAndResetUi();
|
||||||
|
this.seriesName.innerHTML = 'Error playing episode';
|
||||||
|
this.episodeName.innerHTML = message;
|
||||||
|
this.nowPlaying.classList.add('error');
|
||||||
};
|
};
|
||||||
Player.prototype.playPause = function () {
|
Player.prototype.playPause = function () {
|
||||||
if (this.howl && this.howl.playing())
|
if (this.howl && this.howl.playing())
|
||||||
|
@ -184,9 +196,7 @@ var Player = /** @class */ (function () {
|
||||||
? 'Loading episode'
|
? 'Loading episode'
|
||||||
: this.episode.series.title;
|
: this.episode.series.title;
|
||||||
this.episodeName.innerHTML = this.episode.title;
|
this.episodeName.innerHTML = this.episode.title;
|
||||||
this.cover.src = loading
|
this.cover.src = loading ? '/loading.gif' : this.episode.series.cover;
|
||||||
? '/loading.gif'
|
|
||||||
: this.episode.series.cover;
|
|
||||||
this.nowPlaying.title = "".concat(this.episode.series.title, "\n").concat(this.episode.title);
|
this.nowPlaying.title = "".concat(this.episode.series.title, "\n").concat(this.episode.title);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -214,11 +224,13 @@ var Player = /** @class */ (function () {
|
||||||
title: this.episode.title,
|
title: this.episode.title,
|
||||||
album: 'Radiostasis',
|
album: 'Radiostasis',
|
||||||
artist: this.episode.series.title,
|
artist: this.episode.series.title,
|
||||||
artwork: [{
|
artwork: [
|
||||||
|
{
|
||||||
src: this.episode.series.cover,
|
src: this.episode.series.cover,
|
||||||
sizes: '256x256',
|
sizes: '256x256',
|
||||||
type: 'image/jpeg',
|
type: 'image/jpeg',
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -228,7 +240,7 @@ var Player = /** @class */ (function () {
|
||||||
};
|
};
|
||||||
Player.prototype.timeToDisplayString = function (time) {
|
Player.prototype.timeToDisplayString = function (time) {
|
||||||
var mins = Math.floor(time / 60);
|
var mins = Math.floor(time / 60);
|
||||||
var secs = Math.round(time - (mins * 60));
|
var secs = Math.round(time - mins * 60);
|
||||||
var secStr = secs < 10 ? "0".concat(secs) : secs.toString();
|
var secStr = secs < 10 ? "0".concat(secs) : secs.toString();
|
||||||
return "".concat(mins, ":").concat(secStr);
|
return "".concat(mins, ":").concat(secStr);
|
||||||
};
|
};
|
||||||
|
@ -236,7 +248,7 @@ var Player = /** @class */ (function () {
|
||||||
if (this.howl && this.howl.state() === 'loaded') {
|
if (this.howl && this.howl.state() === 'loaded') {
|
||||||
var total = this.howl.duration();
|
var total = this.howl.duration();
|
||||||
var current = this.howl.seek();
|
var current = this.howl.seek();
|
||||||
var pct = "".concat(Math.round(current / total * 1000) / 10, "%");
|
var pct = "".concat(Math.round((current / total) * 1000) / 10, "%");
|
||||||
var timeStamp = "".concat(this.timeToDisplayString(current), " / ").concat(this.timeToDisplayString(total));
|
var timeStamp = "".concat(this.timeToDisplayString(current), " / ").concat(this.timeToDisplayString(total));
|
||||||
// set the new values if they've changed since the last tick
|
// set the new values if they've changed since the last tick
|
||||||
if (this.timeDisplay.innerHTML !== timeStamp)
|
if (this.timeDisplay.innerHTML !== timeStamp)
|
||||||
|
@ -278,17 +290,77 @@ var Playlist = /** @class */ (function () {
|
||||||
function Playlist() {
|
function Playlist() {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
this.queueExpandedHeight = 'calc(100% - 6ex)';
|
this.queueExpandedHeight = 'calc(100% - 6ex)';
|
||||||
this.queueContainer = document.getElementById('queue-container');
|
// event handlers
|
||||||
|
this.changedHandlers = [];
|
||||||
|
this.playEpisodeHandler = null;
|
||||||
|
// the actual episode queue
|
||||||
|
this.episodes = [];
|
||||||
|
this.episodeHash = new Set();
|
||||||
|
this.queueContainer = getOrThrow(document.getElementById('queue-container'));
|
||||||
this.queueInitialHeight = getComputedStyle(this.queueContainer).height;
|
this.queueInitialHeight = getComputedStyle(this.queueContainer).height;
|
||||||
this.queueTab = this.queueContainer.getElementsByTagName('h2').item(0);
|
this.queueTab = getOrThrow(this.queueContainer.getElementsByTagName('h2').item(0));
|
||||||
this.overlay = document.getElementById('overlay');
|
this.queueList = getOrThrow(this.queueContainer.getElementsByTagName('ol').item(0));
|
||||||
|
this.overlay = getOrThrow(document.getElementById('overlay'));
|
||||||
this.queueTab.addEventListener('click', function () { return _this.toggleQueueUI(); });
|
this.queueTab.addEventListener('click', function () { return _this.toggleQueueUI(); });
|
||||||
this.overlay.addEventListener('click', function () { return _this.toggleQueueUI(); });
|
this.overlay.addEventListener('click', function () { return _this.toggleQueueUI(); });
|
||||||
}
|
}
|
||||||
|
Playlist.prototype.addPlaylistChangedHandler = function (handler) {
|
||||||
|
this.changedHandlers.push(handler);
|
||||||
|
};
|
||||||
|
Playlist.prototype.setPlayEpisodeHandler = function (handler) {
|
||||||
|
this.playEpisodeHandler = handler;
|
||||||
|
};
|
||||||
|
Playlist.prototype.hasNextEpisode = function () {
|
||||||
|
return this.episodes.length > 0;
|
||||||
|
};
|
||||||
|
Playlist.prototype.nextEpisode = function () {
|
||||||
|
if (this.episodes.length > 0) {
|
||||||
|
return this.episodes[0][0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
Playlist.prototype.pushEpisode = function (episode) {
|
||||||
|
if (!this.isQueued(episode)) {
|
||||||
|
var litem = this.createQueueListItem(episode);
|
||||||
|
this.episodes.push([episode, litem]);
|
||||||
|
this.episodeHash.add(episode.id);
|
||||||
|
this.queueList.appendChild(litem);
|
||||||
|
}
|
||||||
|
this.playlistChanged();
|
||||||
|
};
|
||||||
|
Playlist.prototype.unshiftEpisodes = function (episodes) {
|
||||||
|
for (var i = episodes.length - 1; i >= 0; i--) {
|
||||||
|
var episode = episodes[i];
|
||||||
|
if (this.isQueued(episode))
|
||||||
|
this.removeEpisode(episode);
|
||||||
|
var litem = this.createQueueListItem(episode);
|
||||||
|
this.episodes.unshift([episode, litem]);
|
||||||
|
this.episodeHash.add(episode.id);
|
||||||
|
this.queueList.prepend(litem);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Playlist.prototype.removeEpisode = function (episode) {
|
||||||
|
if (this.isQueued(episode)) {
|
||||||
|
var idx = this.episodes.findIndex(function (e) { return e[0].id == episode.id; });
|
||||||
|
var deleted = this.episodes.splice(idx, 1);
|
||||||
|
this.episodeHash.delete(episode.id);
|
||||||
|
deleted[0][1].remove();
|
||||||
|
}
|
||||||
|
this.playlistChanged();
|
||||||
|
};
|
||||||
|
Playlist.prototype.isQueued = function (episode) {
|
||||||
|
return this.episodeHash.has(episode.id);
|
||||||
|
};
|
||||||
|
Playlist.prototype.playlistChanged = function () {
|
||||||
|
for (var _i = 0, _a = this.changedHandlers; _i < _a.length; _i++) {
|
||||||
|
var handler = _a[_i];
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
};
|
||||||
Playlist.prototype.toggleQueueUI = function () {
|
Playlist.prototype.toggleQueueUI = function () {
|
||||||
if (this.queueContainer.style.height !== this.queueExpandedHeight) {
|
if (this.queueContainer.style.height !== this.queueExpandedHeight) {
|
||||||
this.queueContainer.style.height = this.queueExpandedHeight;
|
this.queueContainer.style.height = this.queueExpandedHeight;
|
||||||
this.overlay.style.backgroundColor = 'rgba(255, 255, 255, 0.5)';
|
this.overlay.style.backgroundColor = 'rgba(255, 255, 255, 0.75)';
|
||||||
this.overlay.style.backdropFilter = 'blur(5px)';
|
this.overlay.style.backdropFilter = 'blur(5px)';
|
||||||
this.overlay.style.pointerEvents = 'auto';
|
this.overlay.style.pointerEvents = 'auto';
|
||||||
}
|
}
|
||||||
|
@ -299,61 +371,128 @@ var Playlist = /** @class */ (function () {
|
||||||
this.overlay.style.pointerEvents = 'none';
|
this.overlay.style.pointerEvents = 'none';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Playlist.prototype.setPlaylistChangedHandler = function (handler) {
|
Playlist.prototype.createQueueListItem = function (episode) {
|
||||||
this.changedHandler = handler;
|
var _this = this;
|
||||||
};
|
var item = document.createElement('li');
|
||||||
Playlist.prototype.hasNextEpisode = function () {
|
item.classList.add('episode');
|
||||||
return false;
|
item.title = episode.title;
|
||||||
};
|
item.dataset.slug = episode.slug;
|
||||||
Playlist.prototype.queueEpisode = function (episode) {
|
item.dataset.sslug = episode.series.slug;
|
||||||
|
item.dataset.cover = episode.series.cover;
|
||||||
|
item.dataset.series = episode.series.title;
|
||||||
|
item.dataset.file = episode.file;
|
||||||
|
var label = document.createElement('label');
|
||||||
|
label.innerHTML = episode.title;
|
||||||
|
var sspan = document.createElement('span');
|
||||||
|
sspan.innerHTML = episode.series.title;
|
||||||
|
var aside = document.createElement('aside');
|
||||||
|
aside.appendChild(sspan);
|
||||||
|
var controls = document.createElement('div');
|
||||||
|
controls.classList.add('controls');
|
||||||
|
var playBtn = document.createElement('a');
|
||||||
|
playBtn.href = '#';
|
||||||
|
playBtn.innerHTML = 'Play Now';
|
||||||
|
playBtn.addEventListener('click', function () {
|
||||||
|
if (_this.playEpisodeHandler)
|
||||||
|
_this.playEpisodeHandler(episode);
|
||||||
|
});
|
||||||
|
var remBtn = document.createElement('a');
|
||||||
|
remBtn.href = '#';
|
||||||
|
remBtn.innerHTML = 'Remove';
|
||||||
|
remBtn.addEventListener('click', function () { return _this.removeEpisode(episode); });
|
||||||
|
controls.appendChild(playBtn);
|
||||||
|
controls.appendChild(remBtn);
|
||||||
|
item.appendChild(label);
|
||||||
|
item.appendChild(aside);
|
||||||
|
item.appendChild(controls);
|
||||||
|
return item;
|
||||||
};
|
};
|
||||||
return Playlist;
|
return Playlist;
|
||||||
}());
|
}());
|
||||||
var Radiostasis = /** @class */ (function () {
|
var Radiostasis = /** @class */ (function () {
|
||||||
function Radiostasis() {
|
function Radiostasis() {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
|
var _a;
|
||||||
this.lastSearch = null;
|
this.lastSearch = null;
|
||||||
this.debouncer = null;
|
this.debouncer = null;
|
||||||
this.playlist = new Playlist();
|
this.playlist = new Playlist();
|
||||||
this.player = new Player(this.playlist);
|
this.player = new Player(this.playlist);
|
||||||
this.main = document.getElementsByTagName('main').item(0);
|
this.main = getOrThrow(document.getElementsByTagName('main').item(0));
|
||||||
// set up htmx event handlers
|
// set up htmx event handlers
|
||||||
document.addEventListener('htmx:historyRestore', function () { return _this.wireLoadedFragment(); });
|
document.addEventListener('htmx:historyRestore', function () {
|
||||||
document.getElementsByTagName('main').item(0).addEventListener('htmx:afterSwap', function () { return _this.wireLoadedFragment(); });
|
return _this.wireLoadedFragment();
|
||||||
|
});
|
||||||
|
(_a = document
|
||||||
|
.getElementsByTagName('main')
|
||||||
|
.item(0)) === null || _a === void 0 ? void 0 : _a.addEventListener('htmx:afterSwap', function () { return _this.wireLoadedFragment(); });
|
||||||
|
// set up playlist changed handler
|
||||||
|
this.playlist.addPlaylistChangedHandler(function () { return _this.episodeStateChanged(); });
|
||||||
}
|
}
|
||||||
|
Radiostasis.prototype.initialize = function () {
|
||||||
|
var path = location.pathname == '/'
|
||||||
|
? '/partial/home.html'
|
||||||
|
: "/partial/".concat(location.pathname, ".html");
|
||||||
|
htmx.ajax('GET', path, 'main');
|
||||||
|
};
|
||||||
Radiostasis.prototype.wireLoadedFragment = function () {
|
Radiostasis.prototype.wireLoadedFragment = function () {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
||||||
// episode play and queue buttons
|
// episode play and queue buttons
|
||||||
var episodes = this.main.getElementsByClassName('episode');
|
var episodes = this.main.getElementsByClassName('episode');
|
||||||
var _loop_1 = function (i) {
|
var _loop_1 = function (i) {
|
||||||
var el = episodes.item(i);
|
var el = episodes.item(i);
|
||||||
var episode = {
|
var episode = {
|
||||||
slug: el.dataset.slug,
|
id: "".concat((_a = el.dataset.sslug) !== null && _a !== void 0 ? _a : '', "/").concat((_b = el.dataset.slug) !== null && _b !== void 0 ? _b : ''),
|
||||||
title: el.getAttribute('title'),
|
slug: (_c = el.dataset.slug) !== null && _c !== void 0 ? _c : '',
|
||||||
file: el.dataset.file,
|
title: (_d = el.getAttribute('title')) !== null && _d !== void 0 ? _d : '',
|
||||||
|
file: (_e = el.dataset.file) !== null && _e !== void 0 ? _e : '',
|
||||||
|
length: (_f = el.dataset.length) !== null && _f !== void 0 ? _f : '',
|
||||||
|
size: parseInt((_g = el.dataset.size) !== null && _g !== void 0 ? _g : '0'),
|
||||||
series: {
|
series: {
|
||||||
slug: el.dataset.sslug,
|
slug: (_h = el.dataset.sslug) !== null && _h !== void 0 ? _h : '',
|
||||||
title: el.dataset.series,
|
title: (_j = el.dataset.series) !== null && _j !== void 0 ? _j : '',
|
||||||
cover: el.dataset.cover,
|
cover: (_k = el.dataset.cover) !== null && _k !== void 0 ? _k : '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
// store this model for later use
|
||||||
|
el.dataset.ejson = JSON.stringify(episode);
|
||||||
// play button
|
// play button
|
||||||
el.getElementsByTagName('a').item(0).addEventListener('click', function (e) {
|
(_l = el.getElementsByTagName('a')
|
||||||
|
.item(0)) === null || _l === void 0 ? void 0 : _l.addEventListener('click', function (e) {
|
||||||
|
var _a;
|
||||||
|
if (((_a = _this.player.currentEpisode()) === null || _a === void 0 ? void 0 : _a.id) !== episode.id) {
|
||||||
_this.player.playEpisode(episode);
|
_this.player.playEpisode(episode);
|
||||||
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
// queue button
|
// queue button
|
||||||
el.getElementsByTagName('a').item(1).addEventListener('click', function (e) {
|
(_m = el.getElementsByTagName('a')
|
||||||
_this.playlist.queueEpisode(episode);
|
.item(1)) === null || _m === void 0 ? void 0 : _m.addEventListener('click', function (e) {
|
||||||
|
var _a;
|
||||||
|
if (!_this.player.currentEpisode()) {
|
||||||
|
// if nothing is playing, first queued item gets loaded
|
||||||
|
// into the player but does not autoplay
|
||||||
|
_this.player.playEpisode(episode, true);
|
||||||
|
}
|
||||||
|
else if (((_a = _this.player.currentEpisode()) === null || _a === void 0 ? void 0 : _a.id) !== episode.id) {
|
||||||
|
if (_this.playlist.isQueued(episode)) {
|
||||||
|
_this.playlist.removeEpisode(episode);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_this.playlist.pushEpisode(episode);
|
||||||
|
}
|
||||||
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
// trigger the playlist update function to mark queued episodes
|
||||||
|
this_1.episodeStateChanged();
|
||||||
};
|
};
|
||||||
|
var this_1 = this;
|
||||||
for (var i = 0; i < episodes.length; i++) {
|
for (var i = 0; i < episodes.length; i++) {
|
||||||
_loop_1(i);
|
_loop_1(i);
|
||||||
}
|
}
|
||||||
// series filter input
|
// series filter input
|
||||||
var filter = this.main
|
var filter = (this.main.getElementsByClassName('filter').item(0));
|
||||||
.getElementsByClassName('filter').item(0);
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
if (this.lastSearch) {
|
if (this.lastSearch) {
|
||||||
filter.value = this.lastSearch;
|
filter.value = this.lastSearch;
|
||||||
|
@ -368,6 +507,8 @@ var Radiostasis = /** @class */ (function () {
|
||||||
var terms = _this.lastSearch.split(' ');
|
var terms = _this.lastSearch.split(' ');
|
||||||
for (var i = 0; i < allSeries_1.length; i++) {
|
for (var i = 0; i < allSeries_1.length; i++) {
|
||||||
var series = allSeries_1.item(i);
|
var series = allSeries_1.item(i);
|
||||||
|
if (!series || !series.dataset.filter)
|
||||||
|
continue;
|
||||||
var match = true;
|
var match = true;
|
||||||
for (var _i = 0, terms_1 = terms; _i < terms_1.length; _i++) {
|
for (var _i = 0, terms_1 = terms; _i < terms_1.length; _i++) {
|
||||||
var term = terms_1[_i];
|
var term = terms_1[_i];
|
||||||
|
@ -385,14 +526,42 @@ var Radiostasis = /** @class */ (function () {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Radiostasis.prototype.initialize = function () {
|
Radiostasis.prototype.episodeStateChanged = function () {
|
||||||
var path = location.pathname == '/'
|
var _a;
|
||||||
? '/partial/home.html'
|
var episodes = this.main.getElementsByClassName('episode');
|
||||||
: "/partial/".concat(location.pathname, ".html");
|
for (var i = 0; i < episodes.length; i++) {
|
||||||
htmx.ajax('GET', path, 'main');
|
var el = episodes.item(i);
|
||||||
|
if (!el || !el.dataset.ejson)
|
||||||
|
continue;
|
||||||
|
var episode = JSON.parse(el.dataset.ejson);
|
||||||
|
if (((_a = this.player.currentEpisode()) === null || _a === void 0 ? void 0 : _a.id) === episode.id) {
|
||||||
|
el.classList.add('playing');
|
||||||
|
getOrThrow(el.getElementsByTagName('a').item(0)).innerHTML = 'Playing';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
el.classList.remove('playing');
|
||||||
|
getOrThrow(el.getElementsByTagName('a').item(0)).innerHTML =
|
||||||
|
'Play Episode';
|
||||||
|
}
|
||||||
|
var queueBtn = getOrThrow(el.getElementsByTagName('a').item(1));
|
||||||
|
if (this.playlist.isQueued(episode)) {
|
||||||
|
queueBtn.classList.add('queued');
|
||||||
|
queueBtn.innerHTML = 'Remove from Queue';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
queueBtn.classList.remove('queued');
|
||||||
|
queueBtn.innerHTML = 'Queue Episode';
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return Radiostasis;
|
return Radiostasis;
|
||||||
}());
|
}());
|
||||||
|
function getOrThrow(element) {
|
||||||
|
if (!element) {
|
||||||
|
throw new Error("tried to get an element that doesn't exist");
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// start application
|
// start application
|
||||||
new Radiostasis().initialize();
|
new Radiostasis().initialize();
|
||||||
|
|
|
@ -284,11 +284,10 @@ ol > li:not(:last-child) {
|
||||||
#queue-container {
|
#queue-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
max-width: 120ch;
|
|
||||||
border: 2px solid black;
|
border: 2px solid black;
|
||||||
transition: height 0.25s ease-out;
|
transition: height 0.25s ease-out;
|
||||||
height: 10ex;
|
height: 10ex;
|
||||||
min-width: 25%;
|
min-width: 42ch;
|
||||||
background: white;
|
background: white;
|
||||||
margin: 0 1ch;
|
margin: 0 1ch;
|
||||||
}
|
}
|
||||||
|
@ -328,10 +327,10 @@ ol > li:not(:last-child) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#queue-container ol {
|
#queue-container ol {
|
||||||
list-style-type: none;
|
padding-left: 5ch;
|
||||||
padding-left: 0;
|
padding-right: 1rem;
|
||||||
margin: 0 1rem;
|
margin: 0 1rem;
|
||||||
max-height: calc(100% - 16ex);
|
max-height: calc(100% - 15ex);
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -532,6 +531,16 @@ h2 svg {
|
||||||
text-shadow: 0.05rem 0.05rem 0.125rem rgba(0, 0, 0, 0.25);
|
text-shadow: 0.05rem 0.05rem 0.125rem rgba(0, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.episode.playing label {
|
||||||
|
color: #c50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode.playing label:before {
|
||||||
|
content: url('/icon-play.svg');
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
margin: 0.25rem 0;
|
margin: 0.25rem 0;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
|
@ -541,7 +550,7 @@ h2 svg {
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls a {
|
.controls a, .playing .controls a:hover {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0.25rem 0.5rem 0.25rem 1.25rem;
|
padding: 0.25rem 0.5rem 0.25rem 1.25rem;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
|
@ -560,6 +569,10 @@ h2 svg {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.playing .controls {
|
||||||
|
opacity: 0.33;
|
||||||
|
}
|
||||||
|
|
||||||
.controls a:first-child {
|
.controls a:first-child {
|
||||||
background-image: url('/icon-play.svg');
|
background-image: url('/icon-play.svg');
|
||||||
}
|
}
|
||||||
|
@ -568,6 +581,10 @@ h2 svg {
|
||||||
background-image: url('/icon-queue.svg');
|
background-image: url('/icon-queue.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls a:last-child.queued {
|
||||||
|
background-image: url('/icon-trash.svg');
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 50ch) {
|
@media only screen and (max-width: 50ch) {
|
||||||
.seriesDetails section {
|
.seriesDetails section {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
@ -592,4 +609,11 @@ h2 svg {
|
||||||
.detail article {
|
.detail article {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
#queue-container {
|
||||||
|
min-width: inherit;
|
||||||
|
width: calc(100% - 2ch);
|
||||||
|
}
|
||||||
|
#queue-container ol {
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
quote_type = single
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {"project": "tsconfig.json"},
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/triple-slash-reference": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/member-ordering": "warn",
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "error",
|
||||||
|
"prettier/prettier": "warn"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
26
src/Model.ts
26
src/Model.ts
|
@ -1,12 +1,20 @@
|
||||||
type Series = {
|
type Series = {
|
||||||
slug: string,
|
slug: string;
|
||||||
title: string,
|
title: string;
|
||||||
cover: string,
|
cover: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
type Episode = {
|
type Episode = {
|
||||||
series: Series,
|
id: string;
|
||||||
slug: string,
|
series: Series;
|
||||||
title: string,
|
slug: string;
|
||||||
file: string,
|
title: string;
|
||||||
}
|
file: string;
|
||||||
|
length: string;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UrlResponse = {
|
||||||
|
url: string;
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
|
184
src/Player.ts
184
src/Player.ts
|
@ -25,20 +25,41 @@ class Player {
|
||||||
|
|
||||||
public constructor(playlist: Playlist) {
|
public constructor(playlist: Playlist) {
|
||||||
this.playlist = playlist;
|
this.playlist = playlist;
|
||||||
const controls = document.getElementById('controls')!;
|
this.playlist.setPlayEpisodeHandler((episode) => this.playEpisode(episode));
|
||||||
const timeVolume = document.getElementById('timeVolume')!;
|
const controls = getOrThrow(document.getElementById('controls'));
|
||||||
this.nowPlaying = document.getElementById('nowPlaying')!;
|
const timeVolume = getOrThrow(document.getElementById('timeVolume'));
|
||||||
this.rewButton = controls.getElementsByTagName('button').item(0)!;
|
this.nowPlaying = getOrThrow(document.getElementById('nowPlaying'));
|
||||||
this.playButton = controls.getElementsByTagName('button').item(1)!;
|
this.rewButton = getOrThrow(
|
||||||
this.playButtonPath = this.playButton.getElementsByTagName('path').item(0)!;
|
controls.getElementsByTagName('button').item(0)
|
||||||
this.ffwButton = controls.getElementsByTagName('button').item(2)!;
|
);
|
||||||
this.skipButton = controls.getElementsByTagName('button').item(3)!;
|
this.playButton = getOrThrow(
|
||||||
this.cover = this.nowPlaying.getElementsByTagName('img').item(0)!;
|
controls.getElementsByTagName('button').item(1)
|
||||||
this.seriesName = this.nowPlaying.getElementsByTagName('span').item(0)!;
|
);
|
||||||
this.episodeName = this.nowPlaying.getElementsByTagName('span').item(1)!;
|
this.playButtonPath = getOrThrow(
|
||||||
this.timeDisplay = timeVolume.getElementsByTagName('span').item(0)!;
|
this.playButton.getElementsByTagName('path').item(0)
|
||||||
this.volumeSlider = timeVolume.getElementsByTagName('input').item(0)!;
|
);
|
||||||
this.progress = document.getElementById('progress')!;
|
this.ffwButton = getOrThrow(
|
||||||
|
controls.getElementsByTagName('button').item(2)
|
||||||
|
);
|
||||||
|
this.skipButton = getOrThrow(
|
||||||
|
controls.getElementsByTagName('button').item(3)
|
||||||
|
);
|
||||||
|
this.cover = getOrThrow(
|
||||||
|
this.nowPlaying.getElementsByTagName('img').item(0)
|
||||||
|
);
|
||||||
|
this.seriesName = getOrThrow(
|
||||||
|
this.nowPlaying.getElementsByTagName('span').item(0)
|
||||||
|
);
|
||||||
|
this.episodeName = getOrThrow(
|
||||||
|
this.nowPlaying.getElementsByTagName('span').item(1)
|
||||||
|
);
|
||||||
|
this.timeDisplay = getOrThrow(
|
||||||
|
timeVolume.getElementsByTagName('span').item(0)
|
||||||
|
);
|
||||||
|
this.volumeSlider = getOrThrow(
|
||||||
|
timeVolume.getElementsByTagName('input').item(0)
|
||||||
|
);
|
||||||
|
this.progress = getOrThrow(document.getElementById('progress'));
|
||||||
this.ticker = null;
|
this.ticker = null;
|
||||||
this.howl = null;
|
this.howl = null;
|
||||||
this.episode = null;
|
this.episode = null;
|
||||||
|
@ -57,23 +78,74 @@ class Player {
|
||||||
navigator.mediaSession.setActionHandler('pause', () => this.playPause());
|
navigator.mediaSession.setActionHandler('pause', () => this.playPause());
|
||||||
navigator.mediaSession.setActionHandler('play', () => this.playPause());
|
navigator.mediaSession.setActionHandler('play', () => this.playPause());
|
||||||
navigator.mediaSession.setActionHandler('stop', () =>
|
navigator.mediaSession.setActionHandler('stop', () =>
|
||||||
this.stopPlaybackAndResetUi());
|
this.stopPlaybackAndResetUi()
|
||||||
|
);
|
||||||
navigator.mediaSession.setActionHandler('seekforward', () =>
|
navigator.mediaSession.setActionHandler('seekforward', () =>
|
||||||
this.fastForward());
|
this.fastForward()
|
||||||
|
);
|
||||||
navigator.mediaSession.setActionHandler('seekbackward', () =>
|
navigator.mediaSession.setActionHandler('seekbackward', () =>
|
||||||
this.rewind());
|
this.rewind()
|
||||||
|
);
|
||||||
// don't support previous track yet, queue removes them once finished
|
// don't support previous track yet, queue removes them once finished
|
||||||
// wire this up to rewind instead
|
// wire this up to rewind instead
|
||||||
navigator.mediaSession.setActionHandler('previoustrack', () =>
|
navigator.mediaSession.setActionHandler('previoustrack', () =>
|
||||||
this.rewind());
|
this.rewind()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set up playlist changed handler
|
// set up playlist changed handler
|
||||||
this.playlist.setPlaylistChangedHandler(() => {
|
this.playlist.addPlaylistChangedHandler(() => {
|
||||||
this.skipButton.disabled = !this.playlist.hasNextEpisode();
|
this.skipButton.disabled = !this.playlist.hasNextEpisode();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public currentEpisode(): Episode | null {
|
||||||
|
return this.episode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public playEpisode(episode: Episode, paused = false): void {
|
||||||
|
this.stopPlaybackAndResetUi();
|
||||||
|
this.setPauseButtonUI();
|
||||||
|
this.episode = episode;
|
||||||
|
this.playlist.removeEpisode(episode);
|
||||||
|
this.updateNowPlayingUI(true);
|
||||||
|
|
||||||
|
void fetch(`/api/r/${episode.file}`)
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
this.setErrorUI(`API returned ${res.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const link = <UrlResponse>await res.json();
|
||||||
|
this.howl = new Howl({
|
||||||
|
src: `${link.url}?Authorization=${link.token}`,
|
||||||
|
html5: true,
|
||||||
|
autoplay: !paused,
|
||||||
|
volume: this.getVolume(),
|
||||||
|
onload: (): void => {
|
||||||
|
this.updateTimeUI();
|
||||||
|
this.sendMediaSessionMetadata();
|
||||||
|
this.updateNowPlayingUI();
|
||||||
|
this.sendMediaSessionMetadata();
|
||||||
|
if (paused) this.setPlayButtonUI();
|
||||||
|
},
|
||||||
|
onplay: (): void => {
|
||||||
|
this.setPauseButtonUI();
|
||||||
|
this.startTicker();
|
||||||
|
},
|
||||||
|
onpause: (): void => {
|
||||||
|
this.setPlayButtonUI();
|
||||||
|
this.stopTicker();
|
||||||
|
},
|
||||||
|
onend: () => this.stopPlaybackAndResetUi(),
|
||||||
|
onloaderror: () => this.setErrorUI('Playback error'),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.setErrorUI('Fetch episode error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private setErrorUI(message: string): void {
|
private setErrorUI(message: string): void {
|
||||||
this.stopPlaybackAndResetUi();
|
this.stopPlaybackAndResetUi();
|
||||||
this.seriesName.innerHTML = 'Error playing episode';
|
this.seriesName.innerHTML = 'Error playing episode';
|
||||||
|
@ -81,43 +153,6 @@ class Player {
|
||||||
this.nowPlaying.classList.add('error');
|
this.nowPlaying.classList.add('error');
|
||||||
}
|
}
|
||||||
|
|
||||||
public playEpisode(episode: Episode) {
|
|
||||||
this.stopPlaybackAndResetUi();
|
|
||||||
this.setPauseButtonUI();
|
|
||||||
this.episode = episode;
|
|
||||||
this.updateNowPlayingUI(true);
|
|
||||||
|
|
||||||
fetch(`/api/r/${episode.file}`).then(async (res) => {
|
|
||||||
if (!res.ok) {
|
|
||||||
this.setErrorUI(`API returned ${res.status}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const link = await res.json();
|
|
||||||
this.howl = new Howl({
|
|
||||||
src: `${link.url}?Authorization=${link.token}`,
|
|
||||||
html5: true,
|
|
||||||
autoplay: true,
|
|
||||||
volume: this.getVolume(),
|
|
||||||
onload: () => {
|
|
||||||
this.updateTimeUI();
|
|
||||||
this.sendMediaSessionMetadata();
|
|
||||||
},
|
|
||||||
onplay: () => {
|
|
||||||
this.updateNowPlayingUI();
|
|
||||||
this.sendMediaSessionMetadata();
|
|
||||||
this.setPauseButtonUI();
|
|
||||||
this.startTicker();
|
|
||||||
},
|
|
||||||
onpause: () => {
|
|
||||||
this.setPlayButtonUI();
|
|
||||||
this.stopTicker();
|
|
||||||
},
|
|
||||||
onend: () => this.stopPlaybackAndResetUi(),
|
|
||||||
onloaderror: () => this.setErrorUI('Playback error'),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private playPause(): void {
|
private playPause(): void {
|
||||||
if (this.howl && this.howl.playing()) this.howl.pause();
|
if (this.howl && this.howl.playing()) this.howl.pause();
|
||||||
else if (this.howl && !this.howl.playing()) this.howl.play();
|
else if (this.howl && !this.howl.playing()) this.howl.play();
|
||||||
|
@ -142,13 +177,17 @@ class Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
private setPlayButtonUI(): void {
|
private setPlayButtonUI(): void {
|
||||||
this.playButtonPath.setAttribute('d',
|
this.playButtonPath.setAttribute(
|
||||||
'M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.79 5.093A.5.5 0 0 0 6 5.5v5a.5.5 0 0 0 .79.407l3.5-2.5a.5.5 0 0 0 0-.814l-3.5-2.5z');
|
'd',
|
||||||
|
'M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.79 5.093A.5.5 0 0 0 6 5.5v5a.5.5 0 0 0 .79.407l3.5-2.5a.5.5 0 0 0 0-.814l-3.5-2.5z'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setPauseButtonUI(): void {
|
private setPauseButtonUI(): void {
|
||||||
this.playButtonPath.setAttribute('d',
|
this.playButtonPath.setAttribute(
|
||||||
'M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.25 5C5.56 5 5 5.56 5 6.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C7.5 5.56 6.94 5 6.25 5zm3.5 0c-.69 0-1.25.56-1.25 1.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C11 5.56 10.44 5 9.75 5z');
|
'd',
|
||||||
|
'M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.25 5C5.56 5 5 5.56 5 6.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C7.5 5.56 6.94 5 6.25 5zm3.5 0c-.69 0-1.25.56-1.25 1.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C11 5.56 10.44 5 9.75 5z'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getVolume(): number {
|
private getVolume(): number {
|
||||||
|
@ -159,16 +198,14 @@ class Player {
|
||||||
this.howl?.volume(this.getVolume());
|
this.howl?.volume(this.getVolume());
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateNowPlayingUI(loading: boolean = false) {
|
private updateNowPlayingUI(loading = false): void {
|
||||||
this.nowPlaying.classList.remove('error');
|
this.nowPlaying.classList.remove('error');
|
||||||
if (this.episode) {
|
if (this.episode) {
|
||||||
this.seriesName.innerHTML = loading
|
this.seriesName.innerHTML = loading
|
||||||
? 'Loading episode'
|
? 'Loading episode'
|
||||||
: this.episode.series.title;
|
: this.episode.series.title;
|
||||||
this.episodeName.innerHTML = this.episode.title;
|
this.episodeName.innerHTML = this.episode.title;
|
||||||
this.cover.src = loading
|
this.cover.src = loading ? '/loading.gif' : this.episode.series.cover;
|
||||||
? '/loading.gif'
|
|
||||||
: this.episode.series.cover;
|
|
||||||
this.nowPlaying.title = `${this.episode.series.title}\n${this.episode.title}`;
|
this.nowPlaying.title = `${this.episode.series.title}\n${this.episode.title}`;
|
||||||
} else {
|
} else {
|
||||||
this.seriesName.innerHTML = 'No episode playing';
|
this.seriesName.innerHTML = 'No episode playing';
|
||||||
|
@ -190,18 +227,20 @@ class Player {
|
||||||
this.skipButton.disabled = !this.playlist.hasNextEpisode();
|
this.skipButton.disabled = !this.playlist.hasNextEpisode();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendMediaSessionMetadata() {
|
private sendMediaSessionMetadata(): void {
|
||||||
if ('mediaSession' in navigator) {
|
if ('mediaSession' in navigator) {
|
||||||
if (this.episode) {
|
if (this.episode) {
|
||||||
navigator.mediaSession.metadata = new MediaMetadata({
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
title: this.episode.title,
|
title: this.episode.title,
|
||||||
album: 'Radiostasis',
|
album: 'Radiostasis',
|
||||||
artist: this.episode.series.title,
|
artist: this.episode.series.title,
|
||||||
artwork: [{
|
artwork: [
|
||||||
|
{
|
||||||
src: this.episode.series.cover,
|
src: this.episode.series.cover,
|
||||||
sizes: '256x256',
|
sizes: '256x256',
|
||||||
type: 'image/jpeg',
|
type: 'image/jpeg',
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
navigator.mediaSession.metadata = null;
|
navigator.mediaSession.metadata = null;
|
||||||
|
@ -211,7 +250,7 @@ class Player {
|
||||||
|
|
||||||
private timeToDisplayString(time: number): string {
|
private timeToDisplayString(time: number): string {
|
||||||
const mins = Math.floor(time / 60);
|
const mins = Math.floor(time / 60);
|
||||||
const secs = Math.round(time - (mins * 60));
|
const secs = Math.round(time - mins * 60);
|
||||||
const secStr = secs < 10 ? `0${secs}` : secs.toString();
|
const secStr = secs < 10 ? `0${secs}` : secs.toString();
|
||||||
return `${mins}:${secStr}`;
|
return `${mins}:${secStr}`;
|
||||||
}
|
}
|
||||||
|
@ -220,9 +259,10 @@ class Player {
|
||||||
if (this.howl && this.howl.state() === 'loaded') {
|
if (this.howl && this.howl.state() === 'loaded') {
|
||||||
const total = this.howl.duration();
|
const total = this.howl.duration();
|
||||||
const current = this.howl.seek();
|
const current = this.howl.seek();
|
||||||
const pct = `${Math.round(current / total * 1000) / 10}%`;
|
const pct = `${Math.round((current / total) * 1000) / 10}%`;
|
||||||
const timeStamp =
|
const timeStamp = `${this.timeToDisplayString(
|
||||||
`${this.timeToDisplayString(current)} / ${this.timeToDisplayString(total)}`;
|
current
|
||||||
|
)} / ${this.timeToDisplayString(total)}`;
|
||||||
|
|
||||||
// set the new values if they've changed since the last tick
|
// set the new values if they've changed since the last tick
|
||||||
if (this.timeDisplay.innerHTML !== timeStamp)
|
if (this.timeDisplay.innerHTML !== timeStamp)
|
||||||
|
@ -248,7 +288,7 @@ class Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
private stopPlaybackAndResetUi(): void {
|
private stopPlaybackAndResetUi(): void {
|
||||||
this.howl?.unload()
|
this.howl?.unload();
|
||||||
this.howl = null;
|
this.howl = null;
|
||||||
this.episode = null;
|
this.episode = null;
|
||||||
this.setPlayButtonUI();
|
this.setPlayButtonUI();
|
||||||
|
|
127
src/Playlist.ts
127
src/Playlist.ts
|
@ -2,26 +2,99 @@ class Playlist {
|
||||||
private readonly queueContainer: HTMLElement;
|
private readonly queueContainer: HTMLElement;
|
||||||
private readonly queueTab: HTMLElement;
|
private readonly queueTab: HTMLElement;
|
||||||
private readonly overlay: HTMLElement;
|
private readonly overlay: HTMLElement;
|
||||||
|
private readonly queueList: HTMLOListElement;
|
||||||
private readonly queueInitialHeight: string;
|
private readonly queueInitialHeight: string;
|
||||||
private readonly queueExpandedHeight = 'calc(100% - 6ex)';
|
private readonly queueExpandedHeight = 'calc(100% - 6ex)';
|
||||||
|
|
||||||
// changed event handler
|
// event handlers
|
||||||
private changedHandler?: () => void;
|
private changedHandlers: Array<() => void> = [];
|
||||||
|
private playEpisodeHandler: ((episode: Episode) => void) | null = null;
|
||||||
|
|
||||||
|
// the actual episode queue
|
||||||
|
private readonly episodes: Array<[Episode, HTMLLIElement]> = [];
|
||||||
|
private readonly episodeHash: Set<string> = new Set<string>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.queueContainer = document.getElementById('queue-container')!;
|
this.queueContainer = getOrThrow(
|
||||||
|
document.getElementById('queue-container')
|
||||||
|
);
|
||||||
this.queueInitialHeight = getComputedStyle(this.queueContainer).height;
|
this.queueInitialHeight = getComputedStyle(this.queueContainer).height;
|
||||||
this.queueTab = this.queueContainer.getElementsByTagName('h2').item(0)!;
|
this.queueTab = getOrThrow(
|
||||||
this.overlay = document.getElementById('overlay')!;
|
this.queueContainer.getElementsByTagName('h2').item(0)
|
||||||
|
);
|
||||||
|
this.queueList = getOrThrow(
|
||||||
|
this.queueContainer.getElementsByTagName('ol').item(0)
|
||||||
|
);
|
||||||
|
this.overlay = getOrThrow(document.getElementById('overlay'));
|
||||||
|
|
||||||
this.queueTab.addEventListener('click', () => this.toggleQueueUI());
|
this.queueTab.addEventListener('click', () => this.toggleQueueUI());
|
||||||
this.overlay.addEventListener('click', () => this.toggleQueueUI());
|
this.overlay.addEventListener('click', () => this.toggleQueueUI());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addPlaylistChangedHandler(handler: () => void): void {
|
||||||
|
this.changedHandlers.push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setPlayEpisodeHandler(handler: (episode: Episode) => void): void {
|
||||||
|
this.playEpisodeHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasNextEpisode(): boolean {
|
||||||
|
return this.episodes.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextEpisode(): Episode | null {
|
||||||
|
if (this.episodes.length > 0) {
|
||||||
|
return this.episodes[0][0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public pushEpisode(episode: Episode): void {
|
||||||
|
if (!this.isQueued(episode)) {
|
||||||
|
const litem = this.createQueueListItem(episode);
|
||||||
|
this.episodes.push([episode, litem]);
|
||||||
|
this.episodeHash.add(episode.id);
|
||||||
|
this.queueList.appendChild(litem);
|
||||||
|
}
|
||||||
|
this.playlistChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public unshiftEpisodes(episodes: Array<Episode>): void {
|
||||||
|
for (let i = episodes.length - 1; i >= 0; i--) {
|
||||||
|
const episode = episodes[i];
|
||||||
|
if (this.isQueued(episode)) this.removeEpisode(episode);
|
||||||
|
const litem = this.createQueueListItem(episode);
|
||||||
|
this.episodes.unshift([episode, litem]);
|
||||||
|
this.episodeHash.add(episode.id);
|
||||||
|
this.queueList.prepend(litem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeEpisode(episode: Episode): void {
|
||||||
|
if (this.isQueued(episode)) {
|
||||||
|
const idx = this.episodes.findIndex((e) => e[0].id == episode.id);
|
||||||
|
const deleted = this.episodes.splice(idx, 1);
|
||||||
|
this.episodeHash.delete(episode.id);
|
||||||
|
deleted[0][1].remove();
|
||||||
|
}
|
||||||
|
this.playlistChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isQueued(episode: Episode): boolean {
|
||||||
|
return this.episodeHash.has(episode.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private playlistChanged(): void {
|
||||||
|
for (const handler of this.changedHandlers) {
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private toggleQueueUI(): void {
|
private toggleQueueUI(): void {
|
||||||
if (this.queueContainer.style.height !== this.queueExpandedHeight) {
|
if (this.queueContainer.style.height !== this.queueExpandedHeight) {
|
||||||
this.queueContainer.style.height = this.queueExpandedHeight;
|
this.queueContainer.style.height = this.queueExpandedHeight;
|
||||||
this.overlay.style.backgroundColor = 'rgba(255, 255, 255, 0.5)';
|
this.overlay.style.backgroundColor = 'rgba(255, 255, 255, 0.75)';
|
||||||
this.overlay.style.backdropFilter = 'blur(5px)';
|
this.overlay.style.backdropFilter = 'blur(5px)';
|
||||||
this.overlay.style.pointerEvents = 'auto';
|
this.overlay.style.pointerEvents = 'auto';
|
||||||
} else {
|
} else {
|
||||||
|
@ -32,14 +105,38 @@ class Playlist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public setPlaylistChangedHandler(handler: () => void) {
|
private createQueueListItem(episode: Episode): HTMLLIElement {
|
||||||
this.changedHandler = handler;
|
const item = document.createElement('li');
|
||||||
}
|
item.classList.add('episode');
|
||||||
|
item.title = episode.title;
|
||||||
public hasNextEpisode(): boolean {
|
item.dataset.slug = episode.slug;
|
||||||
return false;
|
item.dataset.sslug = episode.series.slug;
|
||||||
}
|
item.dataset.cover = episode.series.cover;
|
||||||
|
item.dataset.series = episode.series.title;
|
||||||
public queueEpisode(episode: Episode): void {
|
item.dataset.file = episode.file;
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.innerHTML = episode.title;
|
||||||
|
const sspan = document.createElement('span');
|
||||||
|
sspan.innerHTML = episode.series.title;
|
||||||
|
const aside = document.createElement('aside');
|
||||||
|
aside.appendChild(sspan);
|
||||||
|
const controls = document.createElement('div');
|
||||||
|
controls.classList.add('controls');
|
||||||
|
const playBtn = document.createElement('a');
|
||||||
|
playBtn.href = '#';
|
||||||
|
playBtn.innerHTML = 'Play Now';
|
||||||
|
playBtn.addEventListener('click', () => {
|
||||||
|
if (this.playEpisodeHandler) this.playEpisodeHandler(episode);
|
||||||
|
});
|
||||||
|
const remBtn = document.createElement('a');
|
||||||
|
remBtn.href = '#';
|
||||||
|
remBtn.innerHTML = 'Remove';
|
||||||
|
remBtn.addEventListener('click', () => this.removeEpisode(episode));
|
||||||
|
controls.appendChild(playBtn);
|
||||||
|
controls.appendChild(remBtn);
|
||||||
|
item.appendChild(label);
|
||||||
|
item.appendChild(aside);
|
||||||
|
item.appendChild(controls);
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,46 +13,87 @@ class Radiostasis {
|
||||||
this.playlist = new Playlist();
|
this.playlist = new Playlist();
|
||||||
this.player = new Player(this.playlist);
|
this.player = new Player(this.playlist);
|
||||||
|
|
||||||
this.main = document.getElementsByTagName('main').item(0)!;
|
this.main = getOrThrow(document.getElementsByTagName('main').item(0));
|
||||||
|
|
||||||
// set up htmx event handlers
|
// set up htmx event handlers
|
||||||
document.addEventListener('htmx:historyRestore', () => this.wireLoadedFragment());
|
document.addEventListener('htmx:historyRestore', () =>
|
||||||
document.getElementsByTagName('main').item(0)!.addEventListener(
|
this.wireLoadedFragment()
|
||||||
'htmx:afterSwap', () => this.wireLoadedFragment());
|
);
|
||||||
|
document
|
||||||
|
.getElementsByTagName('main')
|
||||||
|
.item(0)
|
||||||
|
?.addEventListener('htmx:afterSwap', () => this.wireLoadedFragment());
|
||||||
|
|
||||||
|
// set up playlist changed handler
|
||||||
|
this.playlist.addPlaylistChangedHandler(() => this.episodeStateChanged());
|
||||||
|
}
|
||||||
|
|
||||||
|
public initialize(): void {
|
||||||
|
const path =
|
||||||
|
location.pathname == '/'
|
||||||
|
? '/partial/home.html'
|
||||||
|
: `/partial/${location.pathname}.html`;
|
||||||
|
htmx.ajax('GET', path, 'main');
|
||||||
}
|
}
|
||||||
|
|
||||||
private wireLoadedFragment(): void {
|
private wireLoadedFragment(): void {
|
||||||
// episode play and queue buttons
|
// episode play and queue buttons
|
||||||
const episodes = this.main.getElementsByClassName('episode');
|
const episodes = this.main.getElementsByClassName('episode');
|
||||||
for (let i = 0; i < episodes.length; i++) {
|
for (let i = 0; i < episodes.length; i++) {
|
||||||
const el = <HTMLElement>episodes.item(i)!;
|
const el = <HTMLElement>episodes.item(i);
|
||||||
const episode: Episode = {
|
const episode: Episode = {
|
||||||
slug: el.dataset.slug!,
|
id: `${el.dataset.sslug ?? ''}/${el.dataset.slug ?? ''}`,
|
||||||
title: el.getAttribute('title')!,
|
slug: el.dataset.slug ?? '',
|
||||||
file: el.dataset.file!,
|
title: el.getAttribute('title') ?? '',
|
||||||
|
file: el.dataset.file ?? '',
|
||||||
|
length: el.dataset.length ?? '',
|
||||||
|
size: parseInt(el.dataset.size ?? '0'),
|
||||||
series: {
|
series: {
|
||||||
slug: el.dataset.sslug!,
|
slug: el.dataset.sslug ?? '',
|
||||||
title: el.dataset.series!,
|
title: el.dataset.series ?? '',
|
||||||
cover: el.dataset.cover!,
|
cover: el.dataset.cover ?? '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// store this model for later use
|
||||||
|
el.dataset.ejson = JSON.stringify(episode);
|
||||||
|
|
||||||
// play button
|
// play button
|
||||||
el.getElementsByTagName('a').item(0)!.addEventListener('click', (e) => {
|
el.getElementsByTagName('a')
|
||||||
|
.item(0)
|
||||||
|
?.addEventListener('click', (e) => {
|
||||||
|
if (this.player.currentEpisode()?.id !== episode.id) {
|
||||||
this.player.playEpisode(episode);
|
this.player.playEpisode(episode);
|
||||||
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
// queue button
|
// queue button
|
||||||
el.getElementsByTagName('a').item(1)!.addEventListener('click', (e) => {
|
el.getElementsByTagName('a')
|
||||||
this.playlist.queueEpisode(episode);
|
.item(1)
|
||||||
|
?.addEventListener('click', (e) => {
|
||||||
|
if (!this.player.currentEpisode()) {
|
||||||
|
// if nothing is playing, first queued item gets loaded
|
||||||
|
// into the player but does not autoplay
|
||||||
|
this.player.playEpisode(episode, true);
|
||||||
|
} else if (this.player.currentEpisode()?.id !== episode.id) {
|
||||||
|
if (this.playlist.isQueued(episode)) {
|
||||||
|
this.playlist.removeEpisode(episode);
|
||||||
|
} else {
|
||||||
|
this.playlist.pushEpisode(episode);
|
||||||
|
}
|
||||||
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// trigger the playlist update function to mark queued episodes
|
||||||
|
this.episodeStateChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
// series filter input
|
// series filter input
|
||||||
const filter = <HTMLInputElement>this.main
|
const filter = <HTMLInputElement>(
|
||||||
.getElementsByClassName('filter').item(0);
|
this.main.getElementsByClassName('filter').item(0)
|
||||||
|
);
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
if (this.lastSearch) {
|
if (this.lastSearch) {
|
||||||
|
@ -66,11 +107,12 @@ class Radiostasis {
|
||||||
this.debouncer = setTimeout(() => {
|
this.debouncer = setTimeout(() => {
|
||||||
this.lastSearch = filter.value.toLowerCase();
|
this.lastSearch = filter.value.toLowerCase();
|
||||||
const terms = this.lastSearch.split(' ');
|
const terms = this.lastSearch.split(' ');
|
||||||
for (var i = 0; i < allSeries.length; i++) {
|
for (let i = 0; i < allSeries.length; i++) {
|
||||||
const series = allSeries.item(i)!;
|
const series = allSeries.item(i);
|
||||||
|
if (!series || !series.dataset.filter) continue;
|
||||||
let match = true;
|
let match = true;
|
||||||
for (let term of terms) {
|
for (const term of terms) {
|
||||||
if (term.length > 0 && series.dataset.filter!.indexOf(term) < 0) {
|
if (term.length > 0 && series.dataset.filter.indexOf(term) < 0) {
|
||||||
match = false;
|
match = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -83,10 +125,28 @@ class Radiostasis {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public initialize(): void {
|
private episodeStateChanged(): void {
|
||||||
const path = location.pathname == '/'
|
const episodes = this.main.getElementsByClassName('episode');
|
||||||
? '/partial/home.html'
|
for (let i = 0; i < episodes.length; i++) {
|
||||||
: `/partial/${location.pathname}.html`;
|
const el = <HTMLElement>episodes.item(i);
|
||||||
htmx.ajax('GET', path, 'main');
|
if (!el || !el.dataset.ejson) continue;
|
||||||
|
const episode = <Episode>JSON.parse(el.dataset.ejson);
|
||||||
|
if (this.player.currentEpisode()?.id === episode.id) {
|
||||||
|
el.classList.add('playing');
|
||||||
|
getOrThrow(el.getElementsByTagName('a').item(0)).innerHTML = 'Playing';
|
||||||
|
} else {
|
||||||
|
el.classList.remove('playing');
|
||||||
|
getOrThrow(el.getElementsByTagName('a').item(0)).innerHTML =
|
||||||
|
'Play Episode';
|
||||||
|
}
|
||||||
|
const queueBtn = getOrThrow(el.getElementsByTagName('a').item(1));
|
||||||
|
if (this.playlist.isQueued(episode)) {
|
||||||
|
queueBtn.classList.add('queued');
|
||||||
|
queueBtn.innerHTML = 'Remove from Queue';
|
||||||
|
} else {
|
||||||
|
queueBtn.classList.remove('queued');
|
||||||
|
queueBtn.innerHTML = 'Queue Episode';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
function getOrThrow<T>(element: T | null): T {
|
||||||
|
if (!element) {
|
||||||
|
throw new Error("tried to get an element that doesn't exist");
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// start application
|
// start application
|
||||||
new Radiostasis().initialize();
|
new Radiostasis().initialize();
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"lint": "eslint"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||||
|
"@typescript-eslint/parser": "^5.57.1",
|
||||||
|
"@typescript-eslint/typescript-estree": "^5.57.1",
|
||||||
|
"eslint": "^8.38.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"prettier": "^2.3.2",
|
||||||
|
"typescript": "^5.0.4",
|
||||||
|
"typescript-eslint-language-service": "^5.0.5",
|
||||||
|
"typescript-language-server": "^3.3.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,5 +8,6 @@
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"outFile": "../site/radiostasis.js",
|
"outFile": "../site/radiostasis.js",
|
||||||
"lib": ["DOM", "ESNext"],
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"plugins": [{"name": "typescript-eslint-language-service"}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue