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 %} + +
+ {{ track.title }} cover + {% 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 icons/quaver.html class="fallback-image-icon" %} +
+
+
Title
+ Artist +
+
+
+
+
+ {% include components/progressbar.html id="seekbar" %} +
+ 00:00 + 00:00 +
+
+
+ + {% include icons/collections.html class="w-6 h-6" %} + + + + + + {% include icons/download.html class="w-6 h-6" %} + +
+
+ + {% 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 %}{% 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: | + +---