diff --git a/site/icon-pause.svg b/site/icon-pause.svg
new file mode 100644
index 0000000..68285b2
--- /dev/null
+++ b/site/icon-pause.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/site/radiostasis.js b/site/radiostasis.js
index 696352c..7f04322 100644
--- a/site/radiostasis.js
+++ b/site/radiostasis.js
@@ -38,10 +38,8 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
var Player = /** @class */ (function () {
function Player(playlist) {
var _this = this;
+ this.stateChangedHandlers = [];
this.playlist = playlist;
- this.playlist.setPlayEpisodeHandler(function (episode) { return _this.playEpisode(episode); });
- this.playlist.setNowPlayingEpisodeHandler(function () { return _this.currentEpisode(); });
- this.playlist.setStopHandler(function () { return _this.stopPlaybackAndResetUi(); });
var controls = getOrThrow(document.getElementById('controls'));
var timeVolume = getOrThrow(document.getElementById('timeVolume'));
this.nowPlaying = getOrThrow(document.getElementById('nowPlaying'));
@@ -94,6 +92,9 @@ var Player = /** @class */ (function () {
_this.skipButton.disabled = !_this.playlist.hasNextEpisode();
});
}
+ Player.prototype.addStateChangedHandler = function (handler) {
+ this.stateChangedHandlers.push(handler);
+ };
Player.prototype.currentEpisode = function () {
return this.episode;
};
@@ -135,10 +136,18 @@ var Player = /** @class */ (function () {
onplay: function () {
_this.setPauseButtonUI();
_this.startTicker();
+ for (var _i = 0, _a = _this.stateChangedHandlers; _i < _a.length; _i++) {
+ var handler = _a[_i];
+ handler();
+ }
},
onpause: function () {
_this.setPlayButtonUI();
_this.stopTicker();
+ for (var _i = 0, _a = _this.stateChangedHandlers; _i < _a.length; _i++) {
+ var handler = _a[_i];
+ handler();
+ }
},
onend: function () { return _this.nextEpisode(); },
onloaderror: function () { return _this.setErrorUI('Error playing episode.'); },
@@ -157,6 +166,38 @@ var Player = /** @class */ (function () {
}
return false;
};
+ Player.prototype.playPause = function () {
+ if (this.howl && this.howl.playing())
+ this.howl.pause();
+ else if (this.howl && !this.howl.playing())
+ this.howl.play();
+ };
+ Player.prototype.stopPlaybackAndResetUi = function () {
+ var _a;
+ (_a = this.howl) === null || _a === void 0 ? void 0 : _a.unload();
+ this.howl = null;
+ this.episode = null;
+ this.setPlayButtonUI();
+ this.updateNowPlayingUI();
+ this.sendMediaSessionMetadata();
+ this.stopTicker();
+ this.updateTimeUI();
+ for (var _i = 0, _b = this.stateChangedHandlers; _i < _b.length; _i++) {
+ var handler = _b[_i];
+ handler();
+ }
+ };
+ Player.prototype.nextEpisode = function () {
+ var next = this.playlist.nextEpisode();
+ if (next)
+ this.playEpisode(next);
+ else {
+ this.stopPlaybackAndResetUi();
+ // manually trigger playlist changed
+ // so that currently playing media gets wiped
+ this.playlist.triggerPlaylistChanged();
+ }
+ };
Player.prototype.setErrorUI = function (message) {
this.stopPlaybackAndResetUi();
Toastify({
@@ -171,12 +212,6 @@ var Player = /** @class */ (function () {
},
}).showToast();
};
- Player.prototype.playPause = function () {
- if (this.howl && this.howl.playing())
- this.howl.pause();
- else if (this.howl && !this.howl.playing())
- this.howl.play();
- };
Player.prototype.rewind = function () {
if (this.howl && this.howl.state() === 'loaded') {
var newPos = this.howl.seek() - 10;
@@ -195,17 +230,6 @@ var Player = /** @class */ (function () {
this.updateTimeUI();
}
};
- Player.prototype.nextEpisode = function () {
- var next = this.playlist.nextEpisode();
- if (next)
- this.playEpisode(next);
- else {
- this.stopPlaybackAndResetUi();
- // manually trigger playlist changed
- // so that currently playing media gets wiped
- this.playlist.triggerPlaylistChanged();
- }
- };
Player.prototype.setPlayButtonUI = function () {
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');
};
@@ -305,31 +329,18 @@ var Player = /** @class */ (function () {
this.ticker = null;
}
};
- Player.prototype.stopPlaybackAndResetUi = function () {
- var _a;
- (_a = this.howl) === null || _a === void 0 ? void 0 : _a.unload();
- this.howl = null;
- this.episode = null;
- this.setPlayButtonUI();
- this.updateNowPlayingUI();
- this.sendMediaSessionMetadata();
- this.stopTicker();
- this.updateTimeUI();
- };
return Player;
}());
var Playlist = /** @class */ (function () {
- function Playlist() {
+ function Playlist(playerDelegate) {
var _this = this;
this.queueExpandedHeight = 'calc(100% - 6ex)';
// event handlers
this.changedHandlers = [];
- this.nowPlayingEpisodeHandler = null;
- this.stopHandler = null;
- this.playEpisodeHandler = null;
// the actual episode queue
this.episodes = [];
this.episodeHash = new Set();
+ this.playerDelegate = playerDelegate;
this.queueContainer = getOrThrow(document.getElementById('queue-container'));
this.queueInitialHeight = getComputedStyle(this.queueContainer).height;
this.queueTab = getOrThrow(this.queueContainer.getElementsByTagName('h2').item(0));
@@ -340,20 +351,15 @@ var Playlist = /** @class */ (function () {
this.queueTab.addEventListener('click', function () { return _this.toggleQueueUI(); });
this.overlay.addEventListener('click', function () { return _this.toggleQueueUI(); });
this.clearButton.addEventListener('click', function () { return _this.clearPlaylist(); });
- this.unshiftCurrent();
}
+ Playlist.prototype.initialize = function () {
+ var _this = this;
+ this.playerDelegate().addStateChangedHandler(function () { return _this.stateChanged(); });
+ this.unshiftCurrent();
+ };
Playlist.prototype.addPlaylistChangedHandler = function (handler) {
this.changedHandlers.push(handler);
};
- Playlist.prototype.setPlayEpisodeHandler = function (handler) {
- this.playEpisodeHandler = handler;
- };
- Playlist.prototype.setNowPlayingEpisodeHandler = function (handler) {
- this.nowPlayingEpisodeHandler = handler;
- };
- Playlist.prototype.setStopHandler = function (handler) {
- this.stopHandler = handler;
- };
Playlist.prototype.hasNextEpisode = function () {
return this.episodes.length > 0;
};
@@ -383,9 +389,7 @@ var Playlist = /** @class */ (function () {
};
Playlist.prototype.unshiftEpisodes = function (episodes) {
this.shiftCurrent();
- var nowPlaying = this.nowPlayingEpisodeHandler
- ? this.nowPlayingEpisodeHandler()
- : null;
+ var nowPlaying = this.playerDelegate().currentEpisode();
if (nowPlaying) {
var litem = this.createQueueListItem(nowPlaying);
this.episodes.unshift([nowPlaying, litem]);
@@ -429,10 +433,8 @@ var Playlist = /** @class */ (function () {
};
Playlist.prototype.clearPlaylist = function () {
var removed = this.episodes.length;
- if (this.nowPlayingEpisodeHandler &&
- this.nowPlayingEpisodeHandler() != null) {
- if (this.stopHandler)
- this.stopHandler();
+ if (this.playerDelegate().currentEpisode() != null) {
+ this.playerDelegate().stopPlaybackAndResetUi();
removed++;
}
this.episodes.length = 0;
@@ -445,9 +447,7 @@ var Playlist = /** @class */ (function () {
}
};
Playlist.prototype.reQueueNowPlaying = function () {
- var currentEpisode = this.nowPlayingEpisodeHandler
- ? this.nowPlayingEpisodeHandler()
- : null;
+ var currentEpisode = this.playerDelegate().currentEpisode();
if (currentEpisode) {
this.unshiftEpisodes([currentEpisode]);
}
@@ -464,10 +464,12 @@ var Playlist = /** @class */ (function () {
var _a;
(_a = this.queueList.firstChild) === null || _a === void 0 ? void 0 : _a.remove();
};
+ Playlist.prototype.stateChanged = function () {
+ this.shiftCurrent();
+ this.unshiftCurrent();
+ };
Playlist.prototype.unshiftCurrent = function () {
- var currentEpisode = this.nowPlayingEpisodeHandler
- ? this.nowPlayingEpisodeHandler()
- : null;
+ var currentEpisode = this.playerDelegate().currentEpisode();
if (currentEpisode) {
this.queueList.prepend(this.createQueueListItem(currentEpisode, false));
}
@@ -499,9 +501,9 @@ var Playlist = /** @class */ (function () {
this.queueTab.classList.remove('expanded');
}
};
- Playlist.prototype.createQueueListItem = function (episode, withControls) {
+ Playlist.prototype.createQueueListItem = function (episode, queueControls) {
var _this = this;
- if (withControls === void 0) { withControls = true; }
+ if (queueControls === void 0) { queueControls = true; }
var item = document.createElement('li');
item.classList.add('episode');
item.title = episode.title;
@@ -513,25 +515,57 @@ var Playlist = /** @class */ (function () {
aside.appendChild(sspan);
item.appendChild(label);
item.appendChild(aside);
- if (withControls) {
+ if (queueControls) {
+ // play now/remove buttons
var controls = document.createElement('div');
controls.classList.add('controls');
var playBtn = document.createElement('a');
playBtn.href = '#';
playBtn.innerHTML = 'Play Now';
- playBtn.addEventListener('click', function () {
+ playBtn.addEventListener('click', function (e) {
_this.reQueueNowPlaying();
- if (_this.playEpisodeHandler)
- _this.playEpisodeHandler(episode);
+ _this.playerDelegate().playEpisode(episode);
+ e.preventDefault();
});
var remBtn = document.createElement('a');
remBtn.href = '#';
remBtn.innerHTML = 'Remove';
- remBtn.addEventListener('click', function () { return _this.removeEpisode(episode); });
+ remBtn.addEventListener('click', function (e) {
+ _this.removeEpisode(episode);
+ e.preventDefault();
+ });
controls.appendChild(playBtn);
controls.appendChild(remBtn);
item.appendChild(controls);
}
+ else {
+ // pause/skip buttons
+ item.classList.add('playing');
+ var controls = document.createElement('div');
+ controls.classList.add('controls');
+ var pauseBtn_1 = document.createElement('a');
+ pauseBtn_1.href = '#';
+ pauseBtn_1.innerHTML = this.playerDelegate().isPlaying() ? 'Pause' : 'Play';
+ if (this.playerDelegate().isPlaying()) {
+ pauseBtn_1.classList.add('pause');
+ }
+ pauseBtn_1.addEventListener('click', function () {
+ _this.playerDelegate().playPause();
+ pauseBtn_1.innerHTML = _this.playerDelegate().isPlaying()
+ ? 'Pause'
+ : 'Play';
+ });
+ var skipBtn = document.createElement('a');
+ skipBtn.href = '#';
+ skipBtn.innerHTML = 'Skip';
+ skipBtn.addEventListener('click', function (e) {
+ _this.playerDelegate().nextEpisode();
+ e.preventDefault();
+ });
+ controls.appendChild(pauseBtn_1);
+ controls.appendChild(skipBtn);
+ item.appendChild(controls);
+ }
return item;
};
Playlist.prototype.notify = function (message) {
@@ -555,8 +589,9 @@ var Radiostasis = /** @class */ (function () {
var _a;
this.lastSearch = null;
this.debouncer = null;
- this.playlist = new Playlist();
+ this.playlist = new Playlist(function () { return _this.player; });
this.player = new Player(this.playlist);
+ this.playlist.initialize();
this.main = getOrThrow(document.getElementsByTagName('main').item(0));
// set up htmx event handlers
document.addEventListener('htmx:historyRestore', function () {
diff --git a/site/style.css b/site/style.css
index 4fb7384..0a2d173 100644
--- a/site/style.css
+++ b/site/style.css
@@ -606,7 +606,7 @@ h2 svg {
text-decoration: underline;
}
-.playing .controls {
+.seriesDetails .playing .controls {
opacity: 0.33;
}
@@ -614,6 +614,10 @@ h2 svg {
background-image: url('/icon-play.svg');
}
+.playing .controls a.pause:first-child {
+ background-image: url('/icon-pause.svg');
+}
+
.controls a:last-child {
background-image: url('/icon-queue.svg');
}
diff --git a/src/Player.ts b/src/Player.ts
index 8178c21..e7e8646 100644
--- a/src/Player.ts
+++ b/src/Player.ts
@@ -22,12 +22,10 @@ class Player {
// currently playing episode and playlist
private episode: Episode | null;
private playlist: Playlist;
+ private stateChangedHandlers: Array<() => void> = [];
public constructor(playlist: Playlist) {
this.playlist = playlist;
- this.playlist.setPlayEpisodeHandler((episode) => this.playEpisode(episode));
- this.playlist.setNowPlayingEpisodeHandler(() => this.currentEpisode());
- this.playlist.setStopHandler(() => this.stopPlaybackAndResetUi());
const controls = getOrThrow(document.getElementById('controls'));
const timeVolume = getOrThrow(document.getElementById('timeVolume'));
@@ -106,6 +104,10 @@ class Player {
});
}
+ public addStateChangedHandler(handler: () => void): void {
+ this.stateChangedHandlers.push(handler);
+ }
+
public currentEpisode(): Episode | null {
return this.episode;
}
@@ -139,10 +141,16 @@ class Player {
onplay: (): void => {
this.setPauseButtonUI();
this.startTicker();
+ for (const handler of this.stateChangedHandlers) {
+ handler();
+ }
},
onpause: (): void => {
this.setPlayButtonUI();
this.stopTicker();
+ for (const handler of this.stateChangedHandlers) {
+ handler();
+ }
},
onend: () => this.nextEpisode(),
onloaderror: () => this.setErrorUI('Error playing episode.'),
@@ -160,6 +168,36 @@ class Player {
return false;
}
+ public playPause(): void {
+ if (this.howl && this.howl.playing()) this.howl.pause();
+ else if (this.howl && !this.howl.playing()) this.howl.play();
+ }
+
+ public stopPlaybackAndResetUi(): void {
+ this.howl?.unload();
+ this.howl = null;
+ this.episode = null;
+ this.setPlayButtonUI();
+ this.updateNowPlayingUI();
+ this.sendMediaSessionMetadata();
+ this.stopTicker();
+ this.updateTimeUI();
+ for (const handler of this.stateChangedHandlers) {
+ handler();
+ }
+ }
+
+ public nextEpisode(): void {
+ const next = this.playlist.nextEpisode();
+ if (next) this.playEpisode(next);
+ else {
+ this.stopPlaybackAndResetUi();
+ // manually trigger playlist changed
+ // so that currently playing media gets wiped
+ this.playlist.triggerPlaylistChanged();
+ }
+ }
+
private setErrorUI(message: string): void {
this.stopPlaybackAndResetUi();
Toastify({
@@ -175,11 +213,6 @@ class Player {
}).showToast();
}
- private playPause(): void {
- if (this.howl && this.howl.playing()) this.howl.pause();
- else if (this.howl && !this.howl.playing()) this.howl.play();
- }
-
private rewind(): void {
if (this.howl && this.howl.state() === 'loaded') {
let newPos = this.howl.seek() - 10;
@@ -198,17 +231,6 @@ class Player {
}
}
- private nextEpisode(): void {
- const next = this.playlist.nextEpisode();
- if (next) this.playEpisode(next);
- else {
- this.stopPlaybackAndResetUi();
- // manually trigger playlist changed
- // so that currently playing media gets wiped
- this.playlist.triggerPlaylistChanged();
- }
- }
-
private setPlayButtonUI(): void {
this.playButtonPath.setAttribute(
'd',
@@ -319,15 +341,4 @@ class Player {
this.ticker = null;
}
}
-
- private stopPlaybackAndResetUi(): void {
- this.howl?.unload();
- this.howl = null;
- this.episode = null;
- this.setPlayButtonUI();
- this.updateNowPlayingUI();
- this.sendMediaSessionMetadata();
- this.stopTicker();
- this.updateTimeUI();
- }
}
diff --git a/src/Playlist.ts b/src/Playlist.ts
index 96f153b..1ccaf94 100644
--- a/src/Playlist.ts
+++ b/src/Playlist.ts
@@ -10,15 +10,14 @@ class Playlist {
// event handlers
private changedHandlers: Array<() => void> = [];
- private nowPlayingEpisodeHandler: (() => Episode | null) | null = null;
- private stopHandler: (() => void) | null = null;
- private playEpisodeHandler: ((episode: Episode) => void) | null = null;
+ private playerDelegate: () => Player;
// the actual episode queue
private readonly episodes: Array<[Episode, HTMLLIElement]> = [];
private readonly episodeHash: Set = new Set();
- constructor() {
+ constructor(playerDelegate: () => Player) {
+ this.playerDelegate = playerDelegate;
this.queueContainer = getOrThrow(
document.getElementById('queue-container')
);
@@ -38,7 +37,10 @@ class Playlist {
this.queueTab.addEventListener('click', () => this.toggleQueueUI());
this.overlay.addEventListener('click', () => this.toggleQueueUI());
this.clearButton.addEventListener('click', () => this.clearPlaylist());
+ }
+ public initialize(): void {
+ this.playerDelegate().addStateChangedHandler(() => this.stateChanged());
this.unshiftCurrent();
}
@@ -46,18 +48,6 @@ class Playlist {
this.changedHandlers.push(handler);
}
- public setPlayEpisodeHandler(handler: (episode: Episode) => void): void {
- this.playEpisodeHandler = handler;
- }
-
- public setNowPlayingEpisodeHandler(handler: () => Episode | null): void {
- this.nowPlayingEpisodeHandler = handler;
- }
-
- public setStopHandler(handler: () => void): void {
- this.stopHandler = handler;
- }
-
public hasNextEpisode(): boolean {
return this.episodes.length > 0;
}
@@ -91,9 +81,7 @@ class Playlist {
public unshiftEpisodes(episodes: Array): void {
this.shiftCurrent();
- const nowPlaying = this.nowPlayingEpisodeHandler
- ? this.nowPlayingEpisodeHandler()
- : null;
+ const nowPlaying = this.playerDelegate().currentEpisode();
if (nowPlaying) {
const litem = this.createQueueListItem(nowPlaying);
this.episodes.unshift([nowPlaying, litem]);
@@ -142,11 +130,8 @@ class Playlist {
public clearPlaylist(): void {
let removed = this.episodes.length;
- if (
- this.nowPlayingEpisodeHandler &&
- this.nowPlayingEpisodeHandler() != null
- ) {
- if (this.stopHandler) this.stopHandler();
+ if (this.playerDelegate().currentEpisode() != null) {
+ this.playerDelegate().stopPlaybackAndResetUi();
removed++;
}
@@ -161,9 +146,7 @@ class Playlist {
}
public reQueueNowPlaying(): void {
- const currentEpisode = this.nowPlayingEpisodeHandler
- ? this.nowPlayingEpisodeHandler()
- : null;
+ const currentEpisode = this.playerDelegate().currentEpisode();
if (currentEpisode) {
this.unshiftEpisodes([currentEpisode]);
}
@@ -181,11 +164,13 @@ class Playlist {
this.queueList.firstChild?.remove();
}
- private unshiftCurrent(): void {
- const currentEpisode = this.nowPlayingEpisodeHandler
- ? this.nowPlayingEpisodeHandler()
- : null;
+ private stateChanged(): void {
+ this.shiftCurrent();
+ this.unshiftCurrent();
+ }
+ private unshiftCurrent(): void {
+ const currentEpisode = this.playerDelegate().currentEpisode();
if (currentEpisode) {
this.queueList.prepend(this.createQueueListItem(currentEpisode, false));
} else {
@@ -219,7 +204,7 @@ class Playlist {
private createQueueListItem(
episode: Episode,
- withControls = true
+ queueControls = true
): HTMLLIElement {
const item = document.createElement('li');
item.classList.add('episode');
@@ -232,23 +217,55 @@ class Playlist {
aside.appendChild(sspan);
item.appendChild(label);
item.appendChild(aside);
- if (withControls) {
+ if (queueControls) {
+ // play now/remove buttons
const controls = document.createElement('div');
controls.classList.add('controls');
const playBtn = document.createElement('a');
playBtn.href = '#';
playBtn.innerHTML = 'Play Now';
- playBtn.addEventListener('click', () => {
+ playBtn.addEventListener('click', (e) => {
this.reQueueNowPlaying();
- if (this.playEpisodeHandler) this.playEpisodeHandler(episode);
+ this.playerDelegate().playEpisode(episode);
+ e.preventDefault();
});
const remBtn = document.createElement('a');
remBtn.href = '#';
remBtn.innerHTML = 'Remove';
- remBtn.addEventListener('click', () => this.removeEpisode(episode));
+ remBtn.addEventListener('click', (e) => {
+ this.removeEpisode(episode);
+ e.preventDefault();
+ });
controls.appendChild(playBtn);
controls.appendChild(remBtn);
item.appendChild(controls);
+ } else {
+ // pause/skip buttons
+ item.classList.add('playing');
+ const controls = document.createElement('div');
+ controls.classList.add('controls');
+ const pauseBtn = document.createElement('a');
+ pauseBtn.href = '#';
+ pauseBtn.innerHTML = this.playerDelegate().isPlaying() ? 'Pause' : 'Play';
+ if (this.playerDelegate().isPlaying()) {
+ pauseBtn.classList.add('pause');
+ }
+ pauseBtn.addEventListener('click', () => {
+ this.playerDelegate().playPause();
+ pauseBtn.innerHTML = this.playerDelegate().isPlaying()
+ ? 'Pause'
+ : 'Play';
+ });
+ const skipBtn = document.createElement('a');
+ skipBtn.href = '#';
+ skipBtn.innerHTML = 'Skip';
+ skipBtn.addEventListener('click', (e) => {
+ this.playerDelegate().nextEpisode();
+ e.preventDefault();
+ });
+ controls.appendChild(pauseBtn);
+ controls.appendChild(skipBtn);
+ item.appendChild(controls);
}
return item;
}
diff --git a/src/Radiostasis.ts b/src/Radiostasis.ts
index b7e0194..9097756 100644
--- a/src/Radiostasis.ts
+++ b/src/Radiostasis.ts
@@ -10,8 +10,9 @@ class Radiostasis {
private debouncer: number | null = null;
constructor() {
- this.playlist = new Playlist();
+ this.playlist = new Playlist(() => this.player);
this.player = new Player(this.playlist);
+ this.playlist.initialize();
this.main = getOrThrow(document.getElementsByTagName('main').item(0));