// vim: set ts=2 sw=2 et tw=80: class App { static get BRUSHES() { return { 'PenBrush': new PenBrush(), 'DiscBrush': new DiscBrush(), 'StarBrush': new StarBrush(), }; } render_timestamp = 0; render_last = null; render_timeout = null; socketSessionId = null; registerFavoriteEvents(_id, dom) { const imgurForm = dom.querySelector('form.favorites'); const replaceToggle = dom.querySelector('input[name=replace]'); const albumInput = imgurForm.querySelector('input[name=album]'); const oldNameInput = imgurForm.querySelector('input[name=old_name]'); const replacePanel = imgurForm.querySelector('.replace'); const detailsPanel = imgurForm.querySelector('.details'); dom.querySelector('button.imgur').onclick = () => { imgurForm.classList.toggle('hidden'); }; replaceToggle.onclick = () => { if (replaceToggle.checked) { replacePanel.classList.remove('hidden'); albumInput.setAttribute('disabled', 'disabled'); albumInput.removeAttribute('required'); oldNameInput.setAttribute('required', 'required'); } else { replacePanel.classList.add('hidden'); albumInput.removeAttribute('disabled'); albumInput.setAttribute('required', 'required'); oldNameInput.removeAttribute('required'); } }; imgurForm.onsubmit = e => { imgurForm.classList.remove('ok'); imgurForm.classList.remove('err'); detailsPanel.innerHTML = ''; e.preventDefault(); const data = new FormData(imgurForm); const obj = { dataURL: data.get('dataURL'), name: dom.querySelector('form.data input[name=name]').value, replace: data.get('replace'), oldName: data.get('old_name'), album: data.get('album'), tags: data.get('tags'), favorites: data.get('favorites') }; doJSONRequest('POST', '/imgur/ordeal', {}, obj) .then(e => { if (e.ordealSuccess === true) { imgurForm.classList.add('ok'); if (e.views !== undefined) { detailsPanel.innerHTML += `Views: ${e.views}
`; } if (e.votes) { detailsPanel.innerHTML += `Upvotes: ${e.votes.ups}
Downvotes: ${e.votes.downs}
`; } } else { imgurForm.classList.add('err'); if (e.error) { detailsPanel.innerHTML += ` Error: ${e.error}
`; } } }).catch(e => { console.error(e); imgurForm.classList.add('err'); }); }; const form = dom.querySelector('form.data'); const delay = (f, ...args) => { const execute = (f, ...args) => { console.log('don\'t delay, solve Carzaniga today', ...args); this.render_timestamp = new Date().getTime(); this.render_timeout = null; this.render_last = null; f(...args); }; if (new Date().getTime() - this.render_timestamp < 100000) { console.log('delaying', ...args); this.render_last = () => execute(f, ...args); if (this.render_timeout === null) { this.render_timeout = setTimeout(() => this.render_last(), 100); } return; } execute(f, ...args); } form.querySelector('input[name=name]').onkeyup = () => delay(submit, 'data-update', false); const submit = (action, update = true) => { let p; const formData = new FormData(form); const name = formData.get('name'); const dataURL = formData.get('dataURL'); const query = '?socketid=' + this.socketSessionId; switch (action) { case 'data-update': p = doJSONRequest('PUT', '/favorites/' + _id + query, {}, { name, dataURL }); break; case 'data-delete': p = doJSONRequest('DELETE', '/favorites/' + _id + query); break; case 'data-bookmark': p = doJSONRequest('PUT', `/favorites/${_id}/bookmarked` + query, {}, { bookmarked: true }); break; case 'data-del-bookmark': p = doJSONRequest('PUT', `/favorites/${_id}/bookmarked` + query, {}, { bookmarked: false }); break; default: console.warn(action, ' is not a valid action'); return; } p.catch(console.error); if (action == 'data-delete' && update) { p.then(_ => dom.remove()); } else if (update) { p.then(e => this.renderFavorite(e, dom)); } if (!update) { // updating via javascript to not disrupt cursor in name when // replacing html dom.querySelector('h3').innerText = name; } }; form.onsubmit = e => { e.preventDefault(); const formData = new FormData(form); const name = formData.get('name'); const dataURL = formData.get('dataURL'); const action = e.explicitOriginalTarget.getAttributeNames() .filter(e => e.startsWith('data-'))[0]; submit(action); }; } renderFavorite(data, div) { data.bookmarked = data.bookmarked === true || data.bookmarked === 'true'; dust.render('favourite_partial', data, (err, out) => { if (err) { console.error(err); } else { let dom; if (div === 'top' || !div) { dom = document.createElement('div'); dom.id = 'favorite_' + data._id; dom.innerHTML = out; if (div === 'top') { this.favourites.prepend(dom); } else { this.favourites.appendChild(dom); } } else { div.innerHTML = out; dom = div; } dom.className = data.bookmarked ? 'bookmarked' : ''; this.registerFavoriteEvents(data._id, dom); } }); } getFavorites(nameFilter, onlyBookmarked = false) { let url = '/favorites/search?ajax=true'; if (nameFilter) { url += '&name=' + encodeURIComponent(nameFilter); } if (onlyBookmarked) { url += '&bookmarked=true'; } doFetchRequest('GET', url, { 'Accept': 'text/html' }) .then(res => res.text()) .then(html => { this.favourites.innerHTML = html; this.favourites.childNodes.forEach(e => { const id = e.id.substring('favorite_'.length); this.registerFavoriteEvents(id, e); }); }) .catch(console.error); } initSocket() { this.socket = io(); this.socket.on('connect', () => { console.log('Socket connected'); }); this.socket.on('sessionId', (msg) => { console.log('Socket session id is', msg.id); this.socketSessionId = msg.id; }); this.socket.on('disconnect', (reason) => { console.log('Socket disconnected'); }); this.socket.on('reconnect', (attemptNumber) => { console.log('Socket reconnected'); }); this.socket.on('favorite.created', (msg) => { if (msg.socketid != this.socketSessionId) { console.log(msg); this.renderFavorite(msg, 'top'); } }); this.socket.on('favorite.updated', (msg) => { if (msg.socketid != this.socketSessionId) { this.renderFavorite(msg, document .getElementById('favorite_' + msg._id)); } console.log(msg); }); this.socket.on('favorite.deleted', (msg) => { if (msg.socketid != this.socketSessionId) { document.getElementById('favorite_' + msg._id).remove(); } }); this.socket.on('canvas.draw', (arr) => { console.log('canvas.draw recieved', arr); const m = this.mousedown; this.mousedown = false; if (m) { const last = history.pop(); history.pushAll(arr); history.pushAll(last); } else { history.pushAll(arr); } this.redrawAll(); this.mousedown = m; }); this.socket.on('canvas.clear', () => { this.clear(); this.background = null; this.history.clear(); this.redrawAll(); }); this.socket.on('canvas.undo', () => { history.pop(); this.redrawAll(); }); } constructor(conf) { if (!(typeof conf === 'object' && conf)) { throw new Error('Argument conf different from specification'); } this.canvas = document.getElementById(conf.canvas); if (!this.canvas || this.canvas.tagName !== 'CANVAS') { throw new Error(`canvas is not a canvas`); } this.ctx = this.canvas.getContext('2d'); this.favourites = document.querySelector('div#favourites'); this.initSocket(); this.getFavorites(false); const nameFilter = document.getElementById('name-filter'); const bookmarkFilter = document.getElementById('bookmarked-filter'); const onSearch = () => { const name = nameFilter.value; const bookmark = bookmarkFilter.checked; this.getFavorites(name, bookmark); } nameFilter.onkeyup = onSearch; bookmarkFilter.onclick = onSearch; const imgurSearch = document.getElementById('imgur-search'); const imgurSearchBtn = document.getElementById('imgur-search-btn'); const imgurSearchDetails = document.getElementById('imgur-search-details'); imgurSearchBtn.onclick = async () => { imgurSearch.classList.remove('ok'); imgurSearch.classList.remove('err'); imgurSearchDetails.innerHTML = '
'; const e = await doJSONRequest('POST', '/imgur/search', {}, { name: imgurSearch.value }); if (e.success) { imgurSearch.classList.add('ok'); } else { imgurSearch.classList.add('err'); if(e.error) { imgurSearchDetails.innerHTML = ` Error: ${e.error}
`; } } }; if (typeof conf.buttons === 'object' && conf.buttons) { this.buttons = {} for (const b of ['clear', 'undo', 'camera']) this.buttons[b] = document.getElementById(conf.buttons[b]); if (this.buttons.clear) { this.buttons.clear.addEventListener('click', () => { this.erase(); this.background = null; history.clear(); this.socket.emit('canvas.clear', null); }); } if (this.buttons.undo) { this.buttons.undo.addEventListener('click', () => { history.pop(); this.redrawAll(); this.socket.emit('canvas.undo', null); }); } if (this.buttons.camera) { this.buttons.camera.addEventListener('click', () => { const base64 = this.canvas.toDataURL(); const data = { dataURL: base64, name: 'My New Masterpiece', bookmarked: false }; doJSONRequest('POST', '/favorites?socketid=' + this.socketSessionId, {}, data) .then(data => this.renderFavorite(data, 'top')) .catch(console.error); }); } const player = document.createElement('video'); player.addEventListener('loadeddata', () => { player.play(); setTimeout(() => { const imgCanvas = document.createElement('canvas'); imgCanvas.width = this.canvas.width; imgCanvas.height = this.canvas.height; const img = document.createElement('img'); imgCanvas.getContext('2d').drawImage(player, 0, 0, imgCanvas.width, imgCanvas.height); console.log(imgCanvas.toDataURL()); img.src = imgCanvas.toDataURL(); img.addEventListener('load', () => { this.background = img; this.redrawAll(); }); player.srcObject.getVideoTracks().forEach(track => track.stop()); }, 100); }); const button = document.createElement('button'); button.type = 'button'; button.innerHTML = 'Photo'; button.addEventListener('click', () => { navigator.mediaDevices.getUserMedia({ video: true }) .then((stream) => { player.srcObject = stream; }); }); document.getElementById('left-toolbar').appendChild(button); } this.ctx.lineWidth = 1; this.strokeStyle = this.constructor.defaultStrokeStyle; this.brush = 'PenBrush'; const brushToolbar = document.querySelector('#brush-toolbar'); if (brushToolbar) { for (const name in App.BRUSHES) { const b = document.createElement('button'); b.innerText = name; b.addEventListener('click', () => this.brush = name); brushToolbar.appendChild(b); } const label = document.createElement('label'); label.setAttribute('for', 'color'); const color = document.createElement('input'); color.type = 'color'; color.name = 'color'; color.value = this.constructor.defaultStrokeStyle; color.addEventListener('change', () => this.strokeStyle = color.value); brushToolbar.appendChild(label); brushToolbar.appendChild(color); } const toMouse = (e, func) => { if (e && e.touches && e.touches[0]) { return func.bind(this)({ offsetX: e.touches[0].pageX - this.canvas.offsetLeft, offsetY: e.touches[0].pageY - this.canvas.offsetTop }); } }; this.canvas.addEventListener('touchstart', e => toMouse(e, this.startPath)); this.canvas.addEventListener('mousedown', this.startPath.bind(this)); this.canvas.addEventListener('touchmove', e => toMouse(e, this.draw)); this.canvas.addEventListener('mousemove', this.draw.bind(this)); this.canvas.addEventListener('touchcancel', e => toMouse(e, this.endPath)); this.canvas.addEventListener('mouseup', this.endPath.bind(this)); this.canvas.addEventListener('mouseout', this.endPath.bind(this)); } static get defaultStrokeStyle() { return 'black'; } get strokeStyle() { if (this.ctx.strokeStyle == '#000000') { return 'black'; } return this.ctx.strokeStyle; } set strokeStyle(style) { if (typeof style !== 'string') { throw new Error('style is not a string'); } this.ctx.strokeStyle = style; } get brush() { return this._brush.name; } set brush(brushName) { this._brush = App.BRUSHES[brushName]; } erase() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); } startPath(e, record = true) { this.ctx.beginPath(); this.ctx.moveTo(e.offsetX, e.offsetY); if (record) { history.initializeNewPath(); history.push(new Stroke(this.brush, this.strokeStyle, e.offsetX, e.offsetY)); } if (e instanceof MouseEvent) { this.mousedown = true; } } draw(e, beginNew = true, record = true) { if (this.mousedown || !(e instanceof MouseEvent)) { this._brush.draw(this.ctx, this.strokeStyle, e.offsetX, e.offsetY); if (record) { const lastArr = history.push(new Stroke(this.brush, this.strokeStyle, e.offsetX, e.offsetY)); if (!beginNew) { console.log('canvas.draw submit', lastArr); this.socket.emit('canvas.draw', lastArr); } } if (beginNew) { this.ctx.beginPath(); this.ctx.moveTo(e.offsetX, e.offsetY); } else { if (e instanceof MouseEvent) { this.mousedown = false; } } } } endPath(e, record = true) { this.draw(e, false, record); } drawPath(path) { const last = path.length - 1; const lastBrush = this.brush; const lastStyle = this.strokeStyle; for (let i = 0; i <= last; i++) { this.brush = path[i].brushName; this.strokeStyle = path[i].strokeStyle; switch(i) { case 0: this.startPath(path[i], false); break; case last: this.endPath(path[i], false); break; default: this.draw(path[i], true, false); } } this.brush = lastBrush; this.strokeStyle = lastStyle; } redrawAll() { this.erase(); if (this.background) { this.ctx.drawImage(this.background, 0, 0, this.canvas.width, this.canvas.height); } for (const path of history.paths) { this.drawPath(path); } } }