big update with fixes and playlist/queuing capabilities #20

Merged
rudism merged 11 commits from typescript-queue into main 2023-04-09 19:30:52 -05:00
16 changed files with 3519 additions and 415 deletions
Showing only changes of commit e769ec5566 - Show all commits

View File

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

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

View File

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

View File

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

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 = {
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;
};

View File

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

View File

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

View File

@ -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) => {
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);
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';
}
}
}
}

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', () => {
// start application
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,
"outFile": "../site/radiostasis.js",
"lib": ["DOM", "ESNext"],
"plugins": [{"name": "typescript-eslint-language-service"}]
}
}