working on queue functionality

This commit is contained in:
Rudis Muiznieks 2023-04-09 15:40:40 -05:00
parent f52300bfcf
commit e769ec5566
Signed by: rudism
GPG Key ID: CABF2F86EF7884F9
16 changed files with 3519 additions and 415 deletions

View File

@ -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>

3
site/icon-trash.svg Normal file
View File

@ -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

View File

@ -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>

View File

@ -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')
_this.player.playEpisode(episode); .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(); 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();

View File

@ -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;
}
} }

10
src/.editorconfig Normal file
View File

@ -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

21
src/.eslintrc.json Normal file
View File

@ -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"
}
}

3
src/.prettierrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"singleQuote": true
}

View File

@ -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;
};

View File

@ -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, {
sizes: '256x256', src: this.episode.series.cover,
type: 'image/jpeg', sizes: '256x256',
}], 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();

View File

@ -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;
} }
} }

View File

@ -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')
this.player.playEpisode(episode); .item(0)
e.preventDefault(); ?.addEventListener('click', (e) => {
}); if (this.player.currentEpisode()?.id !== episode.id) {
this.player.playEpisode(episode);
}
e.preventDefault();
});
// queue button // queue button
el.getElementsByTagName('a').item(1)!.addEventListener('click', (e) => { el.getElementsByTagName('a')
this.playlist.queueEpisode(episode); .item(1)
e.preventDefault(); ?.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 // 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';
}
}
} }
} }

View File

@ -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();

2857
src/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
src/package.json Normal file
View File

@ -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"
}
}

View File

@ -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"}]
} }
} }