mirror of
https://github.com/9001/copyparty.git
synced 2025-08-19 09:52:21 -06:00
648 lines
15 KiB
JavaScript
648 lines
15 KiB
JavaScript
"use strict";
|
|
|
|
// error handler for mobile devices
|
|
function hcroak(msg) {
|
|
document.body.innerHTML = msg;
|
|
window.onerror = undefined;
|
|
throw 'fatal_err';
|
|
}
|
|
function croak(msg) {
|
|
document.body.textContent = msg;
|
|
window.onerror = undefined;
|
|
throw msg;
|
|
}
|
|
function esc(txt) {
|
|
return txt.replace(/[&"<>]/g, function (c) {
|
|
return {
|
|
'&': '&',
|
|
'"': '"',
|
|
'<': '<',
|
|
'>': '>'
|
|
}[c];
|
|
});
|
|
}
|
|
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
|
window.onerror = undefined;
|
|
var html = ['<h1>you hit a bug!</h1><p>please screenshot this error and send me a copy arigathanks gozaimuch (ed/irc.rizon.net or ed#2644)</p><p>',
|
|
esc(String(msg)), '</p><p>', esc(url + ' @' + lineNo + ':' + columnNo), '</p>'];
|
|
|
|
if (error) {
|
|
var find = ['desc', 'stack', 'trace'];
|
|
for (var a = 0; a < find.length; a++)
|
|
if (String(error[find[a]]) !== 'undefined')
|
|
html.push('<h2>' + find[a] + '</h2>' +
|
|
esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
|
|
}
|
|
document.body.style.fontSize = '0.8em';
|
|
document.body.style.padding = '0 1em 1em 1em';
|
|
hcroak(html.join('\n'));
|
|
};
|
|
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
|
|
if (!String.prototype.endsWith) {
|
|
String.prototype.endsWith = function (search, this_len) {
|
|
if (this_len === undefined || this_len > this.length) {
|
|
this_len = this.length;
|
|
}
|
|
return this.substring(this_len - search.length, this_len) === search;
|
|
};
|
|
}
|
|
|
|
|
|
// https://stackoverflow.com/a/950146
|
|
function import_js(url, cb) {
|
|
var head = document.head || document.getElementsByTagName('head')[0];
|
|
var script = document.createElement('script');
|
|
script.type = 'text/javascript';
|
|
script.src = url;
|
|
|
|
script.onreadystatechange = cb;
|
|
script.onload = cb;
|
|
|
|
head.appendChild(script);
|
|
}
|
|
|
|
|
|
function o(id) {
|
|
return document.getElementById(id);
|
|
}
|
|
|
|
function dbg(msg) {
|
|
o('path').innerHTML = msg;
|
|
}
|
|
|
|
function ev(e) {
|
|
e = e || window.event;
|
|
e.preventDefault ? e.preventDefault() : (e.returnValue = false);
|
|
return e;
|
|
}
|
|
|
|
|
|
function sortTable(table, col) {
|
|
var tb = table.tBodies[0], // use `<tbody>` to ignore `<thead>` and `<tfoot>` rows
|
|
th = table.tHead.rows[0].cells,
|
|
tr = Array.prototype.slice.call(tb.rows, 0),
|
|
i, reverse = th[col].className == 'sort1' ? -1 : 1;
|
|
for (var a = 0, thl = th.length; a < thl; a++)
|
|
th[a].className = '';
|
|
th[col].className = 'sort' + reverse;
|
|
var stype = th[col].getAttribute('sort');
|
|
tr = tr.sort(function (a, b) {
|
|
var v1 = a.cells[col].textContent.trim();
|
|
var v2 = b.cells[col].textContent.trim();
|
|
if (stype == 'int') {
|
|
v1 = parseInt(v1.replace(/,/g, ''));
|
|
v2 = parseInt(v2.replace(/,/g, ''));
|
|
return reverse * (v1 - v2);
|
|
}
|
|
return reverse * (v1.localeCompare(v2));
|
|
});
|
|
for (i = 0; i < tr.length; ++i) tb.appendChild(tr[i]);
|
|
}
|
|
function makeSortable(table) {
|
|
var th = table.tHead, i;
|
|
th && (th = th.rows[0]) && (th = th.cells);
|
|
if (th) i = th.length;
|
|
else return; // if no `<thead>` then do nothing
|
|
while (--i >= 0) (function (i) {
|
|
th[i].onclick = function () {
|
|
sortTable(table, i);
|
|
};
|
|
}(i));
|
|
}
|
|
makeSortable(o('files'));
|
|
|
|
|
|
// extract songs + add play column
|
|
var mp = (function () {
|
|
var tracks = [];
|
|
var ret = {
|
|
'au': null,
|
|
'au_native': null,
|
|
'au_ogvjs': null,
|
|
'tracks': tracks,
|
|
'cover_url': ''
|
|
};
|
|
var re_audio = new RegExp('\.(opus|ogg|m4a|aac|mp3|wav|flac)$', 'i');
|
|
|
|
var trs = document.getElementById('files').getElementsByTagName('tbody')[0].getElementsByTagName('tr');
|
|
for (var a = 0, aa = trs.length; a < aa; a++) {
|
|
var tds = trs[a].getElementsByTagName('td');
|
|
var link = tds[1].getElementsByTagName('a')[0];
|
|
var url = link.getAttribute('href');
|
|
|
|
var m = re_audio.exec(url);
|
|
if (m) {
|
|
var ntrack = tracks.length;
|
|
tracks.push(url);
|
|
|
|
tds[0].innerHTML = '<a id="trk' + ntrack + '" href="#trk' + ntrack + '" class="play">play</a></td>';
|
|
}
|
|
}
|
|
|
|
for (var a = 0, aa = tracks.length; a < aa; a++)
|
|
o('trk' + a).onclick = ev_play;
|
|
|
|
ret.vol = localStorage.getItem('vol');
|
|
if (ret.vol !== null)
|
|
ret.vol = parseFloat(ret.vol);
|
|
else
|
|
ret.vol = 0.5;
|
|
|
|
ret.expvol = function () {
|
|
return 0.5 * ret.vol + 0.5 * ret.vol * ret.vol;
|
|
};
|
|
|
|
ret.setvol = function (vol) {
|
|
ret.vol = Math.max(Math.min(vol, 1), 0);
|
|
localStorage.setItem('vol', vol);
|
|
|
|
if (ret.au)
|
|
ret.au.volume = ret.expvol();
|
|
};
|
|
|
|
return ret;
|
|
})();
|
|
|
|
|
|
// toggle player widget
|
|
var widget = (function () {
|
|
var ret = {};
|
|
var widget = document.getElementById('widget');
|
|
var wtoggle = document.getElementById('wtoggle');
|
|
var touchmode = false;
|
|
var side_open = false;
|
|
var was_paused = true;
|
|
|
|
ret.open = function () {
|
|
if (side_open)
|
|
return false;
|
|
|
|
widget.className = 'open';
|
|
side_open = true;
|
|
return true;
|
|
};
|
|
ret.close = function () {
|
|
if (!side_open)
|
|
return false;
|
|
|
|
widget.className = '';
|
|
side_open = false;
|
|
return true;
|
|
};
|
|
ret.toggle = function (e) {
|
|
ret.open() || ret.close();
|
|
ev(e);
|
|
return false;
|
|
};
|
|
ret.paused = function (paused) {
|
|
if (was_paused != paused) {
|
|
was_paused = paused;
|
|
o('bplay').innerHTML = paused ? '▶' : '⏸';
|
|
}
|
|
};
|
|
var click_handler = function (e) {
|
|
if (!touchmode)
|
|
ret.toggle(e);
|
|
|
|
return false;
|
|
};
|
|
if (window.Touch) {
|
|
var touch_handler = function (e) {
|
|
touchmode = true;
|
|
return ret.toggle(e);
|
|
};
|
|
wtoggle.addEventListener('touchstart', touch_handler, false);
|
|
}
|
|
wtoggle.onclick = click_handler;
|
|
return ret;
|
|
})();
|
|
|
|
|
|
// buffer/position bar
|
|
var pbar = (function () {
|
|
var r = {};
|
|
r.bcan = o('barbuf');
|
|
r.pcan = o('barpos');
|
|
r.bctx = r.bcan.getContext('2d');
|
|
r.pctx = r.pcan.getContext('2d');
|
|
|
|
var bctx = r.bctx;
|
|
var pctx = r.pctx;
|
|
var scale = (window.devicePixelRatio || 1) / (
|
|
bctx.webkitBackingStorePixelRatio ||
|
|
bctx.mozBackingStorePixelRatio ||
|
|
bctx.msBackingStorePixelRatio ||
|
|
bctx.oBackingStorePixelRatio ||
|
|
bctx.BackingStorePixelRatio || 1);
|
|
|
|
var gradh = 0;
|
|
var grad = null;
|
|
|
|
r.drawbuf = function () {
|
|
var cs = getComputedStyle(r.bcan);
|
|
var sw = parseInt(cs['width']);
|
|
var sh = parseInt(cs['height']);
|
|
var sm = sw * 1.0 / mp.au.duration;
|
|
|
|
r.bcan.width = (sw * scale);
|
|
r.bcan.height = (sh * scale);
|
|
bctx.setTransform(scale, 0, 0, scale, 0, 0);
|
|
|
|
if (!grad || gradh != sh) {
|
|
grad = bctx.createLinearGradient(0, 0, 0, sh);
|
|
grad.addColorStop(0, 'hsl(85,35%,42%)');
|
|
grad.addColorStop(0.49, 'hsl(85,40%,49%)');
|
|
grad.addColorStop(0.50, 'hsl(85,37%,47%)');
|
|
grad.addColorStop(1, 'hsl(85,35%,42%)');
|
|
gradh = sh;
|
|
}
|
|
bctx.fillStyle = grad;
|
|
bctx.clearRect(0, 0, sw, sh);
|
|
for (var a = 0; a < mp.au.buffered.length; a++) {
|
|
var x1 = sm * mp.au.buffered.start(a);
|
|
var x2 = sm * mp.au.buffered.end(a);
|
|
bctx.fillRect(x1, 0, x2 - x1, sh);
|
|
}
|
|
};
|
|
r.drawpos = function () {
|
|
var cs = getComputedStyle(r.bcan);
|
|
var sw = parseInt(cs['width']);
|
|
var sh = parseInt(cs['height']);
|
|
var sm = sw * 1.0 / mp.au.duration;
|
|
|
|
r.pcan.width = (sw * scale);
|
|
r.pcan.height = (sh * scale);
|
|
pctx.setTransform(scale, 0, 0, scale, 0, 0);
|
|
pctx.clearRect(0, 0, sw, sh);
|
|
|
|
var w = 8;
|
|
var x = sm * mp.au.currentTime;
|
|
pctx.fillStyle = '#573'; pctx.fillRect((x - w / 2) - 1, 0, w + 2, sh);
|
|
pctx.fillStyle = '#dfc'; pctx.fillRect((x - w / 2), 0, 8, sh);
|
|
};
|
|
return r;
|
|
})();
|
|
|
|
|
|
// volume bar
|
|
var vbar = (function () {
|
|
var r = {};
|
|
r.can = o('pvol');
|
|
r.ctx = r.can.getContext('2d');
|
|
|
|
var bctx = r.ctx;
|
|
var scale = (window.devicePixelRatio || 1) / (
|
|
bctx.webkitBackingStorePixelRatio ||
|
|
bctx.mozBackingStorePixelRatio ||
|
|
bctx.msBackingStorePixelRatio ||
|
|
bctx.oBackingStorePixelRatio ||
|
|
bctx.BackingStorePixelRatio || 1);
|
|
|
|
var gradh = 0;
|
|
var grad1 = null;
|
|
var grad2 = null;
|
|
|
|
r.draw = function () {
|
|
var cs = getComputedStyle(r.can);
|
|
var sw = parseInt(cs['width']);
|
|
var sh = parseInt(cs['height']);
|
|
|
|
r.can.width = (sw * scale);
|
|
r.can.height = (sh * scale);
|
|
bctx.setTransform(scale, 0, 0, scale, 0, 0);
|
|
|
|
if (!grad1 || gradh != sh) {
|
|
gradh = sh;
|
|
|
|
grad1 = bctx.createLinearGradient(0, 0, 0, sh);
|
|
grad1.addColorStop(0, 'hsl(50,45%,42%)');
|
|
grad1.addColorStop(0.49, 'hsl(50,50%,49%)');
|
|
grad1.addColorStop(0.50, 'hsl(50,47%,47%)');
|
|
grad1.addColorStop(1, 'hsl(50,45%,42%)');
|
|
|
|
grad2 = bctx.createLinearGradient(0, 0, 0, sh);
|
|
grad2.addColorStop(0, 'hsl(205,10%,16%)');
|
|
grad2.addColorStop(0.49, 'hsl(205,15%,20%)');
|
|
grad2.addColorStop(0.50, 'hsl(205,13%,18%)');
|
|
grad2.addColorStop(1, 'hsl(205,10%,16%)');
|
|
}
|
|
bctx.fillStyle = grad2; bctx.fillRect(0, 0, sw, sh);
|
|
bctx.fillStyle = grad1; bctx.fillRect(0, 0, sw * mp.vol, sh);
|
|
};
|
|
|
|
var rect;
|
|
function mousedown(e) {
|
|
rect = r.can.getBoundingClientRect();
|
|
mousemove(e);
|
|
}
|
|
function mousemove(e) {
|
|
if (e.changedTouches && e.changedTouches.length > 0) {
|
|
e = e.changedTouches[0];
|
|
}
|
|
else if (e.buttons === 0) {
|
|
r.can.onmousemove = null;
|
|
return;
|
|
}
|
|
|
|
var x = e.clientX - rect.left;
|
|
var mul = x * 1.0 / rect.width;
|
|
if (mul > 0.98)
|
|
mul = 1;
|
|
|
|
mp.setvol(mul);
|
|
r.draw();
|
|
}
|
|
r.can.onmousedown = function (e) {
|
|
if (e.button !== 0)
|
|
return;
|
|
|
|
r.can.onmousemove = mousemove;
|
|
mousedown(e);
|
|
};
|
|
r.can.onmouseup = function (e) {
|
|
if (e.button === 0)
|
|
r.can.onmousemove = null;
|
|
};
|
|
if (window.Touch) {
|
|
r.can.ontouchstart = mousedown;
|
|
r.can.ontouchmove = mousemove;
|
|
}
|
|
r.draw();
|
|
return r;
|
|
})();
|
|
|
|
|
|
// hook up the widget buttons
|
|
(function () {
|
|
var bskip = function (n) {
|
|
var tid = null;
|
|
if (mp.au)
|
|
tid = mp.au.tid;
|
|
|
|
if (tid !== null)
|
|
play(tid + n);
|
|
else
|
|
play(0);
|
|
};
|
|
o('bplay').onclick = function (e) {
|
|
ev(e);
|
|
if (mp.au) {
|
|
if (mp.au.paused)
|
|
mp.au.play();
|
|
else
|
|
mp.au.pause();
|
|
}
|
|
else
|
|
play(0);
|
|
};
|
|
o('bprev').onclick = function (e) {
|
|
ev(e);
|
|
bskip(-1);
|
|
};
|
|
o('bnext').onclick = function (e) {
|
|
ev(e);
|
|
bskip(1);
|
|
};
|
|
o('barpos').onclick = function (e) {
|
|
if (!mp.au) {
|
|
//dbg((new Date()).getTime());
|
|
return play(0);
|
|
}
|
|
|
|
var rect = pbar.pcan.getBoundingClientRect();
|
|
var x = e.clientX - rect.left;
|
|
var mul = x * 1.0 / rect.width;
|
|
|
|
mp.au.currentTime = mp.au.duration * mul;
|
|
|
|
if (mp.au === mp.au_native)
|
|
// hack: ogv.js breaks on .play() during playback
|
|
mp.au.play();
|
|
};
|
|
})();
|
|
|
|
|
|
// periodic tasks
|
|
(function () {
|
|
var nth = 0;
|
|
var last_skip_url = '';
|
|
var progress_updater = function () {
|
|
if (!mp.au) {
|
|
widget.paused(true);
|
|
}
|
|
else {
|
|
// indicate playback state in ui
|
|
widget.paused(mp.au.paused);
|
|
|
|
// draw current position in song
|
|
if (!mp.au.paused)
|
|
pbar.drawpos();
|
|
|
|
// occasionally draw buffered regions
|
|
if (++nth == 10) {
|
|
pbar.drawbuf();
|
|
nth = 0;
|
|
}
|
|
|
|
// switch to next track if approaching the end
|
|
if (last_skip_url != mp.au.src) {
|
|
var pos = mp.au.currentTime;
|
|
var len = mp.au.duration;
|
|
if (pos > 0 && pos > len - 0.1) {
|
|
last_skip_url = mp.au.src;
|
|
play(mp.au.tid + 1);
|
|
}
|
|
}
|
|
}
|
|
setTimeout(progress_updater, 100);
|
|
};
|
|
progress_updater();
|
|
})();
|
|
|
|
|
|
// event from play button next to a file in the list
|
|
function ev_play(e) {
|
|
ev(e);
|
|
play(parseInt(this.getAttribute('id').substr(3)));
|
|
return false;
|
|
}
|
|
|
|
|
|
function setclass(id, clas) {
|
|
o(id).setAttribute('class', clas);
|
|
}
|
|
|
|
|
|
var need_ogv = true;
|
|
try {
|
|
need_ogv = new Audio().canPlayType('audio/ogg; codecs=opus') !== 'probably';
|
|
|
|
if (/ Edge\//.exec(navigator.userAgent + ''))
|
|
need_ogv = true;
|
|
}
|
|
catch (ex) { }
|
|
|
|
|
|
// plays the tid'th audio file on the page
|
|
function play(tid, call_depth) {
|
|
if (mp.tracks.length == 0)
|
|
return alert('no audio found wait what');
|
|
|
|
while (tid >= mp.tracks.length)
|
|
tid -= mp.tracks.length;
|
|
|
|
while (tid < 0)
|
|
tid += mp.tracks.length;
|
|
|
|
if (mp.au) {
|
|
mp.au.pause();
|
|
setclass('trk' + mp.au.tid, 'play');
|
|
}
|
|
|
|
// ogv.js breaks on .play() unless directly user-triggered
|
|
var hack_attempt_play = true;
|
|
|
|
var url = mp.tracks[tid];
|
|
if (need_ogv && /\.(ogg|opus)$/i.test(url)) {
|
|
if (mp.au_ogvjs) {
|
|
mp.au = mp.au_ogvjs;
|
|
}
|
|
else if (window['OGVPlayer']) {
|
|
mp.au = mp.au_ogvjs = new OGVPlayer();
|
|
hack_attempt_play = false;
|
|
mp.au.addEventListener('error', evau_error, true);
|
|
mp.au.addEventListener('progress', pbar.drawpos, false);
|
|
widget.open();
|
|
}
|
|
else {
|
|
if (call_depth !== undefined)
|
|
return alert('failed to load ogv.js');
|
|
|
|
show_modal('<h1>loading ogv.js</h1><h2>thanks apple</h2>');
|
|
|
|
import_js('/.cpr/deps/ogv.js', function () {
|
|
play(tid, 1);
|
|
});
|
|
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
if (!mp.au_native) {
|
|
mp.au = mp.au_native = new Audio();
|
|
mp.au.addEventListener('error', evau_error, true);
|
|
mp.au.addEventListener('progress', pbar.drawpos, false);
|
|
widget.open();
|
|
}
|
|
mp.au = mp.au_native;
|
|
}
|
|
|
|
mp.au.tid = tid;
|
|
mp.au.src = url;
|
|
mp.au.volume = mp.expvol();
|
|
setclass('trk' + tid, 'play act');
|
|
|
|
try {
|
|
if (hack_attempt_play)
|
|
mp.au.play();
|
|
|
|
if (mp.au.paused)
|
|
autoplay_blocked();
|
|
|
|
location.hash = 'trk' + tid;
|
|
pbar.drawbuf();
|
|
return true;
|
|
}
|
|
catch (ex) {
|
|
alert('playback failed: ' + ex);
|
|
}
|
|
setclass('trk' + mp.au.tid, 'play');
|
|
setTimeout('play(' + (mp.au.tid + 1) + ');', 500);
|
|
}
|
|
|
|
|
|
// event from the audio object if something breaks
|
|
function evau_error(e) {
|
|
var err = '';
|
|
var eplaya = (e && e.target) || (window.event && window.event.srcElement);
|
|
var url = eplaya.src;
|
|
|
|
switch (eplaya.error.code) {
|
|
case eplaya.error.MEDIA_ERR_ABORTED:
|
|
err = "You aborted the playback attempt (how tho)";
|
|
break;
|
|
case eplaya.error.MEDIA_ERR_NETWORK:
|
|
err = "Your internet connection is wonky";
|
|
break;
|
|
case eplaya.error.MEDIA_ERR_DECODE:
|
|
err = "This file is supposedly corrupted??";
|
|
break;
|
|
case eplaya.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
|
|
err = "Your browser does not understand this audio format";
|
|
break;
|
|
default:
|
|
err = 'Unknown Errol';
|
|
break;
|
|
}
|
|
if (eplaya.error.message)
|
|
err += '\n\n' + eplaya.error.message;
|
|
|
|
err += '\n\nFile: «' + decodeURIComponent(eplaya.src.split('/').slice(-1)[0]) + '»';
|
|
|
|
alert(err);
|
|
}
|
|
|
|
|
|
// show a fullscreen message
|
|
function show_modal(html) {
|
|
var body = document.body || document.getElementsByTagName('body')[0];
|
|
var div = document.createElement('div');
|
|
div.setAttribute('id', 'blocked');
|
|
div.innerHTML = html;
|
|
unblocked();
|
|
body.appendChild(div);
|
|
}
|
|
|
|
|
|
// hide fullscreen message
|
|
function unblocked() {
|
|
var dom = o('blocked');
|
|
if (dom)
|
|
dom.parentNode.removeChild(dom);
|
|
}
|
|
|
|
|
|
// show ui to manually start playback of a linked song
|
|
function autoplay_blocked(tid) {
|
|
show_modal(
|
|
'<div id="blk_play"><a href="#" id="blk_go"></a></div>' +
|
|
'<div id="blk_abrt"><a href="#" id="blk_na">Cancel<br />(show file list)</a></div>');
|
|
|
|
var go = o('blk_go');
|
|
var na = o('blk_na');
|
|
|
|
var fn = mp.tracks[mp.au.tid].split(/\//).pop();
|
|
fn = decodeURIComponent(fn.replace(/\+/g, ' '));
|
|
|
|
go.textContent = 'Play "' + fn + '"';
|
|
go.onclick = function (e) {
|
|
if (e) e.preventDefault();
|
|
unblocked();
|
|
mp.au.play();
|
|
};
|
|
na.onclick = unblocked;
|
|
}
|
|
|
|
|
|
// autoplay linked track
|
|
(function () {
|
|
var v = location.hash;
|
|
if (v && v.length > 4 && v.indexOf('#trk') === 0)
|
|
play(parseInt(v.substr(4)));
|
|
})();
|
|
|
|
|
|
//widget.open();
|