working on queue functionality
This commit is contained in:
parent
f52300bfcf
commit
e769ec5566
|
@ -316,6 +316,8 @@ private void GenerateSeriesDetailsFragment(Series series) {
|
|||
data-sslug='{series.SlugEncoded}'
|
||||
data-cover='/cover/sm/{series.SlugEncoded}.jpg'
|
||||
data-series='{series.TitleEncoded}'
|
||||
data-length='{episode.LengthDisplay}'
|
||||
data-size='{episode.FileSize}'
|
||||
data-file='{episode.FileNameEncoded}'>
|
||||
<label>{episode.TitleEncoded}</label>
|
||||
<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>
|
||||
<h3>Up Next:</h3>
|
||||
<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>
|
||||
</div>
|
||||
<footer>
|
||||
|
|
|
@ -39,20 +39,21 @@ var Player = /** @class */ (function () {
|
|||
function Player(playlist) {
|
||||
var _this = this;
|
||||
this.playlist = playlist;
|
||||
var controls = document.getElementById('controls');
|
||||
var timeVolume = document.getElementById('timeVolume');
|
||||
this.nowPlaying = document.getElementById('nowPlaying');
|
||||
this.rewButton = controls.getElementsByTagName('button').item(0);
|
||||
this.playButton = controls.getElementsByTagName('button').item(1);
|
||||
this.playButtonPath = this.playButton.getElementsByTagName('path').item(0);
|
||||
this.ffwButton = controls.getElementsByTagName('button').item(2);
|
||||
this.skipButton = controls.getElementsByTagName('button').item(3);
|
||||
this.cover = this.nowPlaying.getElementsByTagName('img').item(0);
|
||||
this.seriesName = this.nowPlaying.getElementsByTagName('span').item(0);
|
||||
this.episodeName = this.nowPlaying.getElementsByTagName('span').item(1);
|
||||
this.timeDisplay = timeVolume.getElementsByTagName('span').item(0);
|
||||
this.volumeSlider = timeVolume.getElementsByTagName('input').item(0);
|
||||
this.progress = document.getElementById('progress');
|
||||
this.playlist.setPlayEpisodeHandler(function (episode) { return _this.playEpisode(episode); });
|
||||
var controls = getOrThrow(document.getElementById('controls'));
|
||||
var timeVolume = getOrThrow(document.getElementById('timeVolume'));
|
||||
this.nowPlaying = getOrThrow(document.getElementById('nowPlaying'));
|
||||
this.rewButton = getOrThrow(controls.getElementsByTagName('button').item(0));
|
||||
this.playButton = getOrThrow(controls.getElementsByTagName('button').item(1));
|
||||
this.playButtonPath = getOrThrow(this.playButton.getElementsByTagName('path').item(0));
|
||||
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.howl = null;
|
||||
this.episode = null;
|
||||
|
@ -83,23 +84,23 @@ var Player = /** @class */ (function () {
|
|||
});
|
||||
}
|
||||
// set up playlist changed handler
|
||||
this.playlist.setPlaylistChangedHandler(function () {
|
||||
this.playlist.addPlaylistChangedHandler(function () {
|
||||
_this.skipButton.disabled = !_this.playlist.hasNextEpisode();
|
||||
});
|
||||
}
|
||||
Player.prototype.setErrorUI = function (message) {
|
||||
this.stopPlaybackAndResetUi();
|
||||
this.seriesName.innerHTML = 'Error playing episode';
|
||||
this.episodeName.innerHTML = message;
|
||||
this.nowPlaying.classList.add('error');
|
||||
Player.prototype.currentEpisode = function () {
|
||||
return this.episode;
|
||||
};
|
||||
Player.prototype.playEpisode = function (episode) {
|
||||
Player.prototype.playEpisode = function (episode, paused) {
|
||||
var _this = this;
|
||||
if (paused === void 0) { paused = false; }
|
||||
this.stopPlaybackAndResetUi();
|
||||
this.setPauseButtonUI();
|
||||
this.episode = episode;
|
||||
this.playlist.removeEpisode(episode);
|
||||
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 _this = this;
|
||||
return __generator(this, function (_a) {
|
||||
|
@ -115,15 +116,17 @@ var Player = /** @class */ (function () {
|
|||
this.howl = new Howl({
|
||||
src: "".concat(link.url, "?Authorization=").concat(link.token),
|
||||
html5: true,
|
||||
autoplay: true,
|
||||
autoplay: !paused,
|
||||
volume: this.getVolume(),
|
||||
onload: function () {
|
||||
_this.updateTimeUI();
|
||||
_this.sendMediaSessionMetadata();
|
||||
},
|
||||
onplay: function () {
|
||||
_this.updateNowPlayingUI();
|
||||
_this.sendMediaSessionMetadata();
|
||||
if (paused)
|
||||
_this.setPlayButtonUI();
|
||||
},
|
||||
onplay: function () {
|
||||
_this.setPauseButtonUI();
|
||||
_this.startTicker();
|
||||
},
|
||||
|
@ -137,7 +140,16 @@ var Player = /** @class */ (function () {
|
|||
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 () {
|
||||
if (this.howl && this.howl.playing())
|
||||
|
@ -184,9 +196,7 @@ var Player = /** @class */ (function () {
|
|||
? 'Loading episode'
|
||||
: this.episode.series.title;
|
||||
this.episodeName.innerHTML = this.episode.title;
|
||||
this.cover.src = loading
|
||||
? '/loading.gif'
|
||||
: this.episode.series.cover;
|
||||
this.cover.src = loading ? '/loading.gif' : this.episode.series.cover;
|
||||
this.nowPlaying.title = "".concat(this.episode.series.title, "\n").concat(this.episode.title);
|
||||
}
|
||||
else {
|
||||
|
@ -214,11 +224,13 @@ var Player = /** @class */ (function () {
|
|||
title: this.episode.title,
|
||||
album: 'Radiostasis',
|
||||
artist: this.episode.series.title,
|
||||
artwork: [{
|
||||
artwork: [
|
||||
{
|
||||
src: this.episode.series.cover,
|
||||
sizes: '256x256',
|
||||
type: 'image/jpeg',
|
||||
}],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
else {
|
||||
|
@ -228,7 +240,7 @@ var Player = /** @class */ (function () {
|
|||
};
|
||||
Player.prototype.timeToDisplayString = function (time) {
|
||||
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();
|
||||
return "".concat(mins, ":").concat(secStr);
|
||||
};
|
||||
|
@ -236,7 +248,7 @@ var Player = /** @class */ (function () {
|
|||
if (this.howl && this.howl.state() === 'loaded') {
|
||||
var total = this.howl.duration();
|
||||
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));
|
||||
// set the new values if they've changed since the last tick
|
||||
if (this.timeDisplay.innerHTML !== timeStamp)
|
||||
|
@ -278,17 +290,77 @@ var Playlist = /** @class */ (function () {
|
|||
function Playlist() {
|
||||
var _this = this;
|
||||
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.queueTab = this.queueContainer.getElementsByTagName('h2').item(0);
|
||||
this.overlay = document.getElementById('overlay');
|
||||
this.queueTab = getOrThrow(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', 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 () {
|
||||
if (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.pointerEvents = 'auto';
|
||||
}
|
||||
|
@ -299,61 +371,128 @@ var Playlist = /** @class */ (function () {
|
|||
this.overlay.style.pointerEvents = 'none';
|
||||
}
|
||||
};
|
||||
Playlist.prototype.setPlaylistChangedHandler = function (handler) {
|
||||
this.changedHandler = handler;
|
||||
};
|
||||
Playlist.prototype.hasNextEpisode = function () {
|
||||
return false;
|
||||
};
|
||||
Playlist.prototype.queueEpisode = function (episode) {
|
||||
Playlist.prototype.createQueueListItem = function (episode) {
|
||||
var _this = this;
|
||||
var item = document.createElement('li');
|
||||
item.classList.add('episode');
|
||||
item.title = episode.title;
|
||||
item.dataset.slug = episode.slug;
|
||||
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;
|
||||
}());
|
||||
var Radiostasis = /** @class */ (function () {
|
||||
function Radiostasis() {
|
||||
var _this = this;
|
||||
var _a;
|
||||
this.lastSearch = null;
|
||||
this.debouncer = null;
|
||||
this.playlist = new 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
|
||||
document.addEventListener('htmx:historyRestore', function () { return _this.wireLoadedFragment(); });
|
||||
document.getElementsByTagName('main').item(0).addEventListener('htmx:afterSwap', function () { return _this.wireLoadedFragment(); });
|
||||
document.addEventListener('htmx:historyRestore', function () {
|
||||
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 () {
|
||||
var _this = this;
|
||||
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
||||
// episode play and queue buttons
|
||||
var episodes = this.main.getElementsByClassName('episode');
|
||||
var _loop_1 = function (i) {
|
||||
var el = episodes.item(i);
|
||||
var episode = {
|
||||
slug: el.dataset.slug,
|
||||
title: el.getAttribute('title'),
|
||||
file: el.dataset.file,
|
||||
id: "".concat((_a = el.dataset.sslug) !== null && _a !== void 0 ? _a : '', "/").concat((_b = el.dataset.slug) !== null && _b !== void 0 ? _b : ''),
|
||||
slug: (_c = el.dataset.slug) !== null && _c !== void 0 ? _c : '',
|
||||
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: {
|
||||
slug: el.dataset.sslug,
|
||||
title: el.dataset.series,
|
||||
cover: el.dataset.cover,
|
||||
slug: (_h = el.dataset.sslug) !== null && _h !== void 0 ? _h : '',
|
||||
title: (_j = el.dataset.series) !== null && _j !== void 0 ? _j : '',
|
||||
cover: (_k = el.dataset.cover) !== null && _k !== void 0 ? _k : '',
|
||||
},
|
||||
};
|
||||
// store this model for later use
|
||||
el.dataset.ejson = JSON.stringify(episode);
|
||||
// play button
|
||||
el.getElementsByTagName('a').item(0).addEventListener('click', function (e) {
|
||||
_this.player.playEpisode(episode);
|
||||
(_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);
|
||||
}
|
||||
e.preventDefault();
|
||||
});
|
||||
// queue button
|
||||
el.getElementsByTagName('a').item(1).addEventListener('click', function (e) {
|
||||
_this.playlist.queueEpisode(episode);
|
||||
(_m = el.getElementsByTagName('a')
|
||||
.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();
|
||||
});
|
||||
// trigger the playlist update function to mark queued episodes
|
||||
this_1.episodeStateChanged();
|
||||
};
|
||||
var this_1 = this;
|
||||
for (var i = 0; i < episodes.length; i++) {
|
||||
_loop_1(i);
|
||||
}
|
||||
// series filter input
|
||||
var filter = this.main
|
||||
.getElementsByClassName('filter').item(0);
|
||||
var filter = (this.main.getElementsByClassName('filter').item(0));
|
||||
if (filter) {
|
||||
if (this.lastSearch) {
|
||||
filter.value = this.lastSearch;
|
||||
|
@ -368,6 +507,8 @@ var Radiostasis = /** @class */ (function () {
|
|||
var terms = _this.lastSearch.split(' ');
|
||||
for (var i = 0; i < allSeries_1.length; i++) {
|
||||
var series = allSeries_1.item(i);
|
||||
if (!series || !series.dataset.filter)
|
||||
continue;
|
||||
var match = true;
|
||||
for (var _i = 0, terms_1 = terms; _i < terms_1.length; _i++) {
|
||||
var term = terms_1[_i];
|
||||
|
@ -385,14 +526,42 @@ var Radiostasis = /** @class */ (function () {
|
|||
});
|
||||
}
|
||||
};
|
||||
Radiostasis.prototype.initialize = function () {
|
||||
var path = location.pathname == '/'
|
||||
? '/partial/home.html'
|
||||
: "/partial/".concat(location.pathname, ".html");
|
||||
htmx.ajax('GET', path, 'main');
|
||||
Radiostasis.prototype.episodeStateChanged = function () {
|
||||
var _a;
|
||||
var episodes = this.main.getElementsByClassName('episode');
|
||||
for (var i = 0; i < episodes.length; i++) {
|
||||
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;
|
||||
}());
|
||||
function getOrThrow(element) {
|
||||
if (!element) {
|
||||
throw new Error("tried to get an element that doesn't exist");
|
||||
}
|
||||
return element;
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// start application
|
||||
new Radiostasis().initialize();
|
||||
|
|
|
@ -284,11 +284,10 @@ ol > li:not(:last-child) {
|
|||
#queue-container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
max-width: 120ch;
|
||||
border: 2px solid black;
|
||||
transition: height 0.25s ease-out;
|
||||
height: 10ex;
|
||||
min-width: 25%;
|
||||
min-width: 42ch;
|
||||
background: white;
|
||||
margin: 0 1ch;
|
||||
}
|
||||
|
@ -328,10 +327,10 @@ ol > li:not(:last-child) {
|
|||
}
|
||||
|
||||
#queue-container ol {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
padding-left: 5ch;
|
||||
padding-right: 1rem;
|
||||
margin: 0 1rem;
|
||||
max-height: calc(100% - 16ex);
|
||||
max-height: calc(100% - 15ex);
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
|
@ -532,6 +531,16 @@ h2 svg {
|
|||
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 {
|
||||
margin: 0.25rem 0;
|
||||
font-size: 0.8em;
|
||||
|
@ -541,7 +550,7 @@ h2 svg {
|
|||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.controls a {
|
||||
.controls a, .playing .controls a:hover {
|
||||
display: block;
|
||||
padding: 0.25rem 0.5rem 0.25rem 1.25rem;
|
||||
border: 1px solid black;
|
||||
|
@ -560,6 +569,10 @@ h2 svg {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.playing .controls {
|
||||
opacity: 0.33;
|
||||
}
|
||||
|
||||
.controls a:first-child {
|
||||
background-image: url('/icon-play.svg');
|
||||
}
|
||||
|
@ -568,6 +581,10 @@ h2 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) {
|
||||
.seriesDetails section {
|
||||
font-size: 1em;
|
||||
|
@ -592,4 +609,11 @@ h2 svg {
|
|||
.detail article {
|
||||
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 = {
|
||||
slug: string,
|
||||
title: string,
|
||||
cover: string,
|
||||
}
|
||||
slug: string;
|
||||
title: string;
|
||||
cover: string;
|
||||
};
|
||||
|
||||
type Episode = {
|
||||
series: Series,
|
||||
slug: string,
|
||||
title: string,
|
||||
file: string,
|
||||
}
|
||||
id: string;
|
||||
series: Series;
|
||||
slug: string;
|
||||
title: string;
|
||||
file: string;
|
||||
length: string;
|
||||
size: number;
|
||||
};
|
||||
|
||||
type UrlResponse = {
|
||||
url: string;
|
||||
token: string;
|
||||
};
|
||||
|
|
190
src/Player.ts
190
src/Player.ts
|
@ -25,20 +25,41 @@ class Player {
|
|||
|
||||
public constructor(playlist: Playlist) {
|
||||
this.playlist = playlist;
|
||||
const controls = document.getElementById('controls')!;
|
||||
const timeVolume = document.getElementById('timeVolume')!;
|
||||
this.nowPlaying = document.getElementById('nowPlaying')!;
|
||||
this.rewButton = controls.getElementsByTagName('button').item(0)!;
|
||||
this.playButton = controls.getElementsByTagName('button').item(1)!;
|
||||
this.playButtonPath = this.playButton.getElementsByTagName('path').item(0)!;
|
||||
this.ffwButton = controls.getElementsByTagName('button').item(2)!;
|
||||
this.skipButton = controls.getElementsByTagName('button').item(3)!;
|
||||
this.cover = this.nowPlaying.getElementsByTagName('img').item(0)!;
|
||||
this.seriesName = this.nowPlaying.getElementsByTagName('span').item(0)!;
|
||||
this.episodeName = this.nowPlaying.getElementsByTagName('span').item(1)!;
|
||||
this.timeDisplay = timeVolume.getElementsByTagName('span').item(0)!;
|
||||
this.volumeSlider = timeVolume.getElementsByTagName('input').item(0)!;
|
||||
this.progress = document.getElementById('progress')!;
|
||||
this.playlist.setPlayEpisodeHandler((episode) => this.playEpisode(episode));
|
||||
const controls = getOrThrow(document.getElementById('controls'));
|
||||
const timeVolume = getOrThrow(document.getElementById('timeVolume'));
|
||||
this.nowPlaying = getOrThrow(document.getElementById('nowPlaying'));
|
||||
this.rewButton = getOrThrow(
|
||||
controls.getElementsByTagName('button').item(0)
|
||||
);
|
||||
this.playButton = getOrThrow(
|
||||
controls.getElementsByTagName('button').item(1)
|
||||
);
|
||||
this.playButtonPath = getOrThrow(
|
||||
this.playButton.getElementsByTagName('path').item(0)
|
||||
);
|
||||
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.howl = null;
|
||||
this.episode = null;
|
||||
|
@ -57,23 +78,74 @@ class Player {
|
|||
navigator.mediaSession.setActionHandler('pause', () => this.playPause());
|
||||
navigator.mediaSession.setActionHandler('play', () => this.playPause());
|
||||
navigator.mediaSession.setActionHandler('stop', () =>
|
||||
this.stopPlaybackAndResetUi());
|
||||
this.stopPlaybackAndResetUi()
|
||||
);
|
||||
navigator.mediaSession.setActionHandler('seekforward', () =>
|
||||
this.fastForward());
|
||||
this.fastForward()
|
||||
);
|
||||
navigator.mediaSession.setActionHandler('seekbackward', () =>
|
||||
this.rewind());
|
||||
this.rewind()
|
||||
);
|
||||
// don't support previous track yet, queue removes them once finished
|
||||
// wire this up to rewind instead
|
||||
navigator.mediaSession.setActionHandler('previoustrack', () =>
|
||||
this.rewind());
|
||||
this.rewind()
|
||||
);
|
||||
}
|
||||
|
||||
// set up playlist changed handler
|
||||
this.playlist.setPlaylistChangedHandler(() => {
|
||||
this.playlist.addPlaylistChangedHandler(() => {
|
||||
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 {
|
||||
this.stopPlaybackAndResetUi();
|
||||
this.seriesName.innerHTML = 'Error playing episode';
|
||||
|
@ -81,43 +153,6 @@ class Player {
|
|||
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 {
|
||||
if (this.howl && this.howl.playing()) this.howl.pause();
|
||||
else if (this.howl && !this.howl.playing()) this.howl.play();
|
||||
|
@ -142,13 +177,17 @@ class Player {
|
|||
}
|
||||
|
||||
private setPlayButtonUI(): void {
|
||||
this.playButtonPath.setAttribute('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');
|
||||
this.playButtonPath.setAttribute(
|
||||
'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 {
|
||||
this.playButtonPath.setAttribute('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');
|
||||
this.playButtonPath.setAttribute(
|
||||
'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 {
|
||||
|
@ -159,16 +198,14 @@ class Player {
|
|||
this.howl?.volume(this.getVolume());
|
||||
}
|
||||
|
||||
private updateNowPlayingUI(loading: boolean = false) {
|
||||
private updateNowPlayingUI(loading = false): void {
|
||||
this.nowPlaying.classList.remove('error');
|
||||
if (this.episode) {
|
||||
this.seriesName.innerHTML = loading
|
||||
? 'Loading episode'
|
||||
: this.episode.series.title;
|
||||
this.episodeName.innerHTML = this.episode.title;
|
||||
this.cover.src = loading
|
||||
? '/loading.gif'
|
||||
: this.episode.series.cover;
|
||||
this.cover.src = loading ? '/loading.gif' : this.episode.series.cover;
|
||||
this.nowPlaying.title = `${this.episode.series.title}\n${this.episode.title}`;
|
||||
} else {
|
||||
this.seriesName.innerHTML = 'No episode playing';
|
||||
|
@ -190,18 +227,20 @@ class Player {
|
|||
this.skipButton.disabled = !this.playlist.hasNextEpisode();
|
||||
}
|
||||
|
||||
private sendMediaSessionMetadata() {
|
||||
private sendMediaSessionMetadata(): void {
|
||||
if ('mediaSession' in navigator) {
|
||||
if (this.episode) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: this.episode.title,
|
||||
album: 'Radiostasis',
|
||||
artist: this.episode.series.title,
|
||||
artwork: [{
|
||||
src: this.episode.series.cover,
|
||||
sizes: '256x256',
|
||||
type: 'image/jpeg',
|
||||
}],
|
||||
artwork: [
|
||||
{
|
||||
src: this.episode.series.cover,
|
||||
sizes: '256x256',
|
||||
type: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
navigator.mediaSession.metadata = null;
|
||||
|
@ -211,7 +250,7 @@ class Player {
|
|||
|
||||
private timeToDisplayString(time: number): string {
|
||||
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();
|
||||
return `${mins}:${secStr}`;
|
||||
}
|
||||
|
@ -220,9 +259,10 @@ class Player {
|
|||
if (this.howl && this.howl.state() === 'loaded') {
|
||||
const total = this.howl.duration();
|
||||
const current = this.howl.seek();
|
||||
const pct = `${Math.round(current / total * 1000) / 10}%`;
|
||||
const timeStamp =
|
||||
`${this.timeToDisplayString(current)} / ${this.timeToDisplayString(total)}`;
|
||||
const pct = `${Math.round((current / total) * 1000) / 10}%`;
|
||||
const timeStamp = `${this.timeToDisplayString(
|
||||
current
|
||||
)} / ${this.timeToDisplayString(total)}`;
|
||||
|
||||
// set the new values if they've changed since the last tick
|
||||
if (this.timeDisplay.innerHTML !== timeStamp)
|
||||
|
@ -248,7 +288,7 @@ class Player {
|
|||
}
|
||||
|
||||
private stopPlaybackAndResetUi(): void {
|
||||
this.howl?.unload()
|
||||
this.howl?.unload();
|
||||
this.howl = null;
|
||||
this.episode = null;
|
||||
this.setPlayButtonUI();
|
||||
|
|
127
src/Playlist.ts
127
src/Playlist.ts
|
@ -2,26 +2,99 @@ class Playlist {
|
|||
private readonly queueContainer: HTMLElement;
|
||||
private readonly queueTab: HTMLElement;
|
||||
private readonly overlay: HTMLElement;
|
||||
private readonly queueList: HTMLOListElement;
|
||||
private readonly queueInitialHeight: string;
|
||||
private readonly queueExpandedHeight = 'calc(100% - 6ex)';
|
||||
|
||||
// changed event handler
|
||||
private changedHandler?: () => void;
|
||||
// event handlers
|
||||
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() {
|
||||
this.queueContainer = document.getElementById('queue-container')!;
|
||||
this.queueContainer = getOrThrow(
|
||||
document.getElementById('queue-container')
|
||||
);
|
||||
this.queueInitialHeight = getComputedStyle(this.queueContainer).height;
|
||||
this.queueTab = this.queueContainer.getElementsByTagName('h2').item(0)!;
|
||||
this.overlay = document.getElementById('overlay')!;
|
||||
this.queueTab = getOrThrow(
|
||||
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.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 {
|
||||
if (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.pointerEvents = 'auto';
|
||||
} else {
|
||||
|
@ -32,14 +105,38 @@ class Playlist {
|
|||
}
|
||||
}
|
||||
|
||||
public setPlaylistChangedHandler(handler: () => void) {
|
||||
this.changedHandler = handler;
|
||||
}
|
||||
|
||||
public hasNextEpisode(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public queueEpisode(episode: Episode): void {
|
||||
private createQueueListItem(episode: Episode): HTMLLIElement {
|
||||
const item = document.createElement('li');
|
||||
item.classList.add('episode');
|
||||
item.title = episode.title;
|
||||
item.dataset.slug = episode.slug;
|
||||
item.dataset.sslug = episode.series.slug;
|
||||
item.dataset.cover = episode.series.cover;
|
||||
item.dataset.series = episode.series.title;
|
||||
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.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
|
||||
document.addEventListener('htmx:historyRestore', () => this.wireLoadedFragment());
|
||||
document.getElementsByTagName('main').item(0)!.addEventListener(
|
||||
'htmx:afterSwap', () => this.wireLoadedFragment());
|
||||
document.addEventListener('htmx:historyRestore', () =>
|
||||
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 {
|
||||
// episode play and queue buttons
|
||||
const episodes = this.main.getElementsByClassName('episode');
|
||||
for (let i = 0; i < episodes.length; i++) {
|
||||
const el = <HTMLElement>episodes.item(i)!;
|
||||
const el = <HTMLElement>episodes.item(i);
|
||||
const episode: Episode = {
|
||||
slug: el.dataset.slug!,
|
||||
title: el.getAttribute('title')!,
|
||||
file: el.dataset.file!,
|
||||
id: `${el.dataset.sslug ?? ''}/${el.dataset.slug ?? ''}`,
|
||||
slug: el.dataset.slug ?? '',
|
||||
title: el.getAttribute('title') ?? '',
|
||||
file: el.dataset.file ?? '',
|
||||
length: el.dataset.length ?? '',
|
||||
size: parseInt(el.dataset.size ?? '0'),
|
||||
series: {
|
||||
slug: el.dataset.sslug!,
|
||||
title: el.dataset.series!,
|
||||
cover: el.dataset.cover!,
|
||||
slug: el.dataset.sslug ?? '',
|
||||
title: el.dataset.series ?? '',
|
||||
cover: el.dataset.cover ?? '',
|
||||
},
|
||||
};
|
||||
|
||||
// store this model for later use
|
||||
el.dataset.ejson = JSON.stringify(episode);
|
||||
|
||||
// play button
|
||||
el.getElementsByTagName('a').item(0)!.addEventListener('click', (e) => {
|
||||
this.player.playEpisode(episode);
|
||||
e.preventDefault();
|
||||
});
|
||||
el.getElementsByTagName('a')
|
||||
.item(0)
|
||||
?.addEventListener('click', (e) => {
|
||||
if (this.player.currentEpisode()?.id !== episode.id) {
|
||||
this.player.playEpisode(episode);
|
||||
}
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// queue button
|
||||
el.getElementsByTagName('a').item(1)!.addEventListener('click', (e) => {
|
||||
this.playlist.queueEpisode(episode);
|
||||
e.preventDefault();
|
||||
});
|
||||
el.getElementsByTagName('a')
|
||||
.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();
|
||||
});
|
||||
|
||||
// trigger the playlist update function to mark queued episodes
|
||||
this.episodeStateChanged();
|
||||
}
|
||||
|
||||
// series filter input
|
||||
const filter = <HTMLInputElement>this.main
|
||||
.getElementsByClassName('filter').item(0);
|
||||
const filter = <HTMLInputElement>(
|
||||
this.main.getElementsByClassName('filter').item(0)
|
||||
);
|
||||
|
||||
if (filter) {
|
||||
if (this.lastSearch) {
|
||||
|
@ -66,11 +107,12 @@ class Radiostasis {
|
|||
this.debouncer = setTimeout(() => {
|
||||
this.lastSearch = filter.value.toLowerCase();
|
||||
const terms = this.lastSearch.split(' ');
|
||||
for (var i = 0; i < allSeries.length; i++) {
|
||||
const series = allSeries.item(i)!;
|
||||
for (let i = 0; i < allSeries.length; i++) {
|
||||
const series = allSeries.item(i);
|
||||
if (!series || !series.dataset.filter) continue;
|
||||
let match = true;
|
||||
for (let term of terms) {
|
||||
if (term.length > 0 && series.dataset.filter!.indexOf(term) < 0) {
|
||||
for (const term of terms) {
|
||||
if (term.length > 0 && series.dataset.filter.indexOf(term) < 0) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
|
@ -83,10 +125,28 @@ class Radiostasis {
|
|||
}
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
const path = location.pathname == '/'
|
||||
? '/partial/home.html'
|
||||
: `/partial/${location.pathname}.html`;
|
||||
htmx.ajax('GET', path, 'main');
|
||||
private episodeStateChanged(): void {
|
||||
const episodes = this.main.getElementsByClassName('episode');
|
||||
for (let i = 0; i < episodes.length; i++) {
|
||||
const el = <HTMLElement>episodes.item(i);
|
||||
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', () => {
|
||||
// start application
|
||||
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,
|
||||
"outFile": "../site/radiostasis.js",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"plugins": [{"name": "typescript-eslint-language-service"}]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue