feat(display): pdf support

This commit is contained in:
2025-11-28 23:57:02 +01:00
parent a7184847bb
commit 78fde49329
5 changed files with 69 additions and 20 deletions
+1
View File
@@ -10,6 +10,7 @@ Open source solution for advanced remote control and coordination of one or more
- Bash - Bash
- Chromium - Chromium
- LibreOffice (optional, presentation view) - LibreOffice (optional, presentation view)
- Xreader (optional, pdf view)
- ImageMagick (optional, image preview) - ImageMagick (optional, image preview)
- ffmpeg (optional, video preview) - ffmpeg (optional, video preview)
- Ghostscript (optional, pdf preview) - Ghostscript (optional, pdf preview)
+24 -3
View File
@@ -47,20 +47,33 @@ func GetDeviceMac() (string, error) {
return "", fmt.Errorf("no suitable MAC address found") return "", fmt.Errorf("no suitable MAC address found")
} }
var runningHelpProgram *exec.Cmd = nil
func OpenPresentation(path string) error { func OpenPresentation(path string) error {
tempDirPath, err := os.MkdirTemp("", "plg-mudics-libreoffice-profile-") tempDirPath, err := os.MkdirTemp("", "plg-mudics-libreoffice-profile-")
if err != nil { if err != nil {
return fmt.Errorf("failed to create temporary profile directory: %w", err) return fmt.Errorf("failed to create temporary profile directory: %w", err)
} }
cmd := exec.Command("soffice", "--show", path, "--nologo", "--norestore", fmt.Sprintf("-env:UserInstallation=file:///%s", tempDirPath)) runningHelpProgram = exec.Command("soffice", "--show", path, "--nologo", "--norestore", fmt.Sprintf("-env:UserInstallation=file:///%s", tempDirPath))
result := shared.RunShellCommand(cmd) result := shared.RunShellCommand(runningHelpProgram)
if result.ExitCode != 0 { killedByParent := -1
if result.ExitCode != 0 && result.ExitCode != killedByParent {
return errors.New(result.Stderr) return errors.New(result.Stderr)
} }
return nil return nil
} }
func OpenPDF(path string) error {
runningHelpProgram = exec.Command("xreader", path, "--presentation")
result := shared.RunShellCommandNonBlocking(runningHelpProgram)
killedByParent := -1
if result.ExitCode != 0 && result.ExitCode != killedByParent {
return fmt.Errorf("could not open pdf: %s (%d)", result.Stderr, result.ExitCode)
}
return nil
}
type KeyAction int type KeyAction int
const ( const (
@@ -154,3 +167,11 @@ func ResolveStorageFilePath(pathParam string) (string, bool, error) {
return fullPath, true, nil return fullPath, true, nil
} }
func CloseRunningProgram() error {
if runningHelpProgram == nil {
return nil
}
err := runningHelpProgram.Process.Kill()
return err
}
+13 -17
View File
@@ -15,11 +15,9 @@ import (
"path/filepath" "path/filepath"
shared "plg-mudics/shared" shared "plg-mudics/shared"
"strings" "strings"
"time"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
"github.com/micmonay/keybd_event"
"github.com/skip2/go-qrcode" "github.com/skip2/go-qrcode"
"plg-mudics/display/pkg" "plg-mudics/display/pkg"
@@ -266,6 +264,8 @@ func downloadFileRoute(ctx echo.Context) error {
} }
func openFileRoute(ctx echo.Context) error { func openFileRoute(ctx echo.Context) error {
var err error
pathParam := ctx.Param("path") pathParam := ctx.Param("path")
fullPath := ctx.Get("fullPath").(string) fullPath := ctx.Get("fullPath").(string)
@@ -277,7 +277,7 @@ func openFileRoute(ctx echo.Context) error {
return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Description: "Cant connect to display browser client"}) return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Description: "Cant connect to display browser client"})
} }
err := resetView() err = resetView()
if err != nil { if err != nil {
slog.Error("Failed to reset view", "error", err) slog.Error("Failed to reset view", "error", err)
return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Description: "Failed to reset view"}) return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Description: "Failed to reset view"})
@@ -295,15 +295,18 @@ func openFileRoute(ctx echo.Context) error {
imageTemplate(pathParam).Render(context.Background(), &templateBuffer) imageTemplate(pathParam).Render(context.Background(), &templateBuffer)
sseConnection <- templateBuffer.String() sseConnection <- templateBuffer.String()
case ".pptx", ".odp": case ".pptx", ".odp":
err := pkg.OpenPresentation(fullPath) err = pkg.OpenPresentation(fullPath)
if err != nil { case ".pdf":
slog.Error("Failed to open presentation", "file", pathParam, "error", err) err = pkg.OpenPDF(fullPath)
return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Description: "Failed to open presentation"})
}
default: default:
return ctx.JSON(http.StatusUnsupportedMediaType, shared.ErrorResponse{Description: "Unsupported file type"}) return ctx.JSON(http.StatusUnsupportedMediaType, shared.ErrorResponse{Description: "Unsupported file type"})
} }
if err != nil {
slog.Error("Failed to open file", "file", pathParam, "error", err)
return ctx.JSON(http.StatusInternalServerError, shared.ErrorResponse{Description: "Failed to open file"})
}
slog.Info("Successfully run file", "file", pathParam) slog.Info("Successfully run file", "file", pathParam)
return ctx.JSON(http.StatusOK, struct{}{}) return ctx.JSON(http.StatusOK, struct{}{})
} }
@@ -377,16 +380,9 @@ func previewRoute(ctx echo.Context) error {
// Reset previous file views so they dont collide with the new one // Reset previous file views so they dont collide with the new one
func resetView() error { func resetView() error {
var err error err := pkg.CloseRunningProgram()
err = pkg.KeyboardInput(keybd_event.VK_ESC, pkg.KeyPress)
if err != nil { if err != nil {
return fmt.Errorf("failed to send ESC key: %w", err) return fmt.Errorf("failed to close running program: %w", err)
}
time.Sleep(400 * time.Millisecond)
err = pkg.KeyboardInput(keybd_event.VK_ESC, pkg.KeyRelease)
if err != nil {
return fmt.Errorf("failed to send ESC key: %w", err)
} }
sseConnection <- "" sseConnection <- ""
+1
View File
@@ -91,6 +91,7 @@
nushell nushell
unzip unzip
iputils iputils
xreader
# Libraries # Libraries
imagemagick imagemagick
+30
View File
@@ -2,8 +2,10 @@ package shared
import ( import (
"bytes" "bytes"
"context"
"errors" "errors"
"os/exec" "os/exec"
"time"
) )
type CommandResponse struct { type CommandResponse struct {
@@ -24,6 +26,34 @@ func RunShellCommand(cmd *exec.Cmd) CommandResponse {
cmd.Stderr = &stderr cmd.Stderr = &stderr
err := cmd.Run() err := cmd.Run()
return parseCmdResult(cmd, stdout, stderr, err)
}
func RunShellCommandNonBlocking(cmd *exec.Cmd) CommandResponse {
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Start()
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
select {
case <-ctx.Done():
return CommandResponse{}
case err := <-done:
return parseCmdResult(cmd, stdout, stderr, err)
}
}
func parseCmdResult(cmd *exec.Cmd, stdout, stderr bytes.Buffer, err error) CommandResponse {
commandOutput := CommandResponse{ commandOutput := CommandResponse{
Stdout: stdout.String(), Stdout: stdout.String(),
Stderr: stderr.String(), Stderr: stderr.String(),