diff --git a/db/migrations/010-date_updated.sql b/db/migrations/010-date_updated.sql
new file mode 100644
index 0000000..dca828a
--- /dev/null
+++ b/db/migrations/010-date_updated.sql
@@ -0,0 +1,11 @@
+alter table series
+add column date_updated timestamp;
+
+update series
+set date_updated='2023-04-15';
+
+pragma writable_schema=on;
+update sqlite_master
+set sql=replace(sql, 'date_updated timestamp', 'date_updated timestamp not null')
+where type='table' and name='series';
+pragma writable_schema=off;
diff --git a/site/index.html b/site/index.html
index 6b2d57c..ebbddc3 100644
--- a/site/index.html
+++ b/site/index.html
@@ -96,7 +96,7 @@
Playlist
diff --git a/site/radiostasis.js b/site/radiostasis.js
index a8eb16e..4e2ebb8 100644
--- a/site/radiostasis.js
+++ b/site/radiostasis.js
@@ -40,6 +40,8 @@ var Player = /** @class */ (function () {
var _this = this;
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'));
@@ -161,6 +163,7 @@ var Player = /** @class */ (function () {
text: message,
gravity: 'bottom',
position: 'right',
+ stopOnFocus: false,
style: {
marginBottom: '10ex',
background: '#a00',
@@ -321,6 +324,8 @@ var Playlist = /** @class */ (function () {
this.queueExpandedHeight = 'calc(100% - 6ex)';
// event handlers
this.changedHandlers = [];
+ this.nowPlayingEpisodeHandler = null;
+ this.stopHandler = null;
this.playEpisodeHandler = null;
// the actual episode queue
this.episodes = [];
@@ -335,6 +340,7 @@ 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.addPlaylistChangedHandler = function (handler) {
this.changedHandlers.push(handler);
@@ -342,6 +348,12 @@ var Playlist = /** @class */ (function () {
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;
};
@@ -370,6 +382,16 @@ var Playlist = /** @class */ (function () {
this.playlistChanged();
};
Playlist.prototype.unshiftEpisodes = function (episodes) {
+ this.shiftCurrent();
+ var nowPlaying = this.nowPlayingEpisodeHandler
+ ? this.nowPlayingEpisodeHandler()
+ : null;
+ if (nowPlaying) {
+ var litem = this.createQueueListItem(nowPlaying);
+ this.episodes.unshift([nowPlaying, litem]);
+ this.episodeHash.add(nowPlaying.id);
+ this.queueList.prepend(litem);
+ }
for (var i = episodes.length - 1; i >= 0; i--) {
var episode = episodes[i];
if (this.isQueued(episode))
@@ -382,6 +404,7 @@ var Playlist = /** @class */ (function () {
this.notify(episodes.length == 1
? 'Episode added to queue.'
: "".concat(episodes.length, " episodes added to queue."));
+ this.unshiftCurrent();
this.playlistChanged();
};
Playlist.prototype.removeEpisode = function (episode, ignoreChanged, notify) {
@@ -406,20 +429,50 @@ var Playlist = /** @class */ (function () {
};
Playlist.prototype.clearPlaylist = function () {
var removed = this.episodes.length;
+ if (this.nowPlayingEpisodeHandler &&
+ this.nowPlayingEpisodeHandler() != null) {
+ if (this.stopHandler)
+ this.stopHandler();
+ removed++;
+ }
this.episodes.length = 0;
this.episodeHash.clear();
this.queueList.innerHTML = '';
+ this.unshiftCurrent();
this.playlistChanged();
if (removed > 0) {
this.notify('Playlist cleared.');
}
};
Playlist.prototype.playlistChanged = function () {
+ this.shiftCurrent();
+ this.unshiftCurrent();
for (var _i = 0, _a = this.changedHandlers; _i < _a.length; _i++) {
var handler = _a[_i];
handler();
}
};
+ Playlist.prototype.shiftCurrent = function () {
+ var _a;
+ (_a = this.queueList.firstChild) === null || _a === void 0 ? void 0 : _a.remove();
+ };
+ Playlist.prototype.unshiftCurrent = function () {
+ var currentEpisode = this.nowPlayingEpisodeHandler
+ ? this.nowPlayingEpisodeHandler()
+ : null;
+ if (currentEpisode) {
+ this.queueList.prepend(this.createQueueListItem(currentEpisode, false));
+ }
+ else {
+ var litem = document.createElement('li');
+ litem.classList.add('episode');
+ litem.title = 'No episode playing';
+ var label = document.createElement('label');
+ label.innerHTML = 'No episode playing';
+ litem.appendChild(label);
+ this.queueList.prepend(litem);
+ }
+ };
Playlist.prototype.toggleQueueUI = function () {
if (this.queueContainer.style.height !== this.queueExpandedHeight) {
this.queueContainer.style.height = this.queueExpandedHeight;
@@ -438,8 +491,17 @@ var Playlist = /** @class */ (function () {
this.queueTab.classList.remove('expanded');
}
};
- Playlist.prototype.createQueueListItem = function (episode) {
+ Playlist.prototype.reQueueNowPlaying = function () {
+ var currentEpisode = this.nowPlayingEpisodeHandler
+ ? this.nowPlayingEpisodeHandler()
+ : null;
+ if (currentEpisode) {
+ this.unshiftEpisodes([currentEpisode]);
+ }
+ };
+ Playlist.prototype.createQueueListItem = function (episode, withControls) {
var _this = this;
+ if (withControls === void 0) { withControls = true; }
var item = document.createElement('li');
item.classList.add('episode');
item.title = episode.title;
@@ -449,24 +511,27 @@ var Playlist = /** @class */ (function () {
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);
+ if (withControls) {
+ var controls = document.createElement('div');
+ controls.classList.add('controls');
+ var playBtn = document.createElement('a');
+ playBtn.href = '#';
+ playBtn.innerHTML = 'Play Now';
+ playBtn.addEventListener('click', function () {
+ _this.reQueueNowPlaying();
+ 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(controls);
+ }
return item;
};
Playlist.prototype.notify = function (message) {
@@ -474,6 +539,7 @@ var Playlist = /** @class */ (function () {
text: message,
gravity: 'bottom',
position: 'right',
+ stopOnFocus: false,
style: {
marginBottom: '10ex',
background: '#090',
diff --git a/src/Player.ts b/src/Player.ts
index 6da93df..8178c21 100644
--- a/src/Player.ts
+++ b/src/Player.ts
@@ -26,6 +26,9 @@ class Player {
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'));
this.nowPlaying = getOrThrow(document.getElementById('nowPlaying'));
@@ -163,6 +166,7 @@ class Player {
text: message,
gravity: 'bottom',
position: 'right',
+ stopOnFocus: false,
style: {
marginBottom: '10ex',
background: '#a00',
diff --git a/src/Playlist.ts b/src/Playlist.ts
index 6b52e0f..2174469 100644
--- a/src/Playlist.ts
+++ b/src/Playlist.ts
@@ -10,6 +10,8 @@ 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;
// the actual episode queue
@@ -36,6 +38,8 @@ class Playlist {
this.queueTab.addEventListener('click', () => this.toggleQueueUI());
this.overlay.addEventListener('click', () => this.toggleQueueUI());
this.clearButton.addEventListener('click', () => this.clearPlaylist());
+
+ this.unshiftCurrent();
}
public addPlaylistChangedHandler(handler: () => void): void {
@@ -46,6 +50,14 @@ class Playlist {
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;
}
@@ -78,6 +90,16 @@ class Playlist {
}
public unshiftEpisodes(episodes: Array): void {
+ this.shiftCurrent();
+ const nowPlaying = this.nowPlayingEpisodeHandler
+ ? this.nowPlayingEpisodeHandler()
+ : null;
+ if (nowPlaying) {
+ const litem = this.createQueueListItem(nowPlaying);
+ this.episodes.unshift([nowPlaying, litem]);
+ this.episodeHash.add(nowPlaying.id);
+ this.queueList.prepend(litem);
+ }
for (let i = episodes.length - 1; i >= 0; i--) {
const episode = episodes[i];
if (this.isQueued(episode)) this.removeEpisode(episode, true, false);
@@ -91,6 +113,7 @@ class Playlist {
? 'Episode added to queue.'
: `${episodes.length} episodes added to queue.`
);
+ this.unshiftCurrent();
this.playlistChanged();
}
@@ -118,10 +141,19 @@ class Playlist {
}
public clearPlaylist(): void {
- const removed = this.episodes.length;
+ let removed = this.episodes.length;
+ if (
+ this.nowPlayingEpisodeHandler &&
+ this.nowPlayingEpisodeHandler() != null
+ ) {
+ if (this.stopHandler) this.stopHandler();
+ removed++;
+ }
+
this.episodes.length = 0;
this.episodeHash.clear();
this.queueList.innerHTML = '';
+ this.unshiftCurrent();
this.playlistChanged();
if (removed > 0) {
this.notify('Playlist cleared.');
@@ -129,11 +161,35 @@ class Playlist {
}
private playlistChanged(): void {
+ this.shiftCurrent();
+ this.unshiftCurrent();
for (const handler of this.changedHandlers) {
handler();
}
}
+ private shiftCurrent(): void {
+ this.queueList.firstChild?.remove();
+ }
+
+ private unshiftCurrent(): void {
+ const currentEpisode = this.nowPlayingEpisodeHandler
+ ? this.nowPlayingEpisodeHandler()
+ : null;
+
+ if (currentEpisode) {
+ this.queueList.prepend(this.createQueueListItem(currentEpisode, false));
+ } else {
+ const litem = document.createElement('li');
+ litem.classList.add('episode');
+ litem.title = 'No episode playing';
+ const label = document.createElement('label');
+ label.innerHTML = 'No episode playing';
+ litem.appendChild(label);
+ this.queueList.prepend(litem);
+ }
+ }
+
private toggleQueueUI(): void {
if (this.queueContainer.style.height !== this.queueExpandedHeight) {
this.queueContainer.style.height = this.queueExpandedHeight;
@@ -152,7 +208,19 @@ class Playlist {
}
}
- private createQueueListItem(episode: Episode): HTMLLIElement {
+ private reQueueNowPlaying(): void {
+ const currentEpisode = this.nowPlayingEpisodeHandler
+ ? this.nowPlayingEpisodeHandler()
+ : null;
+ if (currentEpisode) {
+ this.unshiftEpisodes([currentEpisode]);
+ }
+ }
+
+ private createQueueListItem(
+ episode: Episode,
+ withControls = true
+ ): HTMLLIElement {
const item = document.createElement('li');
item.classList.add('episode');
item.title = episode.title;
@@ -162,23 +230,26 @@ class Playlist {
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);
+ if (withControls) {
+ const controls = document.createElement('div');
+ controls.classList.add('controls');
+ const playBtn = document.createElement('a');
+ playBtn.href = '#';
+ playBtn.innerHTML = 'Play Now';
+ playBtn.addEventListener('click', () => {
+ this.reQueueNowPlaying();
+ 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(controls);
+ }
return item;
}
@@ -187,6 +258,7 @@ class Playlist {
text: message,
gravity: 'bottom',
position: 'right',
+ stopOnFocus: false,
style: {
marginBottom: '10ex',
background: '#090',