Compare commits

...
Sign in to create a new pull request.

32 commits

Author SHA1 Message Date
0e0867aa30
feat: handle smartphone keyboard composition 2025-12-15 09:21:12 +01:00
af16752cf8
doc: update changelog 2025-12-11 21:53:25 +01:00
8cb19f2555
feat: center live capture in pointer 2025-12-11 21:53:04 +01:00
b415b00e5b
build: fix static embedding 2025-12-11 21:52:48 +01:00
7ed8364a13
build: fix static embedding 2025-12-11 21:52:37 +01:00
eaa1c9397f
doc: update changelog 2025-12-11 21:30:10 +01:00
b9bea6bdd2
refactor: change organisation 2025-12-11 21:28:27 +01:00
408c9c6d89
feat: use MJPEG for live capture 2025-12-11 20:55:59 +01:00
dd16a82057
update changelog 2025-04-27 21:10:51 +02:00
946d47b4ac
update rice box 2025-04-27 21:09:44 +02:00
30d71da2bd
add input type range for the volume 2025-04-27 17:34:36 +02:00
653ab1d9cd update ui 2025-02-23 13:32:55 +01:00
7cf77935b3
update go version for build 2024-04-05 10:09:30 +02:00
451717ddac
update changelog 2023-12-10 16:27:40 +01:00
ae53fe1ab7
add live on pointer block
allow to show pointer
2023-12-10 16:26:53 +01:00
5aab502a76
update changelog 2023-12-06 17:56:53 +01:00
e3cbf3eda2
change manifest url 2023-12-06 17:54:32 +01:00
ff5a66ad07
fix screenshots 2023-12-06 17:53:48 +01:00
a07f14765a
remove padding en pointer buttons 2023-12-06 17:53:16 +01:00
233d1a4d96
move manifest in controller 2023-11-20 14:40:10 +01:00
a9601c479d
update changelog 2023-11-17 20:20:32 +01:00
e65fccee03 Merge branch 'feature/refactoring' into develop 2023-11-17 20:17:30 +01:00
73b9f98297
fix nav link colors 2023-11-17 20:17:26 +01:00
fc8e74c772
upgrade boostrap from v4 to v5 2023-11-17 20:13:32 +01:00
253073f15f
fix js 2023-11-17 20:10:38 +01:00
c865080df1
move js and css to specific directories 2023-11-17 20:05:50 +01:00
5f18c04d4d
remove jquery 2023-11-17 20:03:42 +01:00
e6934dd9aa Merge branch 'feature/pwa' into develop 2023-11-17 19:02:58 +01:00
71ce3bb90d
add tls option 2023-11-17 18:50:03 +01:00
3533ea4dfe
add minimal info for PWA 2023-11-17 18:25:26 +01:00
Simon Vieille
dd24fda35b
update changelog 2023-09-10 19:37:02 +02:00
Simon Vieille
5e76b76c31
close connection on ws error 2023-09-10 19:35:43 +02:00
32 changed files with 1091 additions and 754 deletions

View file

@ -1,11 +1,41 @@
## [Unreleased]
## v1.0.1
## v3.0.0
### Fixed
* feat: center live capture in pointer
### Changed
* refactor: change organisation
* feat: use MJPEG for live capture
- fix process overload: replace golang.org/x/net/websocket with github.com/gorilla/websocket
## v2.1.0
### Added
* add live on pointer block
* allow to show pointer
* use input type range as volume picker
## v2.0.1
### Fixed
* remove padding on pointer buttons
* fix live and screenshot render
* fix route of manifest
## 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
## v1.0.1
### Fixed
* fix process overload: replace golang.org/x/net/websocket with github.com/gorilla/websocket
## v1.0.0
- rewrite of https://gitnet.fr/deblan/remote-i3wm-ws
- add configuration file
- add authentication
### Added
* rewrite of https://gitnet.fr/deblan/remote-i3wm-ws
* add configuration file
* add authentication

View file

@ -12,11 +12,12 @@ all: build
deps:
go install github.com/GeertJohan/go.rice/rice@latest
rice embed-go
cd cmd && rice embed-go
.PHONY:
build: deps
CGO_ENABLED=$(CGO_ENABLED) GOARCH=amd64 GOOS=linux $(CC) $(CFLAGS) -o $(BUILD_DIR)/$(LINUX_BIN) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)"
CGO_ENABLED=$(CGO_ENABLED) GOARCH=amd64 GOOS=linux $(CC) $(CFLAGS) -o $(BUILD_DIR)/$(LINUX_BIN) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" -tags=static_build ./cmd/
watch:
gowatch -o build/app-live-linux-amd64 -args='./config.yaml'
test -f cmd/rice-box.go && rm cmd/rice-box.go || true
gowatch -o build/app-live-linux-amd64 -args='./config.yaml' -p ./cmd/main.go

