Compare commits

...

10 commits

17 changed files with 460 additions and 381 deletions

View file

@ -1,5 +1,12 @@
## [Unreleased]
## v2.0.0
### Added
* add an option to start the web server with TLS (HTTPS)
### Changed
* remove jquery
* upgrade Bootstrap
## v1.0.2
### Fixed
* close connection on ws error

View file

@ -5,6 +5,12 @@ import (
"os"
)
type TlsConfig struct {
Enable bool `yaml:"enable"`
CertFile string `yaml:"certificate"`
CertKeyFile string `yaml:"certificate_key"`
}
type ServerAuthConfig struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
@ -13,6 +19,7 @@ type ServerAuthConfig struct {
type ServerConfig struct {
Listen string `yaml:"listen"`
Auth ServerAuthConfig `yaml:"auth"`
Tls TlsConfig `yaml:tls`
}
type RemoteItemConfigItem struct {

View file

@ -3,6 +3,10 @@ server:
username: admin
password: admin
listen: 0.0.0.0:4000
tls:
enable: false
certificate: /path/to/server.crt
certificate_key: /path/to/server.key
remote:
- label: Keyboard

10
main.go
View file

@ -62,5 +62,13 @@ func main() {
e.GET("/", homeController)
e.GET("/ws", wsController)
e.Logger.Fatal(e.Start(config.Server.Listen))
if config.Server.Tls.Enable == false {
e.Logger.Fatal(e.Start(config.Server.Listen))
} else {
e.Logger.Fatal(e.StartTLS(
config.Server.Listen,
config.Server.Tls.CertFile,
config.Server.Tls.CertKeyFile,
))
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,10 @@
:root {
--link-color: #1e3650;
--bs-link-color: var(--link-color);
}
a {
color: #1e3650;
color: var(--link-color);
}
.btn-primary {

BIN
static/img/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long

316
static/js/main.js Normal file
View file

@ -0,0 +1,316 @@
let ws
let pointer, scroller, response, screenshotImg
let scrollLastTimestamp, scrollLastValue
let mousePosX, mousePosY, mouseInitPosX, mouseInitPosY
let isLive = false
let isScreenshotWaiting = false
function createWebSocketConnection() {
const protocol = location.protocol === 'https:' ? 'wss' : 'ws'
ws = new WebSocket(`${protocol}://${window.location.hostname}:${window.location.port}/ws`)
ws.onopen = function(event) {
document.querySelector('#disconneced').style.display = 'none'
}
ws.onclose = function(event) {
document.querySelector('#disconneced').style.display = 'block'
window.setTimeout(createWebSocketConnection, 5000)
}
ws.onmessage = function(event) {
let data = JSON.parse(event.data)
if (data.type === 'response') {
response.innerText = data.value
response.style.display = 'block'
window.setTimeout(function() {
response.style.display = 'none'
}, 2500)
} else if (data.type === 'screenshot') {
isScreenshotWaiting = false
screenshotImg.setAttribute('src', 'data:image/pngbase64, ' + data.value)
}
}
}
function navigationClickHandler(e) {
if (e.target.getAttribute('href') === '#') {
return
}
Array.from(document.querySelectorAll('.pane')).forEach((item) => {
item.style.display = 'none'
})
document.querySelector(e.target.getAttribute('href')).style.display = 'block'
Array.from(document.querySelectorAll('#nav a')).forEach((item) => {
item.classList.remove('active')
})
e.target.classList.add('active')
}
function buttonClickHandler(e) {
ws.send(e.target.getAttribute('data-msg'))
}
function shortcutClearClickHandler(e) {
document.querySelector('#shortcut-key').value = ''
Array.from(document.querySelectorAll('#shortcuts_special_keys input:checked')).forEach((item) => {
console.log(item.checked)
item.checked = false
console.log(item.checked)
item.change()
})
}
function shortcutSendClickHandler(e) {
let keys = []
let key = document.querySelector('#shortcut-key').value
Array.from(document.querySelectorAll('#shortcuts_special_keys input:checked')).forEach((item) => {
keys.push(item.value)
})
if (keys.length) {
if (key) {
keys.push(key)
}
ws.send('{"type":"keys","value": "' + (keys.join(',').replace('"', '\\"')) + '"}')
}
}
function textClearClickHandler(e) {
document.querySelector('#text').value = ''
}
function textSendClickHandler(e) {
const keys = document.querySelector('#text').value
if (keys.length) {
ws.send('{"type":"text","value": "' + (keys.replace('"', '\\"')) + '"}')
}
}
function textKeyUpHandler(e) {
const keys = document.querySelector('#text').value
if (e.keyCode === 13) {
ws.send('{"type":"text","value": "' + (keys.replace('"', '\\"')) + '"}')
}
}
function liveTextKeyUpHandler(e) {
const value = e.target.value
if (e.keyCode === 8) {
ws.send('{"type":"key","value": "backspace"}')
} else if (e.keyCode === 13) {
ws.send('{"type":"key","value": "enter"}')
} else if (value.length) {
if (value === ' ') {
ws.send('{"type":"key","value": "space"}')
} else {
ws.send('{"type":"text","value": "' + (value.replace('"', '\\"')) + '"}')
}
e.target.value = ''
}
}
function shortcutsSpecialKeysOnChangeHandler(e) {
Array.from(document.querySelectorAll('#shortcuts_special_keys input:checked')).forEach((item) => {
item.parentNode.classList.add('btn-primary')
item.parentNode.classList.remove('btn-secondary')
})
Array.from(document.querySelectorAll('#shortcuts_special_keys input:not(:checked)')).forEach((item) => {
item.parentNode.classList.add('btn-secondary')
item.parentNode.classList.remove('btn-primary')
})
}
function pointerClickHandler(e) {
ws.send('{"type":"pointer","click":"left"}')
}
function scrollerTouchStartHandler(e) {
mouseInitPosY = e.targetTouches[0].pageY
}
function scrollerTouchMoveHandler(e) {
let touch = e.changedTouches[0]
let value = ((touch.pageY - mouseInitPosY > 0) ? 'down' : 'up')
let now = new Date().getTime()
if (touch.pageY === mouseInitPosY || value === scrollLastValue && scrollLastTimestamp !== null && now - scrollLastTimestamp < 200) {
return
}
scrollLastTimestamp = now
scrollLastValue = value
mouseInitPosY = touch.pageY
ws.send('{"type":"scroll","value": "' + value + '"}')
}
function pointerTouchStartHandler(e) {
const touch = e.targetTouches[0]
mouseInitPosX = touch.pageX
mouseInitPosY = touch.pageY
}
function pointerTouchMoveHandler(e) {
if (e.changedTouches.length === 2) {
return scrollerTouchMoveHandler(e)
}
const touch = e.changedTouches[0]
mousePosX = touch.pageX
mousePosY = touch.pageY
const newX = mousePosX - mouseInitPosX
const newY = mousePosY - mouseInitPosY
mouseInitPosX = mousePosX
mouseInitPosY = mousePosY
let msg = '{"type":"pointer","x": "' + newX + '","y": "' + newY + '"}'
ws.send(msg)
}
function liveHqClickHandler(e) {
return liveClickHandler(e, 'hq')
}
function liveLqClickHandler(e) {
return liveClickHandler(e, 'lq')
}
function liveClickHandler(e, quality) {
if (isLive) {
isLive = false
isScreenshotWaiting = false
document.querySelector('#live-hq').innerText(`Live HQ`)
document.querySelector('#live-lq').innerText(`Live LQ`)
return
}
isLive = true
e.target.innerText('Stop live')
let doScreenshot = function() {
if (isLive) {
if (!isScreenshotWaiting) {
isScreenshotWaiting = true
ws.send(`{"type":"screenshot","quality":"${quality}"}`)
}
window.setTimeout(doScreenshot, 100)
}
}
doScreenshot()
}
function fullscreenHandler(e) {
let element = document.querySelector(e.target.getAttribute('data-target'))
let isFullscreen = parseInt(e.target.getAttribute('data-fullscreen'))
document.querySelector('body').classList.toggle('fullscreen', isFullscreen)
if (isFullscreen) {
e.target.setAttribute('data-fullscreen', '0')
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen()
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
}
} else {
e.target.setAttribute('data-fullscreen', '1')
if (element.requestFullscreen) {
element.requestFullscreen()
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen()
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen()
}
}
}
function documentHashHandler() {
const hash = window.location.hash
if (hash) {
document.querySelector('a[href="' + hash + '"]').click()
} else {
document.querySelector('#nav > li:first-child a').click()
}
}
function addEventListenerOn(selector, eventName, listener) {
if (typeof selector === 'string') {
Array.from(document.querySelectorAll(selector)).forEach((element) => {
element.addEventListener(eventName, listener)
})
} else {
selector.addEventListener(eventName, listener)
}
}
function addListeners() {
addEventListenerOn('#nav a', 'click', navigationClickHandler)
addEventListenerOn('button[data-msg]', 'click', buttonClickHandler)
addEventListenerOn('#shortcut-clear', 'click', shortcutClearClickHandler)
addEventListenerOn('#shortcuts_special_keys input', 'change', shortcutsSpecialKeysOnChangeHandler)
addEventListenerOn('#shortcut-send', 'click', shortcutSendClickHandler)
addEventListenerOn('#text-clear', 'click', textClearClickHandler)
addEventListenerOn('#text-send', 'click', textSendClickHandler)
addEventListenerOn('#text', 'keyup', textKeyUpHandler)
addEventListenerOn('.live-text', 'keyup', liveTextKeyUpHandler)
addEventListenerOn(scroller, 'touchstart', scrollerTouchStartHandler)
addEventListenerOn(scroller, 'touchmove', scrollerTouchMoveHandler)
addEventListenerOn(pointer, 'click', pointerClickHandler)
addEventListenerOn(pointer, 'touchstart', pointerTouchStartHandler)
addEventListenerOn(pointer, 'touchmove', pointerTouchMoveHandler)
addEventListenerOn('#live-hq', 'click', liveHqClickHandler)
addEventListenerOn('#live-lq', 'click', liveLqClickHandler)
addEventListenerOn('.btn-fullscreen', 'click', fullscreenHandler)
}
function bootstrap() {
pointer = document.querySelector('#pointer')
scroller = document.querySelector('#scrollbar')
response = document.querySelector('#response')
screenshotImg = document.querySelector('#screenshot img')
shortcutsSpecialKeysOnChangeHandler()
createWebSocketConnection()
addListeners()
documentHashHandler()
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/static/js/service_worker.js')
}
}
addEventListenerOn(window, 'DOMContentLoaded', bootstrap)

View file

@ -0,0 +1,3 @@
self.addEventListener("install", (e) => {
console.log("[Service Worker] Install");
});

View file

@ -1,308 +0,0 @@
var ws;
var $pointer, $scroller, $response, $screenshotImg;
var scrollLastTimestamp, scrollLastValue;
var mousePosX, mousePosY, mouseInitPosX, mouseInitPosY;
var isLive = false;
var isScreenshotWaiting = false;
var createWebSocketConnection = function() {
ws = new WebSocket(`ws://${window.location.hostname}:${window.location.port}/ws`);
ws.onopen = function(event) {
$('#disconneced').fadeOut();
}
ws.onclose = function(event) {
$('#disconneced').fadeIn();
window.setTimeout(createWebSocketConnection, 5000);
}
ws.onmessage = function(event) {
var data = JSON.parse(event.data);
if (data.type === 'response') {
$response.text(data.value);
$response.fadeIn();
window.setTimeout(function() {
$response.fadeOut();
}, 2500);
} else if (data.type === 'screenshot') {
isScreenshotWaiting = false
$screenshotImg.attr('src', 'data:image/png;base64, ' + data.value);
}
}
}
var navigationClickHandler = function(e) {
if ($(this).attr('href') === '#') {
return
}
$('.pane').hide();
var target = $(this).attr('href');
$(target).show();
$('#nav a').removeClass('active');
$(this).addClass('active');
}
var buttonClickHandler = function(e) {
var msg = $(this).attr('data-msg');
ws.send(msg);
}
var shortcutClearClickHandler = function(e) {
$('#shortcut-key').val('');
$('#shortcuts_special_keys input:checked').each(function() {
$(this).prop('checked', false).trigger('change');
});
}
var shortcutSendClickHandler = function(e) {
var keys = [];
$('#shortcuts_special_keys input:checked').each(function() {
keys.push($(this).val());
});
var key = $('#shortcut-key').val();
if (keys.length) {
if (key) {
keys.push(key);
}
var msg = '{"type":"keys","value": "' + (keys.join(',').replace('"', '\\"')) + '"}';
ws.send(msg);
}
}
var textClearClickHandler = function(e) {
$('#text').val('');
}
var textSendClickHandler = function(e) {
var keys = $('#text').val();
if (keys.length) {
var msg = '{"type":"text","value": "' + (keys.replace('"', '\\"')) + '"}';
ws.send(msg);
}
}
var textKeyUpHandler = function(e) {
var keys = $('#text').val();
if (e.keyCode === 13) {
var msg = '{"type":"text","value": "' + (keys.replace('"', '\\"')) + '"}';
ws.send(msg);
}
}
var liveTextKeyUpHandler = function(e) {
var value = $(this).val();
var live = false;
if (e.keyCode === 8) {
var msg = '{"type":"key","value": "backspace"}';
ws.send(msg);
} else if (e.keyCode === 13) {
var msg = '{"type":"key","value": "enter"}';
ws.send(msg);
} else if (value.length) {
if (value === ' ') {
var msg = '{"type":"key","value": "space"}';
ws.send(msg);
} else {
var msg = '{"type":"text","value": "' + (value.replace('"', '\\"')) + '"}';
ws.send(msg);
}
$(this).val('');
}
}
var shortcutsSpecialKeysOnChangeHandler = function(e) {
$('#shortcuts_special_keys input:checked').each(function() {
$(this).parent().addClass('btn-primary').removeClass('btn-secondary');
})
$('#shortcuts_special_keys input:not(:checked)').each(function() {
$(this).parent().addClass('btn-secondary').removeClass('btn-primary');
})
}
var pointerClickHandler = function(e) {
var msg = '{"type":"pointer","click":"left"}';
ws.send(msg);
}
var scrollerTouchStartHandler = function(e) {
var touch = e.targetTouches[0];
mouseInitPosY = touch.pageY;
}
var scrollerTouchMoveHandler = function(e) {
var touch = e.changedTouches[0];
var value = ((touch.pageY - mouseInitPosY > 0) ? 'down' : 'up');
var now = new Date().getTime();
if (touch.pageY === mouseInitPosY || value === scrollLastValue && scrollLastTimestamp !== null && now - scrollLastTimestamp < 200) {
return;
}
scrollLastTimestamp = now;
scrollLastValue = value;
var msg = '{"type":"scroll","value": "' + value + '"}';
mouseInitPosY = touch.pageY;
ws.send(msg);
}
var pointerTouchStartHandler = function(e) {
var touch = e.targetTouches[0];
mouseInitPosX = touch.pageX;
mouseInitPosY = touch.pageY;
}
var pointerTouchMoveHandler = function(e) {
if (e.changedTouches.length === 2) {
return scrollerTouchMoveHandler(e);
}
var touch = e.changedTouches[0];
mousePosX = touch.pageX;
mousePosY = touch.pageY;
var newX = mousePosX - mouseInitPosX;
var newY = mousePosY - mouseInitPosY;
mouseInitPosX = mousePosX;
mouseInitPosY = mousePosY;
var msg = '{"type":"pointer","x": "' + newX + '","y": "' + newY + '"}';
ws.send(msg);
}
var liveHqClickHandler = function(e) {
return liveClickHandler(e, 'hq')
}
var liveLqClickHandler = function(e) {
return liveClickHandler(e, 'lq')
}
var liveClickHandler = function(e, quality) {
if (isLive) {
isLive = false;
isScreenshotWaiting = false;
$('#live-hq').text(`Live HQ`);
$('#live-lq').text(`Live LQ`);
return;
}
isLive = true;
$(e.target).text('Stop live');
var doScreenshot = function() {
if (isLive) {
if (!isScreenshotWaiting) {
isScreenshotWaiting = true
ws.send(`{"type":"screenshot","quality":"${quality}"}`);
}
window.setTimeout(doScreenshot, 100);
}
}
doScreenshot();
}
var fullscreenHandler = function(e) {
var element = $(e.target.getAttribute('data-target'));
var isFullscreen = parseInt($(e.target).attr('data-fullscreen'));
$('body').toggleClass('fullscreen', isFullscreen)
if (isFullscreen) {
element.attr('data-fullscreen', '0');
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
}
} else {
$(e.target).attr('data-fullscreen', '1');
if (element.get(0).requestFullscreen) {
element.get(0).requestFullscreen();
} else if (element.get(0).webkitRequestFullscreen) {
element.get(0).webkitRequestFullscreen();
} else if (element.get(0).mozRequestFullScreen) {
element.get(0).mozRequestFullScreen();
}
}
}
var documentHashHandler = function() {
var hash = window.location.hash;
if (hash) {
$('a[href="' + hash + '"]').click();
} else {
$('#nav > li:first-child a').click();
}
}
var addListeners = function() {
$('#nav a').click(navigationClickHandler);
$('button[data-msg]').click(buttonClickHandler);
$('#shortcut-clear').click(shortcutClearClickHandler);
$('#shortcuts_special_keys input').change(shortcutsSpecialKeysOnChangeHandler);
$('#shortcut-send').click(shortcutSendClickHandler);
$('#text-clear').click(textClearClickHandler);
$('#text-send').click(textSendClickHandler);
$('#text').on('keyup', textKeyUpHandler);
$('.live-text').on('keyup', liveTextKeyUpHandler);
$scroller
.on('touchstart', scrollerTouchStartHandler)
.on('touchmove', scrollerTouchMoveHandler);
$pointer
.on('click', pointerClickHandler)
.on('touchstart', pointerTouchStartHandler)
.on('touchmove', pointerTouchMoveHandler);
$('#live-hq').click(liveHqClickHandler);
$('#live-lq').click(liveLqClickHandler);
$('.btn-fullscreen').click(fullscreenHandler)
}
var bootstrap = function() {
shortcutsSpecialKeysOnChangeHandler();
createWebSocketConnection();
addListeners();
documentHashHandler();
}
$(function() {
$pointer = $('#pointer');
$scroller = $('#scrollbar');
$response = $('#response');
$screenshotImg = $('#screenshot img');
bootstrap();
});

17
static/manifest.json Normal file
View file

@ -0,0 +1,17 @@
{
"short_name": "RWM",
"name": "Remote i3-wm",
"theme_color": "#1e3650",
"background_color": "#ffffff",
"display": "fullscreen",
"Scope": "/",
"orientation": "portrait",
"icons": [
{
"src": "/static/img/icon.png",
"type": "image/png",
"sizes": "96x96"
}
],
"start_url": "/"
}

View file

@ -4,8 +4,10 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<link rel="stylesheet" href="/static/bootstrap.min.css" type="text/css">
<link rel="stylesheet" href="/static/main.css" type="text/css">
<link rel="stylesheet" href="/static/css/bootstrap.min.css" type="text/css">
<link rel="stylesheet" href="/static/css/main.css" type="text/css">
<link rel="manifest" href="/static/manifest.json">
<link rel="icon" type="image/png" href="/static/img/icon.png">
<title>Remote i3-wm</title>
</head>
<body>
@ -17,10 +19,7 @@
<div id="response"></div>
<script src="/static/jquery.min.js"></script>
<script src="/static/bootstrap.bundle.min.js"></script>
<script src="/static/bootstrap.min.js"></script>
<script src="/static/main.js"></script>
<script src="/static/js/main.js"></script>
</body>
</html>
{{end}}