diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92b8a41 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +web/package-lock.json +web/node_modules +**/*.swp +**/*.swo +gen/ +go.sum diff --git a/web/.eslintrc.json b/web/.eslintrc.json new file mode 100644 index 0000000..67b9be6 --- /dev/null +++ b/web/.eslintrc.json @@ -0,0 +1,15 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "standard" + ], + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "rules": { + } +} diff --git a/web/.stylelintrc.json b/web/.stylelintrc.json new file mode 100644 index 0000000..a446433 --- /dev/null +++ b/web/.stylelintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "stylelint-config-standard" +} diff --git a/web/OliveTinLogo.png b/web/OliveTinLogo.png new file mode 100644 index 0000000..64efd1e Binary files /dev/null and b/web/OliveTinLogo.png differ diff --git a/web/OliveTinLogo.svg b/web/OliveTinLogo.svg new file mode 100644 index 0000000..052bc52 --- /dev/null +++ b/web/OliveTinLogo.svg @@ -0,0 +1,106 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/web/index.htm b/web/index.htm new file mode 100644 index 0000000..eefa134 --- /dev/null +++ b/web/index.htm @@ -0,0 +1,28 @@ + + + + + + + + OliveTin + + + + + +
+
+
+ + + + + + + diff --git a/web/js/classes.js b/web/js/classes.js new file mode 100644 index 0000000..5a1a2ae --- /dev/null +++ b/web/js/classes.js @@ -0,0 +1,92 @@ +class ActionButton extends window.HTMLButtonElement { + constructFromJson (json) { + this.title = json.title + this.states = [] + this.stateLabels = [] + this.currentState = 0 + this.isWaiting = false + this.actionCallUrl = 'http://mindstorm4:1339/StartAction?actionName=' + this.title + + if (json.icon !== undefined) { + this.unicodeIcon = unescape(json.icon) + } else { + this.unicodeIcon = '💩' + } + + this.onclick = () => { this.startAction() } + + this.constructTemplate() + this.updateHtml() + } + + startAction () { + this.disabled = true + this.isWaiting = true + this.updateHtml() + this.classList = [] // Removes old animation classes + + window.fetch(this.actionCallUrl).then(res => { + if (!res.ok) { + return res.json() + } + }).then(json => { + this.onActionResult() + }).catch(err => { + this.onActionError(err) + }) + } + + onActionResult (json) { + this.disabled = false + this.isWaiting = false + this.updateHtml() + this.classList.add('actionSuccess') + } + + onActionError (err) { + console.log('callback error', err) + this.disabled = false + this.isWaiting = false + this.updateHtml() + this.classList.add('actionFailed') + } + + constructTemplate () { + const tpl = document.getElementById('tplActionButton') + const content = tpl.content.cloneNode(true) + + /* + * FIXME: Should probably be using a shadowdom here, but seem to + * get an error when combined with custom elements. + */ + + this.appendChild(content) + + this.domTitle = this.querySelector('.title') + this.domIcon = this.querySelector('.icon') + } + + updateHtml () { + if (this.isWaiting) { + this.domTitle.innerText = 'Waiting...' + } else { + this.domTitle.innerText = this.title + } + + this.domIcon.innerHTML = this.unicodeIcon + } + + getCurrentStateLabel (useLabels = true) { + if (useLabels) { + return this.stateLabels[this.currentState] + } else { + return this.states[this.currentState] + } + } + + getNextStateLabel () { + return this.stateLabels[this.currentState + 1] + } +} + +window.customElements.define('action-button', ActionButton, { extends: 'button' }) diff --git a/web/js/loader.js b/web/js/loader.js new file mode 100644 index 0000000..bc5cf8d --- /dev/null +++ b/web/js/loader.js @@ -0,0 +1,10 @@ +import './classes.js' // To define action-button + +export function loadContents (json) { + for (const jsonButton of json.actions) { + const a = document.createElement('button', { is: 'action-button' }) + a.constructFromJson(jsonButton) + + document.getElementById('rootGroup').appendChild(a) + } +} diff --git a/web/main.js b/web/main.js new file mode 100644 index 0000000..b26b686 --- /dev/null +++ b/web/main.js @@ -0,0 +1,9 @@ +'use strict' + +import { loadContents } from './js/loader.js' + +window.fetch('http://mindstorm4:1339/GetButtons').then(res => { + return res.json() +}).then(res => { + loadContents(res) +}) diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..8398a0c --- /dev/null +++ b/web/package.json @@ -0,0 +1,22 @@ +{ + "name": "olivetin", + "version": "1.0.0", + "description": "", + "main": "main.js", + "dependencies": { + "stylelint": "^13.13.0", + "stylelint-config-standard": "^22.0.0" + }, + "devDependencies": { + "eslint": "^7.25.0", + "eslint-config-standard": "^16.0.2", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^4.3.1" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..1e78793 --- /dev/null +++ b/web/style.css @@ -0,0 +1,91 @@ +body { + background-color: #333; + color: white; + text-align: center; + font-family: sans-serif; + padding: 0; + margin: 0; +} + +.group { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + grid-template-rows: auto auto auto auto; + padding: 1em; + grid-gap: 1em; + text-align: center; +} + +div.entity { + background-color: #222; + box-shadow: 0 0 10px 0 #444; + display: grid; + grid-column: auto / span 2; + grid-row: auto / span 2; + grid-template-rows: auto min-content; + grid-template-columns: minmax(min-content, auto); +} + +div.entity h2 { + grid-column: 1 / span all; +} + +button { + padding: 1em; + color: white; + display: table-cell; + text-align: center; + border: 1px solid #666; + background-color: #222; + box-shadow: 0 0 10px 0 #444; + user-select: none; +} + +button.good { + background-color: limegreen; +} + +button:hover { + box-shadow: 0 0 10px 0 #666; + cursor: pointer; +} + +button:disabled { + color: gray; + background-color: black; + cursor: not-allowed; +} + +span.icon { + font-size: 3em; +} + +.actionFailed { + animation: kfActionFailed 1s; +} + +@keyframes kfActionFailed { + 0% { background-color: black; } + 20% { background-color: red; } + 0% { background-color: inherit; } +} + +.actionSuccess { + animation: kfActionSuccess 1s; +} + +@keyframes kfActionSuccess { + 0% { background-color: black; } + 20% { background-color: green; } + 0% { background-color: inherit; } +} + +footer, footer a { + color: gray; +} + +img.logo { + width: 1em; + height: 1em; + vertical-align: middle; +}