diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 648cef64..cc1a2404 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -287,7 +287,11 @@ class HttpCli(object): if not buf: break - self.s.send(buf) + try: + self.s.send(buf) + except ConnectionResetError: + return False + # TODO propagate (self.ok or return) def tx_mounts(self): html = self.conn.tpl_mounts.render(this=self) diff --git a/copyparty/web/browser.html b/copyparty/web/browser.html index 466b2dec..3ca4356c 100644 --- a/copyparty/web/browser.html +++ b/copyparty/web/browser.html @@ -42,6 +42,7 @@ + \ No newline at end of file diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js new file mode 100644 index 00000000..f9ce0cd0 --- /dev/null +++ b/copyparty/web/browser.js @@ -0,0 +1,591 @@ +// 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 = ['

you hit a bug!

please screenshot this error and send me a copy arigathanks gozaimuch (ed/irc.rizon.net or ed#2644)

', + esc(String(msg)), '

', esc(url + ' @' + lineNo + ':' + columnNo), '

']; + + if (error) { + var find = ['desc', 'stack', 'trace']; + for (var a = 0; a < find.length; a++) + if (String(error[find[a]]) !== 'undefined') + html.push('

' + find[a] + '

' + + esc(String(error[find[a]])).replace(/\n/g, '
\n')); + } + document.body.style.fontSize = '0.8em'; + 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 ebi(id) { + return document.getElementById(id); +} + + +// 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 re_cover = new RegExp(/^(cover|folder|cd|front|back)\.(jpe?g|png|gif)$/, '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 = 'play'; + } + } + + for (var a = 0, aa = tracks.length; a < aa; a++) + ebi('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(); + e.preventDefault(); + return false; + }; + ret.paused = function (paused) { + if (was_paused != paused) { + was_paused = paused; + ebi('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 = ebi('barbuf'); + r.pcan = ebi('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 = ebi('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); + }; + ebi('bplay').onclick = function (e) { + e.preventDefault(); + if (mp.au) { + if (mp.au.paused) + mp.au.play(); + else + mp.au.pause(); + } + else + play(0); + }; + ebi('bprev').onclick = function (e) { + e.preventDefault(); + bskip(-1); + }; + ebi('bnext').onclick = function (e) { + e.preventDefault(); + bskip(1); + }; + ebi('barpos').onclick = function (e) { + if (!mp.au) + 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) { + e.preventDefault(); + play(parseInt(this.getAttribute('id').substr(3))); + return false; +} + + +function setclass(id, clas) { + ebi(id).setAttribute('class', clas); +} + + +var iOS = !!navigator.platform && + /iPad|iPhone|iPod/.test(navigator.platform); + + +// 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 (iOS && /\.(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('

loading ogv.js

thanks apple

'); + + import_js('/.nfi-audio/ogvjs-1.5.8/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); + play(eplaya.tid + 1); +} + + +// 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 = ebi('blocked'); + if (dom) + dom.remove(); +} + + +// show ui to manually start playback of a linked song +function autoplay_blocked(tid) { + show_modal(` +
+ +
+
+ Cancel

(show file list)
+
`); + + var go = ebi('blk_go'); + var na = ebi('blk_na'); + + go.textContent = 'Play "' + mp.tracks[mp.au.tid].split(/\//).pop() + '"'; + go.onclick = function () { + 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();