diff --git a/demo.html b/demo.html
deleted file mode 100644
index aa24501..0000000
--- a/demo.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..6853f69
--- /dev/null
+++ b/index.html
@@ -0,0 +1,12 @@
+
+
+
+ podcast-player demo
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..6320409
--- /dev/null
+++ b/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "podcast-player",
+ "version": "1.0.0",
+ "description": "![Screenshot of podcast-player](https://s3.amazonaws.com/f.cl.ly/items/3Q47193Z0f00142R0O42/1d0pzyOUeVH2-d1m77Vxx9QkL0vexZ5bJNbyEGRCfbY.png)",
+ "main": "podcast-player.js",
+ "scripts": {
+ "start": "npx http-server",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/davatron5000/podcast-player.git"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/davatron5000/podcast-player/issues"
+ },
+ "homepage": "https://github.com/davatron5000/podcast-player#readme"
+}
diff --git a/podcast-player.html b/podcast-player.html
deleted file mode 100644
index 3ff5e6e..0000000
--- a/podcast-player.html
+++ /dev/null
@@ -1,215 +0,0 @@
-
-
-
-
-
-
-
-
00:00
-
-
00:00
-
-
-
-
-
diff --git a/podcast-player.js b/podcast-player.js
new file mode 100644
index 0000000..bff9035
--- /dev/null
+++ b/podcast-player.js
@@ -0,0 +1,342 @@
+import {
+ LitElement,
+ html,
+ css,
+} from "https://unpkg.com/lit-element@2.2.1/lit-element.js?module";
+
+class PodcastPlayer extends LitElement {
+ static get properties() {
+ return {
+ currentTime: { type: String },
+ currentSpeedIdx: { type: Number },
+ duration: { type: String },
+ playing: { type: Boolean, reflect: true },
+ muted: { type: Boolean, reflect: true },
+ };
+ }
+
+ // TODO: Make styles optional? Easily overrideable?
+ static get styles() {
+ return css`
+ .sr-only {
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ height: 1px;
+ overflow: hidden;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+ }
+
+ .podcast-player {
+ display: flex;
+ gap: 0.5rem;
+ width: 100%;
+ align-items: center;
+ grid-gap: 0.5rem;
+ }
+
+ .podcast-player .progress {
+ flex: 1;
+ }
+
+ .podcast-player button svg {
+ width: 0.8em;
+ height: 0.8em;
+ }
+
+ /* Speed */
+ .podcast-player .button-speed:after {
+ content: "x";
+ }
+
+ /* Play/Pause */
+ .button-play .pause {
+ display: none;
+ }
+
+ :host([playing]) .button-play .pause {
+ display: inline;
+ }
+ :host([playing]) .button-play .play {
+ display: none;
+ }
+
+ /* Mute/Unmute */
+ .button-mute .muted {
+ display: none;
+ }
+
+ :host([muted]) .button-mute .muted {
+ display: inline;
+ }
+
+ :host([muted]) .button-mute .unmuted {
+ display: none;
+ }
+ `;
+ }
+
+ constructor() {
+ super();
+
+ // HTMLAudioElement
+ this.audio = this.querySelector("audio");
+ this.audio.controls = false; // remove controls if it has 'em
+
+ this.speeds = [1, 1.25, 1.5, 1.75, 2];
+ this.currentSpeedIdx = 0;
+ this.currentTime = 0;
+ this.duration = 0;
+ this.playing = false;
+ this.muted = false;
+
+ this.audio.addEventListener("timeupdate", this.handleTimeUpdate.bind(this));
+ this.audio.addEventListener(
+ "loadedmetadata",
+ this.handleLoadedMetadata.bind(this)
+ );
+ this.audio.addEventListener("ended", this.stop.bind(this));
+
+ window.addEventListener(
+ "DOMContentLoaded",
+ this.timeJump.bind(this),
+ false
+ );
+ window.addEventListener("hashchange", this.timeJump.bind(this), false);
+ }
+
+ handleLoadedMetadata() {
+ this.duration = this.audio.duration;
+ }
+
+ handleTimeUpdate(e) {
+ this.currentTime = this.audio.currentTime;
+ }
+
+ timeJump(event) {
+ let params = new URLSearchParams(window.location.hash.substring(1));
+ let t = params.get("t") || 0;
+
+ console.log(window, params, t, event);
+
+ var timestamp = this.parseTime(t);
+
+ if (t) {
+ // Preload the media
+ this.audio.setAttribute("preload", "true");
+ // Set the current time. Will update if playing. Will fail if paused.
+ this.audio.currentTime = timestamp;
+ // If the media is able to play, play.
+ this.audio.addEventListener(
+ "canplay",
+ () => {
+ /* only start the player if it is not already playing */
+ if (!this.audio.paused) {
+ return false;
+ }
+
+ this.audio.currentTime = timestamp;
+ this.audio.play();
+ this.playing = true;
+ },
+ false
+ );
+ }
+ }
+
+ parseTime(str) {
+ var plain = /^\d+(\.\d+)?$/g,
+ npt = /^(?:npt:)?(?:(?:(\d+):)?(\d\d?):)?(\d\d?)(\.\d+)?$/,
+ quirks = /^(?:(\d\d?)[hH])?(?:(\d\d?)[mM])?(\d\d?)[sS]$/,
+ match;
+ if (plain.test(str)) {
+ return parseFloat(str);
+ }
+ match = npt.exec(str) || quirks.exec(str);
+ if (match) {
+ return (
+ 3600 * (parseInt(match[1], 10) || 0) +
+ 60 * (parseInt(match[2], 10) || 0) +
+ parseInt(match[3], 10) +
+ (parseFloat(match[4]) || 0)
+ );
+ }
+ return 0;
+ }
+
+ toHHMMSS(totalsecs) {
+ var sec_num = parseInt(totalsecs, 10); // don't forget the second param
+ var hours = Math.floor(sec_num / 3600);
+ var minutes = Math.floor((sec_num - hours * 3600) / 60);
+ var seconds = sec_num - hours * 3600 - minutes * 60;
+
+ if (hours < 10) {
+ hours = "0" + hours;
+ }
+ if (minutes < 10) {
+ minutes = "0" + minutes;
+ }
+ if (seconds < 10) {
+ seconds = "0" + seconds;
+ }
+
+ hours = hours > 0 ? hours + ":" : "";
+ minutes = minutes + ":";
+
+ var time = hours + minutes + seconds;
+ return time;
+ }
+
+ changeSpeed() {
+ this.currentSpeedIdx =
+ this.currentSpeedIdx + 1 < this.speeds.length
+ ? this.currentSpeedIdx + 1
+ : 0;
+ this.audio.playbackRate = this.speeds[this.currentSpeedIdx];
+ }
+
+ mute() {
+ this.audio.muted = !this.audio.muted;
+ this.muted = this.audio.muted;
+ }
+
+ play() {
+ if (this.audio.paused) {
+ console.log(this.currentTime, this.audio.currentTime);
+ this.audio.play();
+ } else {
+ this.audio.pause();
+ }
+ this.playing = !this.audio.paused;
+ }
+
+ stop() {
+ this.playing = !this.audio.paused;
+ }
+
+ rewind() {
+ this.audio.currentTime -= 30;
+ }
+
+ ff() {
+ this.audio.currentTime += 30;
+ }
+
+ seek(e) {
+ this.audio.currentTime = e.target.value;
+ }
+
+ render() {
+ // TODO: Make icons, button text, button labels overrideable?
+ return html`
+
+
+
+
+
+
+
+
+
+
+
+ Current Time
+ ${this.toHHMMSS(this.currentTime)}
+
+
+
+
+
+ Duration
+ ${this.toHHMMSS(this.duration)}
+
+
+
+
+
+
+ `;
+ }
+}
+
+customElements.define("podcast-player", PodcastPlayer);