Compare commits
32 commits
feature/au
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
0e0867aa30 |
|||
|
af16752cf8 |
|||
|
8cb19f2555 |
|||
|
b415b00e5b |
|||
|
7ed8364a13 |
|||
|
eaa1c9397f |
|||
|
b9bea6bdd2 |
|||
|
408c9c6d89 |
|||
|
dd16a82057 |
|||
|
946d47b4ac |
|||
|
30d71da2bd |
|||
| 653ab1d9cd | |||
|
7cf77935b3 |
|||
|
451717ddac |
|||
|
ae53fe1ab7 |
|||
|
5aab502a76 |
|||
|
e3cbf3eda2 |
|||
|
ff5a66ad07 |
|||
|
a07f14765a |
|||
|
233d1a4d96 |
|||
|
a9601c479d |
|||
| e65fccee03 | |||
|
73b9f98297 |
|||
|
fc8e74c772 |
|||
|
253073f15f |
|||
|
c865080df1 |
|||
|
5f18c04d4d |
|||
| e6934dd9aa | |||
|
71ce3bb90d |
|||
|
3533ea4dfe |
|||
|
|
dd24fda35b |
||
|
|
5e76b76c31 |
32 changed files with 1091 additions and 754 deletions
42
CHANGELOG.md
42
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
7
Makefile
7
Makefile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
116
cmd/rice-box.go
Normal file
File diff suppressed because one or more lines are too long
10
config.yaml
10
config.yaml
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}))
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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{}
|
||||
|
||||
68
internal/handler/capture.go
Normal file
68
internal/handler/capture.go
Normal 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
22
internal/handler/home.go
Normal 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(),
|
||||
}))
|
||||
}
|
||||
47
internal/handler/manifest.go
Normal file
47
internal/handler/manifest.go
Normal 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, " ")
|
||||
}
|
||||
|
|
@ -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
|
||||
25
internal/pointer/pointer.go
Normal file
25
internal/pointer/pointer.go
Normal 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
17
internal/render/render.go
Normal 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()
|
||||
}
|
||||
84
rice-box.go
84
rice-box.go
File diff suppressed because one or more lines are too long
7
static/bootstrap.bundle.min.js
vendored
7
static/bootstrap.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
7
static/bootstrap.min.css
vendored
7
static/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
7
static/bootstrap.min.js
vendored
7
static/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
6
static/css/bootstrap.min.css
vendored
Normal file
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
176
static/css/main.css
Normal 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
BIN
static/img/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
4
static/jquery.min.js
vendored
4
static/jquery.min.js
vendored
File diff suppressed because one or more lines are too long
400
static/js/main.js
Normal file
400
static/js/main.js
Normal 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)
|
||||
3
static/js/service_worker.js
Normal file
3
static/js/service_worker.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
self.addEventListener("install", (e) => {
|
||||
console.log("[Service Worker] Install");
|
||||
});
|
||||
137
static/main.css
137
static/main.css
|
|
@ -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;
|
||||
}
|
||||
308
static/main.js
308
static/main.js
|
|
@ -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
8
static/static.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package static
|
||||
|
||||
import "embed"
|
||||
|
||||
var (
|
||||
//go:embed css/* img/* js/*
|
||||
Files embed.FS
|
||||
)
|
||||
25
templates/layout/base.html
Normal file
25
templates/layout/base.html
Normal 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}}
|
||||
|
|
@ -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
8
templates/templates.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package templates
|
||||
|
||||
import "embed"
|
||||
|
||||
var (
|
||||
//go:embed layout page
|
||||
Views embed.FS
|
||||
)
|
||||
20
utils.go
20
utils.go
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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}}
|
||||
Loading…
Add table
Add a link
Reference in a new issue