View file

@ -2,24 +2,15 @@ package main
import (
"crypto/subtle"
"embed"
"fmt"
"net/http"
"os"
rice "github.com/GeertJohan/go.rice"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"html/template"
"net/http"
"os"
)
var (
templates map[string]*template.Template
//go:embed static
staticFiles embed.FS
//go:embed views/layout views/page
views embed.FS
config Config
actions Actions
"gitnet.fr/deblan/remote-i3wm-go/internal/config"
"gitnet.fr/deblan/remote-i3wm-go/internal/handler"
)
func main() {
@ -31,7 +22,7 @@ func main() {
os.Exit(1)
}
value, err := createConfigFromFile(os.Args[1])
value, err := config.CreateConfigFromFile(os.Args[1])
if err != nil {
fmt.Printf("Configuration error:")
@ -39,14 +30,15 @@ func main() {
os.Exit(1)
}
config = value
conf := value
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
if config.Server.Auth.Username == "" && config.Server.Auth.Password == "" {
if conf.Server.Auth.Username == "" && conf.Server.Auth.Password == "" {
return true, nil
}
isValidUsername := subtle.ConstantTimeCompare([]byte(username), []byte(config.Server.Auth.Username)) == 1
isValidPassword := subtle.ConstantTimeCompare([]byte(password), []byte(config.Server.Auth.Password)) == 1
isValidUsername := subtle.ConstantTimeCompare([]byte(username), []byte(conf.Server.Auth.Username)) == 1
isValidPassword := subtle.ConstantTimeCompare([]byte(password), []byte(conf.Server.Auth.Password)) == 1
if isValidUsername && isValidPassword {
return true, nil
@ -55,12 +47,23 @@ func main() {
return false, nil
}))
assetHandler := http.FileServer(rice.MustFindBox("static").HTTPBox())
actions = createActions()
assetHandler := http.FileServer(rice.MustFindBox("../static").HTTPBox())
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler)))
e.GET("/", homeController)
e.GET("/ws", wsController)
e.GET("/manifest.webmanifest", handler.ManifestHandler)
e.GET("/ws", handler.WsHandler)
e.GET("/capture", handler.CaptureHandler)
e.GET("/", func(c echo.Context) error {
return handler.HomeHandler(c, conf)
})
e.Logger.Fatal(e.Start(config.Server.Listen))
if conf.Server.Tls.Enable == false {
e.Logger.Fatal(e.Start(conf.Server.Listen))
} else {
e.Logger.Fatal(e.StartTLS(
conf.Server.Listen,
conf.Server.Tls.CertFile,
conf.Server.Tls.CertKeyFile,
))
}
}

116
cmd/rice-box.go Normal file

File diff suppressed because one or more lines are too long

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
@ -98,7 +102,5 @@ remote:
label: Volume
- label: Desktop
items:
- type: screenshot
label: Screenshot
- type: live_video
label: Live video
- type: capture
label: Capture

View file

@ -1,16 +0,0 @@
package main
import (
"github.com/labstack/echo/v4"
"net/http"
)
type HomeViewParams struct {
Config Config
}
func homeController(c echo.Context) error {
return c.HTML(http.StatusOK, view("views/page/home.html", HomeViewParams{
Config: config,
}))
}

View file

