diff --git a/public/assets/app.css b/public/assets/app.css index d85ddea..2b2ba18 100644 --- a/public/assets/app.css +++ b/public/assets/app.css @@ -54,6 +54,7 @@ body, h1, h2, h3, h4, h5, h6, p, ol, ul { [hidden] { display: none !important; } +/* ---------------------------- Header ---------------------------- */ header { border-bottom: 1px solid black; background: var(--header-bg); @@ -78,10 +79,38 @@ header span { font-size: 1.2rem; } -main { - flex: 2 1 auto; +header .overlay { + background: var(--header-bg); + color: var(--foreground); + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + z-index: 10; } +header .overlay h2 { + font-size: 2rem; +} + +header .overlay .bar { + display: flex; + margin-top: 1rem; +} + +header .overlay .bar .led { + margin: 0 0.5rem; +} + +/* ---------------------------- Nav ---------------------------- */ + nav { display: flex; justify-content: center; @@ -133,6 +162,15 @@ nav::after { border-top: 1px solid var(--nav-above-border-color); } +/* ---------------------------- Main ---------------------------- */ + +main { + flex: 2 1 auto; +} + + +/* ---------------------------- Extra ---------------------------- */ + /* Taken from https://github.com/aus/led.css */ .led { margin-top: 1px; diff --git a/public/assets/app.js b/public/assets/app.js index 4c05398..1baae9a 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -1,10 +1,37 @@ +class Animator { + constructor() { + this.running = null + this.runningInterval = null + } + + begin(time) { + if (this.runningInterval === time && this.running) return + if (this.running) { + clearInterval(this.running) + } + this.runningInterval = time + this.running = setInterval(function() { + m.redraw() + }, time) + } + + stop() { + clearInterval(this.running) + this.running = null + } +} + +const animator = new Animator() + class WSClient { constructor(url = 'ws://localhost:4040') { this.url = url this.socket = null this.connected = false - this.connecting = false + this.connecting = null + this.connectingAt = null + this.reconnecting = null this.reconnectingAt = null this.retryDuration = 1000 this.open() @@ -12,22 +39,31 @@ class WSClient open() { if (this.connected || this.connecting) return - this.connecting = true + clearTimeout(this.reconnecting) + this.reconnecting = this.reconnectingAt = null + this.connectingAt = new Date() this.socket = new WebSocket(this.url) this.socket.addEventListener('open', this.onopen.bind(this)) this.socket.addEventListener('message', this.onmessage.bind(this)) this.socket.addEventListener('error', this.onerror.bind(this)) this.socket.addEventListener('close', this.onclose.bind(this)) + + animator.begin(1000) + this.connecting = setTimeout(() => { + this.socket.close() + }, 5000) m.redraw() } onopen() { + animator.stop() this.retryDuration = 1000 - this.connecting = false + clearTimeout(this.connecting) + this.connecting = null + this.connectingAt = null this.reconnectingAt = null this.connected = true - console.log('Sending hello server') this.send({ msg: 'Hello Server!' }) m.redraw() } @@ -37,8 +73,7 @@ class WSClient this.socket.send(JSON.stringify(payload)) } - onerror(err) { - console.error(err) + onerror() { this.reconnect() } @@ -48,14 +83,17 @@ class WSClient reconnect() { if (this.reconnectingAt) return + animator.begin(100) this.socket = null this.connected = false - this.connecting = false + clearTimeout(this.connecting) + this.connecting = null + this.connectingAt = null this.reconnectingAt = new Date(new Date().getTime() + this.retryDuration) - setTimeout(() => { - this.reconnectingAt = null - this.retryDuration *= 1.5 + clearTimeout(this.reconnecting) + this.reconnecting = setTimeout(() => { + this.retryDuration = Math.min(this.retryDuration * 1.5, 60000) this.open() }, this.retryDuration) m.redraw() @@ -73,20 +111,93 @@ const client = new WSClient() class Header { view(vnode) { + console.log(client.connecting) return [ m('h1', 'Fíladelfíu streymi'), m('div.status.green', { hidden: true }, 'No errors'), m('div.filler'), - m('span', 'Live:'), - m('div.led'), client.connected ? null : m('div.overlay', [ - m('h2', client.connecting ? 'Connecting to server...' : 'Connection lost'), - m('') - ]), + m('h2', client.connecting ? 'Connecting to server...' : 'Failed to connect, retrying in...'), + client.connecting + ? this.viewConnectingBar() + : this.viewWaitingCircle(), + ]), + m('span', 'Live:'), + m('div.led'), ] } + viewConnectingBar() { + let diff = Math.round((new Date() - client.connectingAt) / 1000) + let out = [] + for (let i = 0; i < 5; i++) { + out.push(m('div.led', { class: diff >= i ? 'led-blue' : '' })) + } + return m('div.bar', out) + } + + viewWaitingCircle() { + var diff = Math.min((new Date() - client.reconnectingAt + 200), client.retryDuration) + var percentage = Math.abs(Math.min(diff / client.retryDuration, 0)) + return m('svg', { + onclick: function() { client.open() }, + style: 'width: 150px; transform: rotate(-90deg);', + viewBox: '0 0 150 150', + preserveAspectRatio: 'xMidYMid meet', + }, [ + m('filter', { id: 'darker' }, [ + m('feFlood', { 'flood-color': '#000000', 'flood-opacity': '1', in: 'SourceGraphic' }), + m('feComposite', { operator: 'in', in2: 'SourceGraphic' }), + m('feGaussianBlur', { stdDeviation: '2' }), // approx size, with higher values meaning less intensity + m('feComponentTransfer', { result: 'glowd' }, m('feFuncA', { type: 'linear', slope: 1, intercept: 0 })), + + m('feMerge', [ + m('feMergeNode', { in: 'SourceGraphic' }), + m('feMergeNode', { in: 'glowd' }), + ]), + ]), + m('filter', { id: 'glower' }, [ + m('feFlood', { 'flood-color': '#0066ff', 'flood-opacity': '1', in: 'SourceGraphic' }), + m('feComposite', { operator: 'in', in2: 'SourceGraphic' }), + m('feGaussianBlur', { stdDeviation: '10' }), // approx size, with higher values meaning less intensity + m('feComponentTransfer', { result: 'glow1' }, m('feFuncA', { type: 'linear', slope: 1, intercept: 0 })), + + m('feFlood', { 'flood-color': '#0066ff', 'flood-opacity': '0.5', in: 'SourceGraphic' }), + m('feComposite', { operator: 'in', in2: 'SourceGraphic' }), + m('feGaussianBlur', { stdDeviation: '1' }), // approx size, with higher values meaning less intensity + m('feComponentTransfer', { result: 'glow2' }, m('feFuncA', { type: 'linear', slope: 2, intercept: 0 })), + + m('feMerge', [ + m('feMergeNode', { in: 'SourceGraphic' }), + m('feMergeNode', { in: 'glow2' }), + m('feMergeNode', { in: 'glow1' }), + ]), + ]), + m('g', { width: '150px', height: '150px', filter: 'url(#darker)',}, [ + m('rect', { width: '100%', height: '100%', fill: 'transparent' }), + m('circle', { + cx: '50%', + cy: '50%', + fill: 'transparent', + stroke: '#000000', + style: `stroke-width: 12%; transform-origin: 50% 50% 0px; transition: stroke-dashoffset 100ms;`, + r: 45, + }), + ]), + m('g', { width: '150px', height: '150px', filter: 'url(#glower)',}, [ + m('rect', { width: '100%', height: '100%', fill: 'transparent' }), + m('circle', { + cx: '50%', + cy: '50%', + fill: 'transparent', + stroke: '#000000', + style: `stroke-dashoffset: ${Math.round(280 * percentage)}px; stroke-dasharray: 280px; stroke-width: 10%; transform-origin: 50% 50% 0px; transition: stroke-dashoffset 100ms;`, + r: 45, + }) + ]), + ]) + } } class Nav { @@ -94,7 +205,7 @@ class Nav { let path = m.route.get() return [ m(m.route.Link, { - class: path === '/' ? 'active' : '', + class: path === '/' || path === '' ? 'active' : '', href: '/', }, [ m('svg', { diff --git a/server/server.mjs b/server/server.mjs index df4cbf8..37381fd 100644 --- a/server/server.mjs +++ b/server/server.mjs @@ -42,7 +42,9 @@ export function run(http, port, core) { flaska.get('/::file', serve.serve.bind(serve)) return flaska.listenAsync(port).then(function() { - /*const wss = new WebSocketServer({ server: flaska.server }) + core.log.info('Server is listening on port ' + port) + + const wss = new WebSocketServer({ server: flaska.server }) wss.on('connection', function(ws) { console.log('new connection') @@ -88,8 +90,5 @@ export function run(http, port, core) { wss.on('close', function() { clearInterval(interval) }) - */ - - core.log.info('Server is listening on port ' + port) }) } \ No newline at end of file