diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml
new file mode 100644
index 0000000..6791bb5
--- /dev/null
+++ b/.github/workflows/build-and-deploy.yml
@@ -0,0 +1,35 @@
+name: Build and deploy
+
+on:
+ push:
+ branches: main
+ repository_dispatch:
+ types: [jekyll-build]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Build
+ uses: docker://jekyll/jekyll
+ with:
+ entrypoint: bash
+ args: -c "/usr/local/bin/bundle install && /usr/local/bin/bundle exec jekyll build --incremental"
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Deploy
+ run: |
+ sudo chown -R $(whoami):$(whoami) .
+ git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com"
+ git config --global user.name "$GITHUB_ACTOR"
+ cp -r _site /tmp
+ cd /tmp/_site
+ git init
+ git branch -M gh-pages
+ git add .
+ git commit -m "Deploy Jekyll to GitHub Pages"
+ git remote add origin "https://$GITHUB_ACTOR:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY"
+ git push -f origin gh-pages
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..918de83
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+_site
+.sass-cache
+.jekyll-cache
+.jekyll-metadata
+vendor
+Gemfile.lock
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..8ba4883
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,10 @@
+source 'https://rubygems.org'
+
+group :jekyll_plugins do
+ gem 'jekyll', '~> 3.9.3'
+ gem 'webrick', '~> 1.8', '>= 1.8.1'
+ gem 'kramdown-parser-gfm', '~> 1.1.0'
+ gem 'jekyll-optional-front-matter', '~> 0.3.2'
+ gem 'cssminify2', '~> 2.0', '>= 2.0.1'
+ gem 'uglifier', '~> 4.2'
+end
diff --git a/_config.yml b/_config.yml
new file mode 100644
index 0000000..832c8ff
--- /dev/null
+++ b/_config.yml
@@ -0,0 +1,16 @@
+title: xmp3
+description: Listen and download music
+
+github:
+ username: xmp3
+ data:
+ repository: data
+ branch: main
+ collections:
+ repository: xmp3
+ branch: main
+
+defaults:
+ -
+ values:
+ layout: default
diff --git a/_includes/components/progressbar.html b/_includes/components/progressbar.html
new file mode 100644
index 0000000..f2285cb
--- /dev/null
+++ b/_includes/components/progressbar.html
@@ -0,0 +1,16 @@
+
diff --git a/_includes/icons/collections.html b/_includes/icons/collections.html
new file mode 100644
index 0000000..baa7552
--- /dev/null
+++ b/_includes/icons/collections.html
@@ -0,0 +1,6 @@
+
diff --git a/_includes/icons/crotchet.html b/_includes/icons/crotchet.html
new file mode 100644
index 0000000..6ea9c75
--- /dev/null
+++ b/_includes/icons/crotchet.html
@@ -0,0 +1,6 @@
+
diff --git a/_includes/icons/download.html b/_includes/icons/download.html
new file mode 100644
index 0000000..ecd4410
--- /dev/null
+++ b/_includes/icons/download.html
@@ -0,0 +1,6 @@
+
diff --git a/_includes/icons/next.html b/_includes/icons/next.html
new file mode 100644
index 0000000..bb3c268
--- /dev/null
+++ b/_includes/icons/next.html
@@ -0,0 +1,6 @@
+
diff --git a/_includes/icons/play.html b/_includes/icons/play.html
new file mode 100644
index 0000000..9f6957f
--- /dev/null
+++ b/_includes/icons/play.html
@@ -0,0 +1,6 @@
+
diff --git a/_includes/icons/previous.html b/_includes/icons/previous.html
new file mode 100644
index 0000000..ae61b05
--- /dev/null
+++ b/_includes/icons/previous.html
@@ -0,0 +1,6 @@
+
diff --git a/_includes/icons/quaver.html b/_includes/icons/quaver.html
new file mode 100644
index 0000000..1b17017
--- /dev/null
+++ b/_includes/icons/quaver.html
@@ -0,0 +1,6 @@
+
diff --git a/_includes/icons/sound.html b/_includes/icons/sound.html
new file mode 100644
index 0000000..16a0fa0
--- /dev/null
+++ b/_includes/icons/sound.html
@@ -0,0 +1,6 @@
+
diff --git a/_includes/styles/global.css.html b/_includes/styles/global.css.html
new file mode 100644
index 0000000..cec23fc
--- /dev/null
+++ b/_includes/styles/global.css.html
@@ -0,0 +1,105 @@
+
diff --git a/_includes/styles/player.css.html b/_includes/styles/player.css.html
new file mode 100644
index 0000000..724d6bb
--- /dev/null
+++ b/_includes/styles/player.css.html
@@ -0,0 +1,6 @@
+
diff --git a/_includes/styles/progressbar.css.html b/_includes/styles/progressbar.css.html
new file mode 100644
index 0000000..9576fa9
--- /dev/null
+++ b/_includes/styles/progressbar.css.html
@@ -0,0 +1,56 @@
+
diff --git a/_includes/templates/collection.html b/_includes/templates/collection.html
new file mode 100644
index 0000000..3a7c04b
--- /dev/null
+++ b/_includes/templates/collection.html
@@ -0,0 +1,18 @@
+{% assign data = page.username | append: ',' | append: page.filename %}
+{% assign collection = data | get_collection %}
+
+{% for track in collection %}
+
+
+
+ {% include icons/crotchet.html class="fallback-image-icon" %}
+
+
+
+
+
{{ track.title }}
+
+
{{ track.artist }}
+
+
+{% endfor %}
diff --git a/_includes/templates/player.html b/_includes/templates/player.html
new file mode 100644
index 0000000..5e10eaa
--- /dev/null
+++ b/_includes/templates/player.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+ {% include components/progressbar.html id="seekbar" %}
+
+ 00:00
+ 00:00
+
+
+
+
+
+ {% include components/progressbar.html id="volume-control" class="flex-1" min="0" max="100" step="1" %}
+
+
+
+
diff --git a/_includes/templates/user.html b/_includes/templates/user.html
new file mode 100644
index 0000000..ddd8dbf
--- /dev/null
+++ b/_includes/templates/user.html
@@ -0,0 +1,20 @@
+{% assign users = site.data.collections | group_by: 'username' %}
+
+{% for user in users %}
+{% if user.name == page.username %}
+
+{% endif %}
+{% endfor %}
diff --git a/_layouts/compress.liquid b/_layouts/compress.liquid
new file mode 100644
index 0000000..50aa52f
--- /dev/null
+++ b/_layouts/compress.liquid
@@ -0,0 +1,2 @@
+{% capture _LINE_FEED %}
+{% endcapture %}{% if site.compress_html.ignore.envs contains jekyll.environment or site.compress_html.ignore.envs == "all" or page.compress_html == false %}{{ content }}{% else %}{% capture _content %}{{ content }}{% endcapture %}{% assign _profile = site.compress_html.profile %}{% if site.compress_html.endings == "all" %}{% assign _endings = "html head body li dt dd optgroup option colgroup caption thead tbody tfoot tr td th" | split: " " %}{% else %}{% assign _endings = site.compress_html.endings %}{% endif %}{% for _element in _endings %}{% capture _end %}{{ _element }}>{% endcapture %}{% assign _content = _content | remove: _end %}{% endfor %}{% if _profile and _endings %}{% assign _profile_endings = _content | size | plus: 1 %}{% endif %}{% for _element in site.compress_html.startings %}{% capture _start %}<{{ _element }}>{% endcapture %}{% assign _content = _content | remove: _start %}{% endfor %}{% if _profile and site.compress_html.startings %}{% assign _profile_startings = _content | size | plus: 1 %}{% endif %}{% if site.compress_html.comments == "all" %}{% assign _comments = "" | split: " " %}{% else %}{% assign _comments = site.compress_html.comments %}{% endif %}{% if _comments.size == 2 %}{% capture _comment_befores %}.{{ _content }}{% endcapture %}{% assign _comment_befores = _comment_befores | split: _comments.first %}{% for _comment_before in _comment_befores %}{% if forloop.first %}{% continue %}{% endif %}{% capture _comment_outside %}{% if _carry %}{{ _comments.first }}{% endif %}{{ _comment_before }}{% endcapture %}{% capture _comment %}{% unless _carry %}{{ _comments.first }}{% endunless %}{{ _comment_outside | split: _comments.last | first }}{% if _comment_outside contains _comments.last %}{{ _comments.last }}{% assign _carry = false %}{% else %}{% assign _carry = true %}{% endif %}{% endcapture %}{% assign _content = _content | remove_first: _comment %}{% endfor %}{% if _profile %}{% assign _profile_comments = _content | size | plus: 1 %}{% endif %}{% endif %}{% assign _pre_befores = _content | split: "" %}{% assign _pres_after = "" %}{% if _pres.size != 0 %}{% if site.compress_html.blanklines %}{% assign _lines = _pres.last | split: _LINE_FEED %}{% capture _pres_after %}{% for _line in _lines %}{% assign _trimmed = _line | split: " " | join: " " %}{% if _trimmed != empty or forloop.last %}{% unless forloop.first %}{{ _LINE_FEED }}{% endunless %}{{ _line }}{% endif %}{% endfor %}{% endcapture %}{% else %}{% assign _pres_after = _pres.last | split: " " | join: " " %}{% endif %}{% endif %}{% capture _content %}{{ _content }}{% if _pre_before contains "
" %}{% endif %}{% unless _pre_before contains "
" and _pres.size == 1 %}{{ _pres_after }}{% endunless %}{% endcapture %}{% endfor %}{% if _profile %}{% assign _profile_collapse = _content | size | plus: 1 %}{% endif %}{% if site.compress_html.clippings == "all" %}{% assign _clippings = "html head title base link meta style body article section nav aside h1 h2 h3 h4 h5 h6 hgroup header footer address p hr blockquote ol ul li dl dt dd figure figcaption main div table caption colgroup col tbody thead tfoot tr td th" | split: " " %}{% else %}{% assign _clippings = site.compress_html.clippings %}{% endif %}{% for _element in _clippings %}{% assign _edges = " ;; ;" | replace: "e", _element | split: ";" %}{% assign _content = _content | replace: _edges[0], _edges[1] | replace: _edges[2], _edges[3] | replace: _edges[4], _edges[5] %}{% endfor %}{% if _profile and _clippings %}{% assign _profile_clippings = _content | size | plus: 1 %}{% endif %}{{ _content }}{% if _profile %} Step | Bytes |
raw | {{ content | size }}{% if _profile_endings %} |
endings | {{ _profile_endings }}{% endif %}{% if _profile_startings %} |
startings | {{ _profile_startings }}{% endif %}{% if _profile_comments %} |
comments | {{ _profile_comments }}{% endif %}{% if _profile_collapse %} |
collapse | {{ _profile_collapse }}{% endif %}{% if _profile_clippings %} |
clippings | {{ _profile_clippings }}{% endif %} |
{% endif %}{% endif %}
diff --git a/_layouts/default.html b/_layouts/default.html
new file mode 100644
index 0000000..bd58e13
--- /dev/null
+++ b/_layouts/default.html
@@ -0,0 +1,31 @@
+---
+layout: compress
+---
+
+
+
+
+
+
+
+
+
+
+
+ {% if page.extra_head %}{{ page.extra_head }}{% endif %}
+ {% if page.scripts %}
+ {% for script in page.scripts %}
+ {% include scripts/{{ script }}.html %}
+ {% endfor %}
+ {% endif %}
+
+ {% if page.title %}{{ page.title }} | {% endif %}{{ site.title }}
+ {% include styles/global.css.html %}
+ {% if page.styles %}
+ {% for style in page.styles %}
+ {% include styles/{{ style }}.html %}
+ {% endfor %}
+ {% endif %}
+
+{{ content }}
+
diff --git a/_pages/player.md b/_pages/player.md
new file mode 100644
index 0000000..3974338
--- /dev/null
+++ b/_pages/player.md
@@ -0,0 +1,39 @@
+---
+layout: default
+styles:
+ - player.css
+ - progressbar.css
+extra_head: |
+
+---
+
+
+{% include templates/player.html %}
+
+
+
+
+
+
diff --git a/_pages/user.md b/_pages/user.md
new file mode 100644
index 0000000..f2f1f25
--- /dev/null
+++ b/_pages/user.md
@@ -0,0 +1,43 @@
+---
+layout: default
+extra_head: |
+
+---
+
+
+
+
+{% include templates/user.html %}
+
+
+
diff --git a/_plugins/build.rb b/_plugins/build.rb
new file mode 100644
index 0000000..511535e
--- /dev/null
+++ b/_plugins/build.rb
@@ -0,0 +1,46 @@
+module Jekyll
+ class Build < Generator
+ safe true
+
+ def generate(site)
+ site.data['collections'] = Jekyll::GitHub.get_data('collections')
+
+ site.data['collections'].each do |row|
+ site.pages << User.new(site, site.source, row.to_h)
+ site.pages << Player.new(site, site.source, row.to_h)
+ end
+ end
+ end
+
+ class User < Page
+ def initialize(site, base, row)
+ @site = site
+ @base = base
+ @dir = row['username']
+ @name = 'index.html'
+
+ self.process(@name)
+ self.read_yaml(File.join(base, '_pages'), 'user.md')
+ self.data['title'] = row['username']
+ self.data['username'] = row['username']
+ self.data['filename'] = row['filename']
+ self.data['name'] = row['name']
+ end
+ end
+
+ class Player < Page
+ def initialize(site, base, row)
+ @site = site
+ @base = base
+ @dir = "#{row['username']}/#{row['filename']}"
+ @name = 'index.html'
+
+ self.process(@name)
+ self.read_yaml(File.join(base, '_pages'), 'player.md')
+ self.data['title'] = row['username']
+ self.data['username'] = row['username']
+ self.data['filename'] = row['filename']
+ self.data['name'] = row['name']
+ end
+ end
+end
diff --git a/_plugins/github.rb b/_plugins/github.rb
new file mode 100644
index 0000000..1f98a8e
--- /dev/null
+++ b/_plugins/github.rb
@@ -0,0 +1,81 @@
+require 'csv'
+require 'uri'
+require 'net/http'
+require 'yaml'
+
+module Jekyll
+ module GitHub
+ @@tuples = {}
+
+ def self.fetch_csv(repo_config, username, filename)
+ url = "https://raw.githubusercontent.com/#{username}/#{repo_config['repository']}/#{repo_config['branch']}/#{filename}.csv"
+ puts "Fetching #{url}"
+ return @@tuples[url] if @@tuples.key?(url)
+
+ begin
+ uri = URI.parse(url)
+ response = Net::HTTP.get_response(uri)
+
+ if response.code == '200'
+ csv_data = response.body.force_encoding('UTF-8')
+ csv = CSV.parse(csv_data, headers: true)
+
+ rows = []
+ csv.each_with_index do |row, index|
+ begin
+ rows << yield(row)
+ rescue CSV::MalformedCSVError => e
+ puts "Error parsing CSV at line #{index + 2}: #{e.message}"
+ next
+ end
+ end
+
+ @@tuples[url] = rows
+ else
+ puts "Error making HTTP request: #{response.code}"
+ @@tuples[url] = []
+ end
+ rescue => e
+ puts "Error making HTTP request: #{e.message}"
+ @@tuples[url] = []
+ end
+
+ @@tuples[url]
+ end
+
+ def self.get_data(filename)
+ config = YAML.load_file('_config.yml')
+ username = config['github']['username']
+ repo_config = config['github']['data']
+
+ GitHub.fetch_csv(repo_config, username, filename) do |row|
+ {
+ 'username' => row['username'],
+ 'filename' => row['filename'],
+ 'name' => row['name'],
+ 'description' => row['description'],
+ 'image' => row['image']
+ }
+ end
+ end
+
+ def get_collection(data)
+ config = YAML.load_file('_config.yml')
+ repo_config = config['github']['collections']
+
+ username, filename = data.split(',')
+
+ GitHub.fetch_csv(repo_config, username, filename) do |row|
+ {
+ 'title' => row['title'],
+ 'artist' => row['artist'],
+ 'cover' => row['cover'],
+ 'file' => row['file']
+ }
+ end
+ end
+
+ end
+end
+
+Liquid::Template.register_filter(Jekyll::GitHub)
diff --git a/_plugins/minifier.rb b/_plugins/minifier.rb
new file mode 100644
index 0000000..9c28d68
--- /dev/null
+++ b/_plugins/minifier.rb
@@ -0,0 +1,43 @@
+require 'cssminify2'
+require 'uglifier'
+
+module Jekyll
+ class Minifier < Generator
+ safe true
+
+ def generate(site)
+ Jekyll::Hooks.register :site, :post_write do |site|
+ compress_files(site)
+ end
+ end
+
+ private
+
+ def compress_files(site)
+ Dir.glob(File.join(site.dest, "**", "*")).each do |file|
+ next if File.directory?(file)
+
+ case File.extname(file)
+ when '.js'
+ compress_file(file) { |content| compress_js(content) }
+ when '.css'
+ compress_file(file) { |content| compress_css(content) }
+ end
+ end
+ end
+
+ def compress_file(file_path)
+ content = File.read(file_path)
+ compressed_content = yield content
+ File.open(file_path, 'w') { |f| f.write(compressed_content) }
+ end
+
+ def compress_js(content)
+ Uglifier.new(harmony: true).compile(content)
+ end
+
+ def compress_css(content)
+ CSSminify2.new.compress(content)
+ end
+ end
+end
diff --git a/assets/img/equaliser-animated.gif b/assets/img/equaliser-animated.gif
new file mode 100644
index 0000000..27285ed
Binary files /dev/null and b/assets/img/equaliser-animated.gif differ
diff --git a/assets/js/player/data.js b/assets/js/player/data.js
new file mode 100644
index 0000000..87817fa
--- /dev/null
+++ b/assets/js/player/data.js
@@ -0,0 +1,7 @@
+export default Array.from(document.querySelectorAll('.listitem'))
+.map(item => ({
+ title: item.getAttribute('data-title'),
+ artist: item.getAttribute('data-artist'),
+ cover: item.getAttribute('data-cover'),
+ file: item.getAttribute('data-file'),
+}))
diff --git a/assets/js/player/elements.js b/assets/js/player/elements.js
new file mode 100644
index 0000000..e34f277
--- /dev/null
+++ b/assets/js/player/elements.js
@@ -0,0 +1,61 @@
+import * as utils from './utils.js'
+
+export default {
+ get() {
+ this.cover = document.querySelector('#track-cover')
+ this.title = document.querySelector('#track-title')
+ this.artist = document.querySelector('#track-artist')
+ this.playPauseButton = document.querySelector('#play-pause')
+ this.nextButton = document.querySelector('#next')
+ this.previousButton = document.querySelector('#previous')
+ this.volumeButton = document.querySelector('#volume')
+ this.volumeControl = document.querySelector('#volume-control')
+ this.seekbar = document.querySelector('#seekbar')
+ this.currentDuration = document.querySelector('#current-duration')
+ this.totalDuration = document.querySelector('#total-duration')
+ this.downloadButton = document.querySelector('#download')
+ this.collection = document.querySelectorAll('.listitem')
+ this.playIcon = ''
+ this.pauseIcon = ''
+ this.mutedIcon = ''
+ this.soundIcon = ''
+ },
+
+ createAudioElement(audio) {
+ this.audio = new Audio(audio)
+ },
+
+ actions() {
+ this.playPauseButton.onclick = () => this.togglePlayPause()
+ this.audio.onended = () => this.next()
+
+ this.volumeButton.onclick = () => this.toggleMute()
+ this.volumeControl.onchange = () => this.setVolume(this.volumeControl.value)
+
+ this.seekbar.onchange = () => this.setSeekbar(this.seekbar.value)
+
+ this.seekbar.max = this.audio.duration
+ this.totalDuration.innerText = utils.convertTo12HourFormat(this.audio.duration)
+
+ this.audio.ontimeupdate = () => this.timeUpdate()
+
+ this.nextButton.onclick = () => this.next()
+ this.previousButton.onclick = () => this.back()
+
+ this.collection.forEach(
+ (track, index) => track.onclick = () => this.swap(index)
+ )
+
+ if ('mediaSession' in navigator) {
+ navigator.mediaSession.setActionHandler('play', () => this.togglePlayPause())
+ navigator.mediaSession.setActionHandler('pause', () => this.togglePlayPause())
+ navigator.mediaSession.setActionHandler('nexttrack', () => this.next())
+ navigator.mediaSession.setActionHandler('previoustrack', () => this.back())
+ navigator.mediaSession.setActionHandler('seekto', (event) => {
+ if (event.seekTime && typeof event.seekTime === 'number') {
+ this.setSeekbar(event.seekTime)
+ }
+ })
+ }
+ },
+}
diff --git a/assets/js/player/index.js b/assets/js/player/index.js
new file mode 100644
index 0000000..c678b97
--- /dev/null
+++ b/assets/js/player/index.js
@@ -0,0 +1,5 @@
+import player from './player.js'
+
+window.addEventListener('load', () => {
+ player.start()
+})
diff --git a/assets/js/player/player.js b/assets/js/player/player.js
new file mode 100644
index 0000000..6781a4d
--- /dev/null
+++ b/assets/js/player/player.js
@@ -0,0 +1,140 @@
+import audios from './data.js'
+import elements from './elements.js'
+import * as utils from './utils.js'
+
+export default {
+ audioData: audios,
+ currentPlaying: utils.currentIndex(),
+ currentAudio: {},
+ isPlaying: false,
+ savedMuted: false,
+ savedVolume: 1,
+
+ start() {
+ elements.get.call(this)
+ this.update()
+ this.volumeControl.value = 100
+ },
+
+ play() {
+ this.isPlaying = true
+ this.audio.play()
+ this.playPauseButton.innerHTML = this.pauseIcon
+ },
+
+ pause() {
+ this.isPlaying = false
+ this.audio.pause()
+ this.playPauseButton.innerHTML = this.playIcon
+ },
+
+ togglePlayPause() {
+ if (this.isPlaying) {
+ this.pause()
+ } else {
+ this.play()
+ }
+ },
+
+ swap(index) {
+ this.currentPlaying = index
+ this.pause()
+ this.update()
+ this.play()
+ },
+
+ next() {
+ let index = this.currentPlaying + 1
+ if (index === this.audioData.length) {
+ index = 0
+ }
+ this.swap(index)
+ },
+
+ back() {
+ let index = this.currentPlaying - 1
+ if (index === -1) {
+ index = this.audioData.length - 1
+ }
+ this.swap(index)
+ },
+
+ toggleMute() {
+ this.audio.muted = !this.audio.muted
+ this.savedMuted = this.audio.muted
+ this.volumeButton.innerHTML = this.audio.muted ? this.mutedIcon : this.soundIcon
+ },
+
+ setVolume(value) {
+ this.savedVolume = value / 100
+ this.audio.volume = this.savedVolume
+ },
+
+ setSeekbar(value) {
+ this.audio.muted = true
+ this.audio.currentTime = value
+ },
+
+ timeUpdate() {
+ const currentTime = this.audio.currentTime
+ const formattedTime = utils.convertTo12HourFormat(currentTime)
+
+ if (Math.ceil(this.seekbar.value) != Math.ceil(currentTime)) {
+ this.seekbar.value = currentTime
+ }
+
+ if (this.currentDuration.innerText !== formattedTime) {
+ this.currentDuration.innerText = formattedTime
+ }
+
+ if (!this.savedMuted && !this.seekbar.dragging) {
+ this.audio.muted = false
+ }
+
+ if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
+ navigator.mediaSession.setPositionState({
+ duration: this.audio.duration || 0,
+ playbackRate: this.audio.playbackRate || 1,
+ position: this.audio.currentTime || 0,
+ })
+ }
+ },
+
+ update() {
+ if (this.currentPlaying < 0 || this.currentPlaying > this.audioData.length) {
+ this.currentPlaying = 0
+ }
+
+ this.currentAudio = this.audioData[this.currentPlaying]
+ this.cover.src = utils.imageURL(this.currentAudio.cover)
+ this.title.innerText = this.currentAudio.title
+ this.artist.innerText = this.currentAudio.artist
+
+ if ('mediaSession' in navigator) {
+ navigator.mediaSession.metadata = new MediaMetadata({
+ title: this.currentAudio.title,
+ artist: this.currentAudio.artist,
+ artwork: [{ src: this.cover.src, sizes: '512x512', type: 'image/webp' }]
+ })
+ }
+
+ const audio = utils.audioURL(this.currentAudio.file)
+ elements.createAudioElement.call(this, audio)
+
+ this.downloadButton.href = audio
+ this.downloadButton.download = utils.generateFilename(
+ this.currentAudio.file,
+ this.currentAudio.artist,
+ this.currentAudio.title
+ )
+
+ window.location.hash = this.currentPlaying + 1
+ document.title = this.currentAudio.title + ' | xmp3'
+
+ this.audio.onloadeddata = () => {
+ elements.actions.call(this)
+ this.audio.muted = this.savedMuted
+ this.audio.volume = this.savedVolume
+ }
+ },
+}
diff --git a/assets/js/player/utils.js b/assets/js/player/utils.js
new file mode 100644
index 0000000..f7bdb22
--- /dev/null
+++ b/assets/js/player/utils.js
@@ -0,0 +1,25 @@
+export function URL(path = '/') {
+ return 'https://xmp3.github.io' + path
+}
+
+export function imageURL(image) {
+ return image.startsWith('https://') ? image : URL('/image/' + image)
+}
+
+export function audioURL(audio) {
+ return audio.startsWith('https://') ? audio : URL('/audio/' + audio)
+}
+
+export function currentIndex() {
+ return window.location.hash.slice(1) - 1 || 0
+}
+
+export function convertTo12HourFormat(time) {
+ const convertToTwoDigits = number => ('0' + Math.floor(number)).slice(-2)
+ return convertToTwoDigits(time / 60) + ':' + convertToTwoDigits(time % 60)
+}
+
+export function generateFilename(filename, ...names) {
+ const extension = path => path.split('.').pop()
+ return names.join(' - ') + '.' + extension(filename)
+}
diff --git a/assets/js/progressbar/index.js b/assets/js/progressbar/index.js
new file mode 100644
index 0000000..82a5c79
--- /dev/null
+++ b/assets/js/progressbar/index.js
@@ -0,0 +1,6 @@
+import ProgressBar from './progressbar.js'
+
+document.addEventListener('DOMContentLoaded', () => {
+ const progressbars = document.querySelectorAll('.progressbar-container')
+ progressbars.forEach(progressbar => new ProgressBar(progressbar))
+})
diff --git a/assets/js/progressbar/progressbar.js b/assets/js/progressbar/progressbar.js
new file mode 100644
index 0000000..55cfc43
--- /dev/null
+++ b/assets/js/progressbar/progressbar.js
@@ -0,0 +1,113 @@
+export default class {
+ constructor(progressbar) {
+ this.progressbar = progressbar
+ this.properties = {}
+ this.dragging = false
+ this.defineAttributes()
+ this.defineProperties()
+ this.addEventListeners()
+ }
+
+ defineAttributes() {
+ this.attributes = {
+ min: 0,
+ max: 1,
+ step: .1,
+ value: 0,
+ }
+ Object.keys(this.attributes).forEach(
+ attribute => this[attribute] =
+ +this.progressbar.getAttribute('data-' + attribute) || this.attributes[attribute]
+ )
+ }
+
+ defineProperties() {
+ this.properties.dragging = {
+ get: () => this.dragging,
+ }
+ this.properties.min = {
+ get: () => this.min,
+ set: min => this.min = +min,
+ }
+ this.properties.max = {
+ get: () => this.max,
+ set: max => this.max = +max,
+ }
+ this.properties.step = {
+ get: () => this.step,
+ set: step => this.step = +step,
+ }
+ this.properties.value = {
+ get: () => this.value,
+ set: value => this.update(value),
+ }
+ this.properties.onchange = {
+ set: onchange => this.onchange = onchange,
+ }
+ Object.keys(this.properties).forEach(property =>
+ Object.defineProperty(this.progressbar, property, this.properties[property])
+ )
+ }
+
+ addEventListeners() {
+ this.progressbar.addEventListener('mousedown', this.onMouseDown.bind(this))
+ document.addEventListener('mousemove', this.onMouseMove.bind(this))
+ document.addEventListener('mouseup', this.onMouseUp.bind(this))
+ document.addEventListener('mouseleave', this.onMouseUp.bind(this))
+ this.progressbar.addEventListener('touchstart', this.onTouchStart.bind(this))
+ document.addEventListener('touchmove', this.onTouchMove.bind(this))
+ document.addEventListener('touchend', this.onTouchEnd.bind(this))
+ document.addEventListener('touchcancel', this.onTouchEnd.bind(this))
+ }
+
+ eventHandler(event) {
+ if (typeof this[event] === 'function') {
+ this[event]()
+ }
+ }
+
+ eventChange(clientX) {
+ const rect = this.progressbar.getBoundingClientRect()
+ let offsetX = clientX - rect.left
+ offsetX = Math.max(0, Math.min(offsetX, rect.width))
+ let v = (offsetX / rect.width) * (this.max - this.min) + this.min
+ this.progressbar.value = Math.round(v / this.step) * this.step
+ this.eventHandler('onchange')
+ }
+
+ onMouseDown(event) {
+ this.dragging = true
+ this.eventChange(event.clientX)
+ }
+
+ onMouseMove(event) {
+ if (this.dragging) {
+ this.eventChange(event.clientX)
+ }
+ }
+
+ onMouseUp() {
+ this.dragging = false
+ }
+
+ onTouchStart(event) {
+ this.dragging = true
+ this.eventChange(event.touches[0].clientX)
+ }
+
+ onTouchMove(event) {
+ if (this.dragging) {
+ this.eventChange(event.touches[0].clientX)
+ }
+ }
+
+ onTouchEnd() {
+ this.dragging = false
+ }
+
+ update(value) {
+ this.value = +value
+ const percentage = (this.value / this.max) * 100
+ this.progressbar.style.setProperty('--progressbar-transform', percentage + '%')
+ }
+}
diff --git a/index.md b/index.md
new file mode 100644
index 0000000..fe9d447
--- /dev/null
+++ b/index.md
@@ -0,0 +1,6 @@
+---
+layout: default
+title: Redirecting...
+extra_head: |
+
+---