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