@ -1,4 +1,4 @@
package main
package action
import (
"github.com/gorilla/websocket"
@ -8,16 +8,16 @@ type Actions struct {
Functions map[string]func(ws *websocket.Conn, msg []byte) error
}
func (actions Actions) add(name string, callback func(ws *websocket.Conn, msg []byte) error) {
func (actions Actions) Add(name string, callback func(ws *websocket.Conn, msg []byte) error) {
actions.Functions[name] = callback
}
func (actions Actions) has(name string) bool {
func (actions Actions) Has(name string) bool {
_, exists := actions.Functions[name]
return exists
}
func (actions Actions) exec(name string, ws *websocket.Conn, msg []byte) error {
func (actions Actions) Exec(name string, ws *websocket.Conn, msg []byte) error {
return actions.Functions[name](ws, msg)
}

View file

@ -1,10 +1,17 @@
package main
package config
import (
"gopkg.in/yaml.v3"
"os"
"gopkg.in/yaml.v3"
)
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 +20,7 @@ type ServerAuthConfig struct {
type ServerConfig struct {
Listen string `yaml:"listen"`
Auth ServerAuthConfig `yaml:"auth"`
Tls TlsConfig `yaml:tls`
}
type RemoteItemConfigItem struct {
@ -36,7 +44,7 @@ type Config struct {
Remote []RemoteItem `yaml:"remote"`
}
func createConfigFromFile(file string) (Config, error) {
func CreateConfigFromFile(file string) (Config, error) {
data, err := os.ReadFile(file)
value := Config{}

View file

@ -0,0 +1,68 @@
package handler
import (
"bytes"
"image/color"
"image/jpeg"
"math"
"net/http"
"time"
"github.com/kbinani/screenshot"
"github.com/labstack/echo/v4"
"gitnet.fr/deblan/remote-i3wm-go/internal/pointer"
)
func CaptureHandler(c echo.Context) error {
bounds := screenshot.GetDisplayBounds(0)
switch c.QueryParam("type") {
case "screenshot":
if img, err := screenshot.CaptureRect(bounds); err == nil {
var buf bytes.Buffer
jpeg.Encode(&buf, img, nil)
c.Response().Header().Set("Content-Type", "image/jpeg")
return c.Blob(http.StatusOK, "image/jpeg", buf.Bytes())
}
default:
c.Response().Header().Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
for {
if img, err := screenshot.CaptureRect(bounds); err == nil {
var buf bytes.Buffer
if c.QueryParam("pointer") == "1" {
currentX, currentY := pointer.Positions()
pointerSize := 2 * 16.0
pixelColor := color.RGBA{
R: 255,
G: 0,
B: 0,
A: 255,
}
for x := math.Max(0.0, currentX-pointerSize/2); x <= currentX+3; x++ {
for y := math.Max(0.0, currentY-pointerSize/2); y < currentY+3; y++ {
img.SetRGBA(int(x), int(y), pixelColor)
}
}
}
jpeg.Encode(&buf, img, nil)
_, _ = c.Response().Write([]byte("--frame\r\n"))
c.Response().Write([]byte("Content-Type: image/jpeg\r\n\r\n"))
c.Response().Write(buf.Bytes())
c.Response().Write([]byte("\r\n"))
time.Sleep(33 * time.Millisecond)
}
}
}
return nil
}

22
internal/handler/home.go Normal file
View file

@ -0,0 +1,22 @@
package handler
import (
"net/http"
"time"
"github.com/labstack/echo/v4"
"gitnet.fr/deblan/remote-i3wm-go/internal/config"
"gitnet.fr/deblan/remote-i3wm-go/internal/render"
)
type HomeViewParams struct {
Config config.Config
Now time.Time
}
func HomeHandler(c echo.Context, conf config.Config) error {
return c.HTML(http.StatusOK, render.View("page/home.html", HomeViewParams{
Config: conf,
Now: time.Now(),
}))
}

View file

@ -0,0 +1,47 @@
package handler
import (
"net/http"
"github.com/labstack/echo/v4"
)
type ManifestIcon struct {
Src string `json:"src"`
Type string `json:"type"`
Sizes string `json:"sizes"`
}
type Manifest struct {
ShortName string `json:"short_name"`
Name string `json:"name"`
ThemeColor string `json:"theme_color"`
BackgroundColor string `json:"background_color"`
Display string `json:"display"`
Orientation string `json:"orientation"`
Scope string `json:"scope"`
StartUrl string `json:"start_url"`
Icons []ManifestIcon `json:"icons"`
}
func ManifestHandler(c echo.Context) error {
manifest := &Manifest{
ShortName: "RWM",
Name: "Remote i3WM",
ThemeColor: "#1e3650",
BackgroundColor: "#ffffff",
Display: "standalone",
Orientation: "portrait-primary",
Scope: "/",
StartUrl: "/",
Icons: []ManifestIcon{
ManifestIcon{
Src: "/static/img/icon.png",
Type: "image/png",
Sizes: "96x96",
},
},
}
return c.JSONPretty(http.StatusOK, manifest, " ")
}

View file

@ -1,18 +1,19 @@
package main
package handler
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/gorilla/websocket"
"github.com/kbinani/screenshot"
"github.com/labstack/echo/v4"
"image/jpeg"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
"gitnet.fr/deblan/remote-i3wm-go/internal/action"
"gitnet.fr/deblan/remote-i3wm-go/internal/pointer"
)
type Message struct {
@ -37,6 +38,7 @@ type MessagesData struct {
type ScreenshotMessageData struct {
Quality string `json:quality`
Pointer bool `json:pointer`
}
type MessageResponse struct {
@ -56,12 +58,12 @@ func sendMessageResponse(ws *websocket.Conn, r MessageResponse) {
ws.WriteMessage(websocket.TextMessage, value)
}
func createActions() Actions {
actions := Actions{
func createActions() action.Actions {
actions := action.Actions{
Functions: make(map[string]func(ws *websocket.Conn, msg []byte) error),
}
actions.add("pointer", func(ws *websocket.Conn, msg []byte) error {
actions.Add("pointer", func(ws *websocket.Conn, msg []byte) error {
data := PointerMessageData{}
json.Unmarshal([]byte(msg), &data)
@ -83,19 +85,7 @@ func createActions() Actions {
return cmd.Run()
}
location := exec.Command("xdotool", "getmouselocation")
output, _ := location.Output()
position := string(output)
currentX := 0.0
currentY := 0.0
for key, value := range strings.Split(position, " ") {
if key == 0 {
currentX, _ = strconv.ParseFloat(strings.Replace(value, "x:", "", 1), 32)
} else if key == 1 {
currentY, _ = strconv.ParseFloat(strings.Replace(value, "y:", "", 1), 32)
}
}
currentX, currentY := pointer.Positions()
newX, _ := strconv.ParseFloat(data.X, 32)
newY, _ := strconv.ParseFloat(data.Y, 32)
@ -108,7 +98,7 @@ func createActions() Actions {
return cmd.Run()
})
actions.add("scroll", func(ws *websocket.Conn, msg []byte) error {
actions.Add("scroll", func(ws *websocket.Conn, msg []byte) error {
value := getSimpleMessageValue(msg)
key := ""
@ -128,7 +118,7 @@ func createActions() Actions {
return nil
})
actions.add("workspace", func(ws *websocket.Conn, msg []byte) error {
actions.Add("workspace", func(ws *websocket.Conn, msg []byte) error {
value := getSimpleMessageValue(msg)
if value == "" {
@ -140,11 +130,27 @@ func createActions() Actions {
return cmd.Run()
})
actions.add("volume", func(ws *websocket.Conn, msg []byte) error {
actions.Add("volume", func(ws *websocket.Conn, msg []byte) error {
value := getSimpleMessageValue(msg)
if value == "" {
return errors.New("Invalid value")
if value == "value" {
cmd := exec.Command("amixer", "get", "Master")
output, _ := cmd.Output()
r := regexp.MustCompile(`\[([0-9]+%)\]`)
value := string(r.Find(output))
value = strings.Replace(value, "[", "", 1)
value = strings.Replace(value, "]", "", 1)
value = strings.Replace(value, "%", "", 1)
if value != "" {
sendMessageResponse(ws, MessageResponse{
Type: "volume",
Value: value,
})
}
return nil
}
if value == "up" {
@ -177,7 +183,7 @@ func createActions() Actions {
return cmd.Run()
})
actions.add("media", func(ws *websocket.Conn, msg []byte) error {
actions.Add("media", func(ws *websocket.Conn, msg []byte) error {
value := getSimpleMessageValue(msg)
if value == "" {
@ -237,7 +243,7 @@ func createActions() Actions {
return nil
})
actions.add("keys", func(ws *websocket.Conn, msg []byte) error {
actions.Add("keys", func(ws *websocket.Conn, msg []byte) error {
value := strings.TrimSpace(getSimpleMessageValue(msg))
if value == "" {
@ -253,6 +259,8 @@ func createActions() Actions {
key = "Control_L"
} else if key == "alt" {
key = "Alt_L"
} else if key == "tab" {
key = "Tab"
}
if key != "" {
@ -269,7 +277,7 @@ func createActions() Actions {
return cmd.Run()
})
actions.add("key", func(ws *websocket.Conn, msg []byte) error {
actions.Add("key", func(ws *websocket.Conn, msg []byte) error {
value := strings.TrimSpace(getSimpleMessageValue(msg))
keys := make(map[string]string)
@ -294,7 +302,7 @@ func createActions() Actions {
return cmd.Run()
})
actions.add("text", func(ws *websocket.Conn, msg []byte) error {
actions.Add("text", func(ws *websocket.Conn, msg []byte) error {
value := strings.TrimSpace(getSimpleMessageValue(msg))
if value == "" {
@ -306,45 +314,15 @@ func createActions() Actions {
return cmd.Run()
})
actions.add("screenshot", func(ws *websocket.Conn, msg []byte) error {
data := ScreenshotMessageData{}
json.Unmarshal([]byte(msg), &data)
bounds := screenshot.GetDisplayBounds(0)
img, err := screenshot.CaptureRect(bounds)
if err != nil {
return errors.New("Capture error")
}
var quality int
if data.Quality == "lq" {
quality = 10
} else {
quality = 90
}
buff := new(bytes.Buffer)
jpeg.Encode(buff, img, &jpeg.Options{Quality: quality})
sendMessageResponse(ws, MessageResponse{
Type: "screenshot",
Value: toBase64(buff.Bytes()),
})
return nil
})
actions.add("messages", func(ws *websocket.Conn, msg []byte) error {
actions.Add("messages", func(ws *websocket.Conn, msg []byte) error {
data := MessagesData{}
json.Unmarshal([]byte(msg), &data)
for _, value := range data.Value {
msg, _ := json.Marshal(value)
if actions.has(value.Type) {
actions.exec(value.Type, ws, msg)
if actions.Has(value.Type) {
actions.Exec(value.Type, ws, msg)
time.Sleep(400 * time.Millisecond)
}
}
@ -357,9 +335,10 @@ func createActions() Actions {
var (
upgrader = websocket.Upgrader{}
actions = createActions()
)
func wsController(c echo.Context) error {
func WsHandler(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
@ -371,15 +350,22 @@ func wsController(c echo.Context) error {
_, msg, err := ws.ReadMessage()
if err != nil {
ws.Close()
fmt.Printf("%+v\n", "Connection closed")
return err
}
message := Message{}
json.Unmarshal([]byte(msg), &message)
if message.Type != "" && actions.has(message.Type) {
actions.exec(message.Type, ws, msg)
if message.Type != "" && actions.Has(message.Type) {
actions.Exec(message.Type, ws, msg)
}
sendMessageResponse(ws, MessageResponse{
Type: "statement",
Value: "end",
})
}
return nil

View file

@ -0,0 +1,25 @@
package pointer
import (
"os/exec"
"strconv"
"strings"
)
func Positions() (float64, float64) {
location := exec.Command("xdotool", "getmouselocation")
output, _ := location.Output()
position := string(output)
currentX := 0.0
currentY := 0.0
for key, value := range strings.Split(position, " ") {
if key == 0 {
currentX, _ = strconv.ParseFloat(strings.Replace(value, "x:", "", 1), 32)
} else if key == 1 {
currentY, _ = strconv.ParseFloat(strings.Replace(value, "y:", "", 1), 32)
}
}
return currentX, currentY
}

17
internal/render/render.go Normal file
View file

@ -0,0 +1,17 @@
package render
import (
"bytes"
"html/template"
"gitnet.fr/deblan/remote-i3wm-go/templates"
)
func View(viewName string, data any) string {
var render bytes.Buffer
view := template.Must(template.ParseFS(templates.Views, viewName, "layout/base.html"))
view.Execute(&render, data)
return render.String()
}

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

176
static/css/main.css Normal file
View file

@ -0,0 +1,176 @@
:root {
--link-color: #1e3650;
--bs-link-color: var(--link-color);
}
* {
overscroll-behavior: contain !important;
}
a {
color: var(--link-color);
}
.btn-primary {
background: #1e3650;
border-color: #0e2640;
}
.nav-pills .nav-link.active {
background: #1e3650;
}
.nav-pills .nav-link {
padding-left: 3px;
padding-right: 3px;
}
.nav-link {
font-size: 10px;
}
.legend {
color: #777;
margin: 3px 0;
padding: 3px 0;
border-bottom: 1px solid #eee;
font-size: 11px;
text-transform: uppercase;
}
.btn-sm {
font-size: 9px;
}
.select2 {
min-width: 100%;
}
.line {
height: 3px;
}
.pane {
display: none;
}
.no-margin {
margin: 0;
}
.no-padding {
padding: 0;
}
.no-radius {
border-radius: 0 !important;
}
#pointer {
height: calc(100vh - 33px);
bottom: 33px;
margin: auto;
background: #ccc;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
position: absolute;
width: calc(100% - 50px);
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
#scrollbar {
height: calc(100vh - 33px);
width: 50px;
background: #333;
position: fixed;
bottom: 33px;
z-index: 100;
right: 0;
}
#mouse {
position: fixed;
bottom: 83px;
z-index: 200;
width: calc(100vw - 50px);
padding: 10px;
background: #fff;
}
#pane-pointer .form-group {
padding: 0;
margin: 0;
}
#pointer-buttons {
margin-top: -42px;
width: 100%;
z-index: 110;
position: fixed;
bottom: 31px;
padding-left: 0;
padding-right: 0;
}
#pointer-buttons .btn {
height: 50px;
}
#disconneced {
position: absolute;
top: 0;
width: 100%;
background: #ff6161;
color: #fff;
padding: 5px;
}
#disconneced a {
color: #fff;
font-weight: bold;
}
#nav {
border-top: 2px solid #1e3650;
position: fixed;
bottom: 0;
width: 100%;
height: 33px;
}
#shortcuts_special_keys input {
display: none;
}
#response {
position: absolute;
top: 0;
width: 100%;
color: #fff;
background: #748c26;
padding: 5px;
display: none;
}
.capture-img img {
max-width: 100%;
margin-top: 10px;
cursor: pointer;
}
#mouse-screenshot-live {
display: inline-block;
width: 80px;
padding-left: 5px;
}
#mouse-text-live {
display: inline-block;
width: calc(100% - 100px);
}

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

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

@ -0,0 +1,400 @@
let ws
let pointer, scroller, response, screenshotImg
let scrollLastTimestamp, scrollLastValue
let mousePosX, mousePosY, mouseInitPosX, mouseInitPosY
let isLive = false
let wsLock = false
let isPointerLive = false
let isScreenshotWaiting = null
let isPointerScreenshotWaiting = false
let emptyImg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gIJDjc3srQk8gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAADElEQVQI12P48+cPAAXsAvVTWDc6AAAAAElFTkSuQmCC"
function createWebSocketConnection() {
const protocol = location.protocol === 'https:' ? 'wss' : 'ws'
ws = new WebSocket(`${protocol}://${window.location.hostname}:${window.location.port}/ws`)
ws.addEventListener('open', function(event) {
document.querySelector('#disconneced').style.display = 'none'
unLock()
})
ws.addEventListener('close', function(event) {
unLock()
document.querySelector('#disconneced').style.display = 'block'
window.setTimeout(createWebSocketConnection, 5000)
})
ws.addEventListener('message', function(event) {
unLock()
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)
return
}
if (data.type === 'volume') {
if (data.value.length) {
setVolume(parseInt(data.value))
}
return
}
})
}
function isLocked() {
return wsLock === true
}
function lock() {
wsLock = true
}
function unLock() {
wsLock = false
}
function send(message) {
if (isLocked()) {
return
}
lock()
ws.send(message)
}
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) {
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) => {
item.checked = false
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)
}
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) {
send('{"type":"text","value": "' + (keys.replace('"', '\\"')) + '"}')
}
}
function textKeyUpHandler(e) {
const keys = document.querySelector('#text').value
if (e.keyCode === 13) {
send('{"type":"text","value": "' + (keys.replace('"', '\\"')) + '"}')
}
}
function liveTextKeyUpHandler(e) {
const value = e.target.value
const size = value.length
const messages = []
if (size > 0) {
messages.push('{"type":"text","value": "' + (value.replace('"', '\\"')) + '"}')
if (value[size-1] === ' ') {
messages.push('{"type":"key","value":"space"}')
}
}
if (e.keyCode === 8) {
messages.push('{"type":"key","value": "backspace"}')
} else if (e.keyCode === 13 && size === 0) {
messages.push('{"type":"key","value": "enter"}')
}
send(`{"type":"messages","value":[${messages.join(',')}]}`)
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) {
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
send('{"type":"scroll","value": "' + value + '"}')
}
function pointerTouchStartHandler(e) {
const touch = e.targetTouches[0]
mouseInitPosX = touch.pageX
mouseInitPosY = touch.pageY
}
function pointerLiveHandler(e) {
if (!e.target.checked) {
pointer.style.backgroundImage = ""
} else {
pointer.style.backgroundImage = `url("/capture?type=live&pointer=1&${Math.random()}")`
}
}
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 capture(mode) {
}
function captureScreenshotClickHandler(e) {
const img = e.target.parentNode.querySelector('.capture-img img')
img.src = "/capture?type=screenshot&" + Math.random()
}
function captureLiveClickHandler(e) {
const img = e.target.parentNode.querySelector('.capture-img img')
if (img.src.indexOf("live") > -1) {
img.src = emptyImg
} else {
img.src = "/capture?type=live&" + Math.random()
}
}
function fullscreenHandler(e) {
const targetConf = e.target.getAttribute('data-target')
const isFullscreen = parseInt(e.target.getAttribute('data-fullscreen'))
const element = (targetConf === 'this')
? e.target
: document.querySelector(targetConf)
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)
Array.from(document.querySelectorAll('.live-text')).forEach((element) => {
element.setAttribute('data-composing', '0')
addEventListenerOn(element, 'compositionstart', (e) => {
element.setAttribute('data-composing', '1')
})
addEventListenerOn(element, 'compositionend', (e) => {
element.setAttribute('data-composing', '0')
})
addEventListenerOn(element, 'keyup', (e) => {
if (element.getAttribute('data-composing') === '1') {
return
}
liveTextKeyUpHandler(e)
})
})
addEventListenerOn(scroller, 'touchstart', scrollerTouchStartHandler)
addEventListenerOn(scroller, 'touchmove', scrollerTouchMoveHandler)
addEventListenerOn(pointer, 'click', pointerClickHandler)
addEventListenerOn(pointer, 'touchstart', pointerTouchStartHandler)
addEventListenerOn(pointer, 'touchmove', pointerTouchMoveHandler)
addEventListenerOn('#mouse-screenshot-live input', 'change', pointerLiveHandler)
addEventListenerOn('.capture-live', 'click', captureLiveClickHandler)
addEventListenerOn('.capture-screenshot', 'click', captureScreenshotClickHandler)
addEventListenerOn('.btn-fullscreen', 'click', fullscreenHandler)
}
function getVolume() {
if (document.querySelectorAll('.volume').length) {
try {
send('{"type":"volume","value":"value"}')
} catch (e) {
}
document.querySelectorAll('.volume input[type="range"]').forEach(function(input) {
if (input.getAttribute('data-event')) {
return
}
input.setAttribute('data-event', 'ok')
input.addEventListener('change', (e) => {
send(`{"type":"volume","value":"${e.target.value}"}`)
})
})
}
window.setTimeout(getVolume, 2000)
}
function setVolume(value) {
document.querySelectorAll('.volume').forEach(function(item) {
item.querySelector('input[type="range"]').value = value
})
}
function bootstrap() {
pointer = document.querySelector('#pointer')
scroller = document.querySelector('#scrollbar')
response = document.querySelector('#response')
screenshotImg = document.querySelector('#screenshot img')
shortcutsSpecialKeysOnChangeHandler()
createWebSocketConnection()
addListeners()
documentHashHandler()
getVolume()
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,137 +0,0 @@
a {
color: #1e3650;
}
.btn-primary {
background: #1e3650;
border-color: #0e2640;
}
.nav-pills .nav-link.active {
background: #1e3650;
}
.nav-pills .nav-link {
padding-left: 3px;
padding-right: 3px;
}
.nav-link {
font-size: 10px;
}
.legend {
color: #777;
margin: 3px 0;
padding: 3px 0;
border-bottom: 1px solid #eee;
font-size: 11px;
text-transform: uppercase;
}
.btn-sm {
font-size: 9px;
}
.select2 {
min-width: 100%;
}
.line {
height: 3px;
}
.pane {
display: none;
}
.no-margin {
margin: 0;
}
.no-padding {
padding: 0;
}
.no-radius {
border-radius: 0 !important;
}
#pointer {
height: calc(100vh - 33px - 38px);
top: calc(33px + 38px);
margin: auto;
background: #ccc;
position: absolute;
width: 100%;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
#scrollbar {
height: calc(100vh - 80px);
width: 50px;
background: #333;
position: absolute;
z-index: 100;
right: 0;
}
#pane-pointer .form-group {
padding: 0;
margin: 0;
}
#pointer-buttons {
margin-top: -42px;
width: 100%;
z-index: 110;
position: fixed;
bottom: 0;
}
#pointer-buttons .btn {
height: 50px;
}
#disconneced {
position: absolute;
top: 0;
width: 100%;
background: #ff6161;
color: #fff;
padding: 5px;
}
#disconneced a {
color: #fff;
font-weight: bold;
}
#nav {
border-bottom: 2px solid #1e3650;
}
#shortcuts_special_keys input {
display: none;
}
#response {
position: absolute;
bottom: 0;
width: 100%;
color: #fff;
background: #748c26;
padding: 5px;
display: none;
}
#screenshot img {
max-width: 100%;
margin-top: 10px;
cursor: pointer;
}

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();
});

8
static/static.go Normal file
View file

@ -0,0 +1,8 @@
package static
import "embed"
var (
//go:embed css/* img/* js/*
Files embed.FS
)

View file

@ -0,0 +1,25 @@
{{define "main"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<link rel="stylesheet" href="/static/css/bootstrap.min.css?t={{.Now.Format "2006-01-02 15:04:05"}}" type="text/css">
<link rel="stylesheet" href="/static/css/main.css?t={{.Now.Format "2006-01-02 15:04:05"}}" type="text/css">
<link rel="manifest" href="/manifest.webmanifest?t={{.Now.Format "2006-01-02 15:04:05"}}">
<link rel="icon" type="image/png" href="/static/img/icon.png?t={{.Now.Format "2006-01-02 15:04:05"}}">
<title>Remote i3-wm</title>
</head>
<body>
{{template "content" .}}
<div id="disconneced">
You are disconnected [<a href="#" onclick="location.reload(); return false;">Refresh</a>]
</div>
<div id="response"></div>
<script src="/static/js/main.js?t={{.Now.Format "2006-01-02 15:04:05"}}"></script>
</body>
</html>
{{end}}

View file

@ -30,7 +30,8 @@
{{if eq $value.Type "live_text"}}
<div class="form-group col-12">
<input type="text" class="form-control live-text" name="text">
<input type="text" class="form-control live-text" name="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
<pre></pre>
</div>
{{end}}
@ -62,19 +63,23 @@
{{if eq $value.Type "shortcuts"}}
<div class="col-9" id="shortcuts_special_keys">
<label class="btn btn-secondary" for="shortcuts_special_key_ctrl">
<label class="btn btn-secondary mb-1" for="shortcuts_special_key_ctrl">
<input type="checkbox" value="ctrl" id="shortcuts_special_key_ctrl">
ctrl
</label>
<label class="btn btn-secondary" for="shortcuts_special_key_shift">
<label class="btn btn-secondary mb-1" for="shortcuts_special_key_shift">
<input type="checkbox" value="shift" id="shortcuts_special_key_shift">
shift
</label>
<label class="btn btn-secondary" for="shortcuts_special_key_alt">
<label class="btn btn-secondary mb-1" for="shortcuts_special_key_alt">
<input type="checkbox" value="alt" id="shortcuts_special_key_alt">
alt
</label>
<label class="btn btn-secondary" for="shortcuts_special_key_win">
<label class="btn btn-secondary mb-1" for="shortcuts_special_key_tab">
<input type="checkbox" value="tab" id="shortcuts_special_key_tab">
tab
</label>
<label class="btn btn-secondary mb-1" for="shortcuts_special_key_win">
<input type="checkbox" value="win" id="shortcuts_special_key_win">
win
</label>
@ -97,23 +102,21 @@
{{end}}
{{if eq $value.Type "volume"}}
<div class="col-12">
<button type="button" data-msg='{"type":"volume","value":"0"}' class="btn btn-secondary">0%</button>
<button type="button" data-msg='{"type":"volume","value":"25"}' class="btn btn-secondary">25%</button>
<button type="button" data-msg='{"type":"volume","value":"50"}' class="btn btn-secondary">50%</button>
<button type="button" data-msg='{"type":"volume","value":"75"}' class="btn btn-secondary">75%</button>
<button type="button" data-msg='{"type":"volume","value":"100"}' class="btn btn-secondary">100%</button>
</div>
<div class="line col-12"></div>
<div class="col-12">
<div class="col-12 volume">
<input type="range" min="0" max="100" value="50" class="form-range">
<button type="button" data-msg='{"type":"volume","value":"down"}' class="btn btn-secondary">-</button>
<button type="button" data-msg='{"type":"volume","value":"up"}' class="btn btn-secondary">+</button>
</div>
{{end}}
{{if eq $value.Type "mouse"}}
<div class="form-group col-12">
<input type="text" class="form-control live-text" placeholder="Live text" name="text">
<div class="form-group col-12" id="mouse">
<div id="mouse-screenshot-live">
<label>
<input type="checkbox"> Screen
</label>
</div>
<input type="text" id="mouse-text-live" class="form-control live-text" placeholder="Live text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
</div/>
<div id="scrollbar"></div>
<div id="pointer"></div>
@ -122,18 +125,17 @@
</div>
{{end}}
{{if eq $value.Type "screenshot"}}
{{if eq $value.Type "capture"}}
<div class="col-12">
<button type="button" data-msg='{"type":"screenshot","quality":"hq"}' class="btn btn-secondary">Screenshot HQ</button>
<button type="button" data-msg='{"type":"screenshot","quality":"lq"}' class="btn btn-secondary">Screenshot LQ</button>
</div>
{{end}}
{{if eq $value.Type "live_video"}}
<div class="col-12">
<button type="button" id="live-hq" class="btn btn-secondary">Live HQ</button>
<button type="button" id="live-lq" class="btn btn-secondary">Live LQ</button>
<div id="screenshot"><img class="btn-fullscreen" data-target="#screenshot img" src="data:image/png; base64, iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gIJDjc3srQk8gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAADElEQVQI12P48+cPAAXsAvVTWDc6AAAAAElFTkSuQmCC"></div>
<button type="button" class="btn btn-secondary capture-screenshot">
Screenshot
</button>
<button type="button" class="btn btn-secondary capture-live">
Live
</button>
<div class="capture-img">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gIJDjc3srQk8gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAADElEQVQI12P48+cPAAXsAvVTWDc6AAAAAElFTkSuQmCC" class="btn-fullscreen" data-target="this">
</div>
</div>
{{end}}

8
templates/templates.go Normal file
View file

@ -0,0 +1,8 @@
package templates
import "embed"
var (
//go:embed layout page
Views embed.FS
)

View file

@ -1,20 +0,0 @@
package main
import (
"bytes"
"encoding/base64"
"html/template"
)
func view(viewName string, data any) string {
var render bytes.Buffer
view := template.Must(template.ParseFS(views, viewName, "views/layout/base.html"))
view.Execute(&render, data)
return render.String()
}
func toBase64(b []byte) string {
return base64.StdEncoding.EncodeToString(b)
}

View file

@ -1,26 +0,0 @@
{{define "main"}}
<!DOCTYPE html>
<html lang="en">
<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">
<title>Remote i3-wm</title>
</head>
<body>
{{template "content" .}}
<div id="disconneced">
You are disconnected [<a href="#" onclick="location.reload(); return false;">Refresh</a>]
</div>
<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>
</body>
</html>
{{end}}