From 583afc9fe5e98ad742343d345054b76ffddc8075 Mon Sep 17 00:00:00 2001 From: bozokopic Date: Mon, 16 Apr 2018 17:10:27 +0200 Subject: WIP web frontend --- src_js/opcut/common.js | 35 ++++++++++++++++++++++++- src_js/opcut/fs.js | 1 + src_js/opcut/future.js | 45 ++++++++++++++++++++++++++++++++ src_js/opcut/grid.js | 45 ++++++++++++++++---------------- src_js/opcut/main.js | 1 + src_js/opcut/states.js | 7 ++++- src_js/opcut/vt.js | 69 +++++++++++++++++++++++++++++++++++++++++++++++-- src_py/opcut/server.py | 4 +-- src_web/style/main.scss | 56 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 235 insertions(+), 28 deletions(-) create mode 100644 src_js/opcut/future.js diff --git a/src_js/opcut/common.js b/src_js/opcut/common.js index 89088ac..9c5d16d 100644 --- a/src_js/opcut/common.js +++ b/src_js/opcut/common.js @@ -3,6 +3,8 @@ import iziToast from 'izitoast'; import r from 'opcut/renderer'; import * as u from 'opcut/util'; +import * as states from 'opcut/states'; +import * as fs from 'opcut/fs'; const calculateUrl = URI.resolve(window.location.href, './calculate'); @@ -41,6 +43,19 @@ export function calculate() { } +export function generateOutput(output_type) { + const msg = { + output_type: output_type, + result: r.get('result') + }; + const req = new XMLHttpRequest(); + req.onload = () => parseGenerateOutputResponse(JSON.parse(req.responseText), output_type); + req.open('POST', generateOutputUrl); + req.setRequestHeader('Content-Type', 'application/json'); + req.send(JSON.stringify(msg)); +} + + function validateCalculateRequest(msg) { if (!Number.isFinite(msg.params.cut_width) || msg.params.cut_width < 0) throw 'Invalid cut width'; @@ -68,7 +83,25 @@ function validateCalculateRequest(msg) { function parseCalculateResponse(msg) { - + r.change(u.pipe( + u.set('result', msg.result), + u.set('selected', states.main.selected) + )); + if (msg.result) { + showNotification('New calculation available', 'success'); + } else { + showNotification('Could not resolve calculation', 'error'); + } +} + + +function parseGenerateOutputResponse(msg, output_type) { + if (msg.data) { + const fileName = 'output.pdf'; + fs.saveB64Data(msg.data, fileName); + } else { + showNotification('Error generating output', 'error'); + }; } diff --git a/src_js/opcut/fs.js b/src_js/opcut/fs.js index b1d464e..f96485b 100644 --- a/src_js/opcut/fs.js +++ b/src_js/opcut/fs.js @@ -1,6 +1,7 @@ import h from 'hyperscript'; import FileSaver from 'file-saver'; +import * as u from 'opcut/util'; import * as ev from 'opcut/ev'; diff --git a/src_js/opcut/future.js b/src_js/opcut/future.js new file mode 100644 index 0000000..a435e89 --- /dev/null +++ b/src_js/opcut/future.js @@ -0,0 +1,45 @@ + +export function create() { + let data = { + done: false, + error: false, + result: undefined, + resolve: null, + reject: null + }; + let future = new Promise((resolve, reject) => { + data.resolve = resolve; + data.reject = reject; + if (data.error) { + reject(data.result); + } else if (data.done) { + resolve(data.resolve); + } + }); + future.done = () => data.done; + future.result = () => { + if (!data.done) + throw 'Future is not done'; + if (data.error) + throw data.error; + return data.result; + }; + future.setResult = result => { + if (data.done) + throw 'Result already set'; + data.result = result; + data.done = true; + if (data.resolve) + data.resolve(data.result); + }; + future.setError = error => { + if (data.done) + throw 'Result already set'; + data.error = true; + data.result = error; + data.done = true; + if (data.reject) + data.reject(error); + }; + return future; +} diff --git a/src_js/opcut/grid.js b/src_js/opcut/grid.js index 4403195..27f9a23 100644 --- a/src_js/opcut/grid.js +++ b/src_js/opcut/grid.js @@ -3,6 +3,7 @@ import Papa from 'papaparse'; import r from 'opcut/renderer'; import * as u from 'opcut/util'; import * as ev from 'opcut/ev'; +import * as fs from 'opcut/fs'; export const state = { @@ -57,7 +58,7 @@ export function tbody(gridPath, columns, validators) { } } const title = (validator ? validator(u.get(column, row), row) : null); - return ['td', { + return ['td' + (typeof column == 'function' ? '' : '.grid-col-' + column), { class: { invalid: title }, @@ -104,10 +105,9 @@ export function tfoot(gridPath, colspan, newItem, csvColumns) { ['button', { on: { click: () => { - const items = importCsv(csvColumns, newItem); - if (!items) - return; - r.change(itemsPath, state => state.concat(items)); + importCsv(csvColumns, newItem).then(items => { + r.change(itemsPath, state => state.concat(items)); + }); } }}, ['span.fa.fa-download'], @@ -253,24 +253,25 @@ export function selectColumn(gridPath, column, values) { function importCsv(csvColumns, newItem) { - fs.loadText('csv').then(csvData => { - const result = Papa.parse(csvData, { - delimiter: ';', - skipEmptyLines: true, - header: true + return new Promise(resolve => { + fs.loadText('csv').then(csvData => { + const result = Papa.parse(csvData, { + delimiter: ';', + skipEmptyLines: true, + header: true + }); + const items = []; + for (let i of result.data) { + if (!Object.keys(i).every(k => u.contains(k, Object.keys(csvColumns)))) + continue; + const item = u.reduce( + (acc, [name, column]) => column.toItem(i[name], acc), + newItem, + u.toPairs(csvColumns)); + items.push(item); + } + resolve(items); }); - - const items = []; - for (let i of result.data) { - if (!Object.keys(i).every(k => u.contains(k, Object.keys(csvColumns)))) - continue; - const item = u.reduce( - (acc, [name, column]) => column.toItem(i[name], acc), - newItem, - u.toPairs(csvColumns)); - items.push(item); - } - return items; }); } diff --git a/src_js/opcut/main.js b/src_js/opcut/main.js index 8d00c56..7014798 100644 --- a/src_js/opcut/main.js +++ b/src_js/opcut/main.js @@ -14,3 +14,4 @@ function main() { ev.on(window, 'load', main); +window.r = r; diff --git a/src_js/opcut/states.js b/src_js/opcut/states.js index 12c3f8a..1326a70 100644 --- a/src_js/opcut/states.js +++ b/src_js/opcut/states.js @@ -7,7 +7,12 @@ export const main = { cut_width: '1', panels: grid.state, items: grid.state - } + }, + result: null, + selected: { + panel: null, + item: null + }, }; diff --git a/src_js/opcut/vt.js b/src_js/opcut/vt.js index f942e4b..e71d5df 100644 --- a/src_js/opcut/vt.js +++ b/src_js/opcut/vt.js @@ -9,8 +9,8 @@ import * as validators from 'opcut/validators'; export function main() { return ['div.window', leftPanel(), - ['div.center-panel'], - ['div.right-panel'] + centerPanel(), + rightPanel() ]; } @@ -26,6 +26,7 @@ function leftPanel() { ['div.title', 'OPCUT'], ['a', { props: { + title: 'GitHub', href: 'https://github.com/bozokopic/opcut' }}, ['span.fa.fa-github'] @@ -134,3 +135,67 @@ function leftPanelItems() { ] ]; } + + +function rightPanel() { + const result = r.get('result'); + return ['div.right-panel', (!result ? + [] : + [ + ['div.toolbar', + ['button', { + on: { + click: () => common.generateOutput('PDF') + }}, + ['span.fa.fa-file-pdf-o'], + ' PDF' + ] + ], + Object.keys(result.params.panels).map(rightPanelPanel) + ]) + ]; +} + + +function rightPanelPanel(panel) { + const isSelected = item => u.equals(r.get('selected'), {panel: panel, item: item}); + return ['div.panel', + ['div.panel-name', { + class: { + selected: isSelected(null) + }, + on: { + click: () => r.set('selected', {panel: panel, item: null}) + }}, + panel + ], + u.filter(used => used.panel == panel)(r.get('result', 'used')).map(used => + ['div.item', { + class: { + selected: isSelected(used.item) + }, + on: { + click: () => r.set('selected', {panel: panel, item: used.item}) + }}, + ['div.item-name', used.item], + (used.rotate ? ['span.item-rotate.fa.fa-refresh'] : []), + ['div.item-x', + 'X:', + String(used.x) + ], + ['div.item-y', + 'Y:', + String(used.y) + ] + ]) + ]; +} + + +function centerPanel() { + return ['div.center-panel', + ['svg', + + ] + ]; +} diff --git a/src_py/opcut/server.py b/src_py/opcut/server.py index c67279f..02fc161 100644 --- a/src_py/opcut/server.py +++ b/src_py/opcut/server.py @@ -57,7 +57,7 @@ async def _calculate_handler(executor, request): result_json_data = common.result_to_json_data(result) except asyncio.CancelledError: raise - except Exception: + except Exception as e: result_json_data = None return aiohttp.web.json_response({'result': result_json_data}) @@ -73,7 +73,7 @@ async def _generate_output_handler(executor, request): output_json_data = base64.b64encode(output).decode('utf-8') except asyncio.CancelledError: raise - except Exception: + except Exception as e: output_json_data = None return aiohttp.web.json_response({'data': output_json_data}) diff --git a/src_web/style/main.scss b/src_web/style/main.scss index 612c2a7..896df77 100644 --- a/src_web/style/main.scss +++ b/src_web/style/main.scss @@ -83,6 +83,14 @@ html, body { tbody td:last-child { text-align: center; } + + .grid-col-width, .grid-col-height { + text-align: right; + + input { + text-align: right; + } + } } & > *:not(:last-child) { @@ -104,9 +112,57 @@ html, body { .center-panel { flex-grow: 1; + overflow: auto; } .right-panel { + @include shadow-4dp(); + width: 200px; + display: flex; + flex-direction: column; + background-color: $color-grey-100; + overflow: auto; + .toolbar { + display: flex; + justify-content: center; + margin: 10px; + } + + .panel { + margin: 15px 0px; + + & > *:hover { + background-color: $color-grey-400; + cursor: pointer; + } + } + + .panel-name { + padding: 3px 10px; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 600; + color: $color-grey-800; + } + + .item { + padding: 3px 10px 3px 20px; + display: flex; + + .item-name { + flex-grow: 1; + } + + .item-rotate, .item-x, .item-y { + margin-left: 3px; + font-weight: 600; + color: $color-grey-800; + } + } + + .selected { + background-color: $color-grey-400; + } } -- cgit v1.2.3-70-g09d2