Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0e0867aa30 |
|||
|
af16752cf8 |
|||
|
8cb19f2555 |
|||
|
b415b00e5b |
|||
|
7ed8364a13 |
|||
|
eaa1c9397f |
|||
|
b9bea6bdd2 |
|||
|
408c9c6d89 |
21 changed files with 308 additions and 282 deletions
|
|
@ -1,5 +1,12 @@
|
|||
## [Unreleased]
|
||||
|
||||
## v3.0.0
|
||||
### Fixed
|
||||
* feat: center live capture in pointer
|
||||
### Changed
|
||||
* refactor: change organisation
|
||||
* feat: use MJPEG for live capture
|
||||
|
||||
## v2.1.0
|
||||
### Added
|
||||
* add live on pointer block
|
||||
|
|
|
|||
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,25 +2,15 @@ package main
|
|||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
rice "github.com/GeertJohan/go.rice"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
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() {
|
||||
|
|
@ -32,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:")
|
||||
|
|
@ -40,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
|
||||
|
|
@ -56,21 +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("/manifest.webmanifest", manifestController)
|
||||
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)
|
||||
})
|
||||
|
||||
if config.Server.Tls.Enable == false {
|
||||
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(
|
||||
config.Server.Listen,
|
||||
config.Server.Tls.CertFile,
|
||||
config.Server.Tls.CertKeyFile,
|
||||
conf.Server.Listen,
|
||||
conf.Server.Tls.CertFile,
|
||||
conf.Server.Tls.CertKeyFile,
|
||||
))
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -102,7 +102,5 @@ remote:
|
|||
label: Volume
|
||||
- label: Desktop
|
||||
items:
|
||||
- type: screenshot
|
||||
label: Screenshot
|
||||
- type: live_video
|
||||
label: Live video
|
||||
- type: capture
|
||||
label: Capture
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type HomeViewParams struct {
|
||||
Config Config
|
||||
Now time.Time
|
||||
}
|
||||
|
||||
func homeController(c echo.Context) error {
|
||||
return c.HTML(http.StatusOK, view("views/page/home.html", HomeViewParams{
|
||||
Config: config,
|
||||
Now: time.Now(),
|
||||
}))
|
||||
}
|
||||
|
|
@ -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,4 +1,4 @@
|
|||
package main
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
|
@ -44,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(),
|
||||
}))
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
package main
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type ManifestIcon struct {
|
||||
|
|
@ -23,7 +24,7 @@ type Manifest struct {
|
|||
Icons []ManifestIcon `json:"icons"`
|
||||
}
|
||||
|
||||
func manifestController(c echo.Context) error {
|
||||
func ManifestHandler(c echo.Context) error {
|
||||
manifest := &Manifest{
|
||||
ShortName: "RWM",
|
||||
Name: "Remote i3WM",
|
||||
|
|
@ -1,13 +1,9 @@
|
|||
package main
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"math"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
|
@ -15,8 +11,9 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/kbinani/screenshot"
|
||||
"github.com/labstack/echo/v4"
|
||||
"gitnet.fr/deblan/remote-i3wm-go/internal/action"
|
||||
"gitnet.fr/deblan/remote-i3wm-go/internal/pointer"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
|
|
@ -61,30 +58,12 @@ func sendMessageResponse(ws *websocket.Conn, r MessageResponse) {
|
|||
ws.WriteMessage(websocket.TextMessage, value)
|
||||
}
|
||||
|
||||
func getPointerPosition() (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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
|
|
@ -106,7 +85,7 @@ func createActions() Actions {
|
|||
return cmd.Run()
|
||||
}
|
||||
|
||||
currentX, currentY := getPointerPosition()
|
||||
currentX, currentY := pointer.Positions()
|
||||
|
||||
newX, _ := strconv.ParseFloat(data.X, 32)
|
||||
newY, _ := strconv.ParseFloat(data.Y, 32)
|
||||
|
|
@ -119,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 := ""
|
||||
|
||||
|
|
@ -139,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 == "" {
|
||||
|
|
@ -151,7 +130,7 @@ 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 == "value" {
|
||||
|
|
@ -204,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 == "" {
|
||||
|
|
@ -264,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 == "" {
|
||||
|
|
@ -298,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)
|
||||
|
||||
|
|
@ -323,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 == "" {
|
||||
|
|
@ -335,63 +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
|
||||
}
|
||||
|
||||
if data.Pointer {
|
||||
currentX, currentY := getPointerPosition()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -404,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 {
|
||||
|
|
@ -426,8 +358,8 @@ func wsController(c echo.Context) error {
|
|||
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{
|
||||
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()
|
||||
}
|
||||
|
|
@ -73,6 +73,7 @@ a {
|
|||
background: #ccc;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
position: absolute;
|
||||
width: calc(100% - 50px);
|
||||
-webkit-touch-callout: none;
|
||||
|
|
@ -157,7 +158,7 @@ a {
|
|||
display: none;
|
||||
}
|
||||
|
||||
#screenshot img {
|
||||
.capture-img img {
|
||||
max-width: 100%;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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'
|
||||
|
|
@ -24,7 +25,7 @@ function createWebSocketConnection() {
|
|||
|
||||
window.setTimeout(createWebSocketConnection, 5000)
|
||||
})
|
||||
|
||||
|
||||
ws.addEventListener('message', function(event) {
|
||||
unLock()
|
||||
let data = JSON.parse(event.data)
|
||||
|
|
@ -47,27 +48,6 @@ function createWebSocketConnection() {
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type === 'screenshot') {
|
||||
if (isScreenshotWaiting || isScreenshotWaiting === null) {
|
||||
if (isScreenshotWaiting) {
|
||||
isScreenshotWaiting = false
|
||||
}
|
||||
|
||||
screenshotImg.setAttribute('src', 'data:image/png;base64, ' + data.value)
|
||||
}
|
||||
|
||||
let pointer = document.querySelector('#pointer')
|
||||
|
||||
if (isPointerScreenshotWaiting) {
|
||||
pointer.style.backgroundImage = `url('data:image/png;base64, ${data.value}')`
|
||||
isPointerScreenshotWaiting = false
|
||||
} else {
|
||||
pointer.style.backgroundImage = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -163,20 +143,27 @@ function textKeyUpHandler(e) {
|
|||
|
||||
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) {
|
||||
send('{"type":"key","value": "backspace"}')
|
||||
} else if (e.keyCode === 13) {
|
||||
send('{"type":"key","value": "enter"}')
|
||||
} else if (value.length) {
|
||||
if (value === ' ') {
|
||||
send('{"type":"key","value": "space"}')
|
||||
} else {
|
||||
send('{"type":"text","value": "' + (value.replace('"', '\\"')) + '"}')
|
||||
}
|
||||
|
||||
e.target.value = ''
|
||||
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) {
|
||||
|
|
@ -223,26 +210,10 @@ function pointerTouchStartHandler(e) {
|
|||
|
||||
function pointerLiveHandler(e) {
|
||||
if (!e.target.checked) {
|
||||
isPointerLive = false
|
||||
isPointerScreenshotWaiting = null
|
||||
|
||||
return
|
||||
pointer.style.backgroundImage = ""
|
||||
} else {
|
||||
pointer.style.backgroundImage = `url("/capture?type=live&pointer=1&${Math.random()}")`
|
||||
}
|
||||
|
||||
isPointerLive = true
|
||||
|
||||
let doScreenshot = function() {
|
||||
if (isPointerLive) {
|
||||
if (!isPointerScreenshotWaiting) {
|
||||
isPointerScreenshotWaiting = true
|
||||
ws.send(`{"type":"screenshot","quality":"lq","pointer":true}`)
|
||||
}
|
||||
|
||||
window.setTimeout(doScreenshot, 300)
|
||||
}
|
||||
}
|
||||
|
||||
doScreenshot()
|
||||
}
|
||||
|
||||
function pointerTouchMoveHandler(e) {
|
||||
|
|
@ -265,46 +236,30 @@ function pointerTouchMoveHandler(e) {
|
|||
ws.send(msg)
|
||||
}
|
||||
|
||||
function liveHqClickHandler(e) {
|
||||
return liveClickHandler(e, 'hq')
|
||||
function capture(mode) {
|
||||
}
|
||||
|
||||
function liveLqClickHandler(e) {
|
||||
return liveClickHandler(e, 'lq')
|
||||
function captureScreenshotClickHandler(e) {
|
||||
const img = e.target.parentNode.querySelector('.capture-img img')
|
||||
img.src = "/capture?type=screenshot&" + Math.random()
|
||||
}
|
||||
|
||||
function liveClickHandler(e, quality) {
|
||||
if (isLive) {
|
||||
isLive = false
|
||||
isScreenshotWaiting = null
|
||||
function captureLiveClickHandler(e) {
|
||||
const img = e.target.parentNode.querySelector('.capture-img img')
|
||||
|
||||
document.querySelector('#live-hq').innerText = 'Live HQ'
|
||||
document.querySelector('#live-lq').innerText = 'Live LQ'
|
||||
|
||||
return
|
||||
if (img.src.indexOf("live") > -1) {
|
||||
img.src = emptyImg
|
||||
} else {
|
||||
img.src = "/capture?type=live&" + Math.random()
|
||||
}
|
||||
|
||||
isLive = true
|
||||
|
||||
e.target.innerText = 'Stop live'
|
||||
|
||||
let doScreenshot = function() {
|
||||
if (isLive) {
|
||||
if (!isScreenshotWaiting) {
|
||||
isScreenshotWaiting = true
|
||||
ws.send(`{"type":"screenshot","quality":"${quality}"}`)
|
||||
}
|
||||
|
||||
window.setTimeout(doScreenshot, 100)
|
||||
}
|
||||
}
|
||||
|
||||
doScreenshot()
|
||||
}
|
||||
|
||||
function fullscreenHandler(e) {
|
||||
let element = document.querySelector(e.target.getAttribute('data-target'))
|
||||
let isFullscreen = parseInt(e.target.getAttribute('data-fullscreen'))
|
||||
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)
|
||||
|
||||
|
|
@ -362,7 +317,26 @@ function addListeners() {
|
|||
addEventListenerOn('#text-clear', 'click', textClearClickHandler)
|
||||
addEventListenerOn('#text-send', 'click', textSendClickHandler)
|
||||
addEventListenerOn('#text', 'keyup', textKeyUpHandler)
|
||||
addEventListenerOn('.live-text', 'keyup', liveTextKeyUpHandler)
|
||||
|
||||
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)
|
||||
|
|
@ -372,8 +346,9 @@ function addListeners() {
|
|||
addEventListenerOn(pointer, 'touchmove', pointerTouchMoveHandler)
|
||||
addEventListenerOn('#mouse-screenshot-live input', 'change', pointerLiveHandler)
|
||||
|
||||
addEventListenerOn('#live-hq', 'click', liveHqClickHandler)
|
||||
addEventListenerOn('#live-lq', 'click', liveLqClickHandler)
|
||||
addEventListenerOn('.capture-live', 'click', captureLiveClickHandler)
|
||||
addEventListenerOn('.capture-screenshot', 'click', captureScreenshotClickHandler)
|
||||
|
||||
addEventListenerOn('.btn-fullscreen', 'click', fullscreenHandler)
|
||||
}
|
||||
|
||||
|
|
|
|||
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
|
||||
)
|
||||
|
|
@ -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}}
|
||||
|
||||
|
|
@ -115,7 +116,7 @@
|
|||
<input type="checkbox"> Screen
|
||||
</label>
|
||||
</div>
|
||||
<input type="text" id="mouse-text-live" class="form-control live-text" placeholder="Live text">
|
||||
<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>
|
||||
|
|
@ -124,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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue