feat: embed optional apps in player server

This commit is contained in:
wpetit 2024-01-16 09:27:04 +01:00
parent 9884855e3c
commit a32b18d3aa
19 changed files with 340 additions and 17 deletions

View File

@ -1,2 +1,3 @@
ARCAST_DESKTOP_ADDITIONAL_CHROME_ARGS= ARCAST_DESKTOP_ADDITIONAL_CHROME_ARGS=
ARCAST_DESKTOP_INSTANCE_ID= ARCAST_DESKTOP_INSTANCE_ID=
ARCAST_DESKTOP_WEB_APPS=true

View File

@ -77,7 +77,11 @@ func main() {
logger.Fatal(ctx, "could not retrieve instance id", logger.CapturedE(errors.WithStack(err))) logger.Fatal(ctx, "could not retrieve instance id", logger.CapturedE(errors.WithStack(err)))
} }
server := server.New(browser, server.WithInstanceID(instanceID)) server := server.New(
browser,
server.WithInstanceID(instanceID),
server.WithWebApps(true),
)
if err := server.Start(); err != nil { if err := server.Start(); err != nil {
logger.Fatal(ctx, "could not start server", logger.CapturedE(errors.WithStack(err))) logger.Fatal(ctx, "could not start server", logger.CapturedE(errors.WithStack(err)))

9
go.mod
View File

@ -4,12 +4,12 @@ go 1.21.4
require ( require (
gioui.org v0.4.1 gioui.org v0.4.1
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0
github.com/davecgh/go-spew v1.1.1
github.com/gioui-plugins/gio-plugins v0.0.0-20230625001848-8f18aae6c91c github.com/gioui-plugins/gio-plugins v0.0.0-20230625001848-8f18aae6c91c
github.com/google/uuid v1.3.0 github.com/go-chi/cors v1.2.1
github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1 github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1
github.com/jaevor/go-nanoid v1.3.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/wlynxg/anet v0.0.1
github.com/zserge/lorca v0.1.10 github.com/zserge/lorca v0.1.10
) )
@ -17,6 +17,7 @@ require (
cdr.dev/slog v1.6.1 // indirect cdr.dev/slog v1.6.1 // indirect
gioui.org/cpu v0.0.0-20220412190645-f1e9e8c3b1f7 // indirect gioui.org/cpu v0.0.0-20220412190645-f1e9e8c3b1f7 // indirect
gioui.org/shader v1.0.8 // indirect gioui.org/shader v1.0.8 // indirect
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/charmbracelet/lipgloss v0.7.1 // indirect github.com/charmbracelet/lipgloss v0.7.1 // indirect
@ -25,7 +26,6 @@ require (
github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 // indirect github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 // indirect
github.com/inkeliz/go_inkwasm v0.0.0-20220912074516-049d3472c98a // indirect github.com/inkeliz/go_inkwasm v0.0.0-20220912074516-049d3472c98a // indirect
github.com/jaevor/go-nanoid v1.3.0 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/jedib0t/go-pretty/v6 v6.4.9 // indirect github.com/jedib0t/go-pretty/v6 v6.4.9 // indirect
github.com/leodido/go-urn v1.2.1 // indirect github.com/leodido/go-urn v1.2.1 // indirect
@ -37,7 +37,6 @@ require (
github.com/muesli/termenv v0.15.2 // indirect github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.4 // indirect github.com/rivo/uniseg v0.4.4 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/wlynxg/anet v0.0.1 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect

4
go.sum
View File

@ -34,6 +34,8 @@ github.com/gioui-plugins/gio-plugins v0.0.0-20230625001848-8f18aae6c91c h1:naFDa
github.com/gioui-plugins/gio-plugins v0.0.0-20230625001848-8f18aae6c91c/go.mod h1:nBuRsi6udr2x6eorarLHtRkoRaWBICt+WzaE7zQXgYY= github.com/gioui-plugins/gio-plugins v0.0.0-20230625001848-8f18aae6c91c/go.mod h1:nBuRsi6udr2x6eorarLHtRkoRaWBICt+WzaE7zQXgYY=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@ -50,8 +52,6 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1 h1:cNb52t5fkWv8ZiicKWnc2eZnhsCCoH7WmRBMIbMp04Q= github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1 h1:cNb52t5fkWv8ZiicKWnc2eZnhsCCoH7WmRBMIbMp04Q=
github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1/go.mod h1:I6CSXU4zCGL08JOk9NbcT0ofAgnIkS/fVXbYzfSoDic= github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1/go.mod h1:I6CSXU4zCGL08JOk9NbcT0ofAgnIkS/fVXbYzfSoDic=
github.com/inkeliz/go_inkwasm v0.0.0-20220912074516-049d3472c98a h1:uZklbtdSPrDL/d1EUKd9s8a0Byla2TT01Wg/GZ4xj0w= github.com/inkeliz/go_inkwasm v0.0.0-20220912074516-049d3472c98a h1:uZklbtdSPrDL/d1EUKd9s8a0Byla2TT01Wg/GZ4xj0w=

View File

@ -31,6 +31,11 @@ func Run() *cli.Command {
EnvVars: []string{"ARCAST_DESKTOP_WINDOW_HEIGHT"}, EnvVars: []string{"ARCAST_DESKTOP_WINDOW_HEIGHT"},
Value: defaults.Height, Value: defaults.Height,
}, },
&cli.BoolFlag{
Name: "web-apps",
EnvVars: []string{"ARCAST_DESKTOP_WEB_APPS"},
Value: false,
},
&cli.IntFlag{ &cli.IntFlag{
Name: "window-width", Name: "window-width",
EnvVars: []string{"ARCAST_DESKTOP_WINDOW_WIDTH"}, EnvVars: []string{"ARCAST_DESKTOP_WINDOW_WIDTH"},
@ -41,6 +46,7 @@ func Run() *cli.Command {
windowHeight := ctx.Int("window-height") windowHeight := ctx.Int("window-height")
windowWidth := ctx.Int("window-width") windowWidth := ctx.Int("window-width")
chromeArgs := addFlagsPrefix(ctx.StringSlice("additional-chrome-arg")...) chromeArgs := addFlagsPrefix(ctx.StringSlice("additional-chrome-arg")...)
webApps := ctx.Bool("web-apps")
browser := lorca.NewBrowser( browser := lorca.NewBrowser(
lorca.WithAdditionalChromeArgs(chromeArgs...), lorca.WithAdditionalChromeArgs(chromeArgs...),
@ -69,7 +75,10 @@ func Run() *cli.Command {
instanceID = server.NewRandomInstanceID() instanceID = server.NewRandomInstanceID()
} }
server := server.New(browser, server.WithInstanceID(instanceID)) server := server.New(browser,
server.WithInstanceID(instanceID),
server.WithWebApps(webApps),
)
if err := server.Start(); err != nil { if err := server.Start(); err != nil {
return errors.Wrap(err, "could not start server") return errors.Wrap(err, "could not start server")

View File

@ -1,5 +1,6 @@
**/*.go **/*.go
pkg/server/templates/**.gotmpl pkg/server/templates/**.gotmpl
pkg/server/apps/**
modd.conf modd.conf
.env { .env {
prep: make build-client prep: make build-client

91
pkg/server/apps.go Normal file
View File

@ -0,0 +1,91 @@
package server
import (
"embed"
"encoding/json"
"io/fs"
"net/http"
"os"
"path/filepath"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
_ "embed"
)
var (
apps []App
//go:embed apps/**
appsFS embed.FS
)
func init() {
if err := initializeAppsList(); err != nil {
panic(errors.WithStack(err))
}
}
func initializeAppsList() error {
files, err := fs.Glob(appsFS, "apps/*")
if err != nil {
return errors.WithStack(err)
}
for _, f := range files {
stat, err := fs.Stat(appsFS, f)
if err != nil {
return errors.WithStack(err)
}
if !stat.IsDir() {
continue
}
rawManifest, err := fs.ReadFile(appsFS, filepath.Join(f, "manifest.json"))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
continue
}
return errors.WithStack(err)
}
var app App
if err := json.Unmarshal(rawManifest, &app); err != nil {
return errors.WithStack(err)
}
app.ID = filepath.Base(f)
apps = append(apps, app)
}
return nil
}
func (s *Server) handleDefaultApp(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/apps/"+s.defaultWebApp, http.StatusTemporaryRedirect)
}
type AppsResponse struct {
Apps []App
}
type App struct {
ID string `json:"id"`
Title map[string]string `json:"title"`
Description map[string]string `json:"description"`
Icon string `json:"icon"`
}
func (s *Server) handleApps(w http.ResponseWriter, r *http.Request) {
api.DataResponse(w, http.StatusOK, struct {
DefaultApp string `json:"defaultApp"`
Apps []App `json:"apps"`
}{
DefaultApp: s.defaultWebApp,
Apps: apps,
})
}

View File

@ -0,0 +1,19 @@
fetch('/api/v1/apps')
.then(res => res.json())
.then(res => {
const defaultApp = res.data.defaultApp
const apps = res.data.apps
const container = document.createElement("div")
container.className = "container"
apps.forEach(app => {
if (app.id === defaultApp) return
const appLink = document.createElement("a")
appLink.className = "app-link"
appLink.href = "/apps/" + app.id
appLink.innerText = app.title['fr']
container.appendChild(appLink)
})
document.getElementById("main").replaceWith(container);
})

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Arcast - Home</title>
<link rel="stylesheet" href="style.css">
<script type="text/javascript" src="app.js" defer></script>
</head>
<body>
<div id="main" class="container">
<p class="text-center">Loading...</p>
</div>
</body>
</html>

View File

@ -0,0 +1,10 @@
{
"title": {
"fr": "Accueil",
"en": "Home"
},
"description": {
"fr": "Voir la liste des applications",
"en": "See apps list"
}
}

View File

@ -0,0 +1,97 @@
html {
box-sizing: border-box;
font-size: 16px;
width: 100%;
height: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
width: 100%;
height: 100%;
background-color: #e1e1e1;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
ol,
ul {
list-style: none;
}
body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
ol,
ul {
margin: 0;
padding: 0;
font-weight: normal;
}
.container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.panel {
display: block;
background-color: #fff;
border-radius: 5px;
padding: 10px 20px;
box-shadow: 2px 2px #3333331d;
}
.panel p, .panel ul {
margin-top: 10px;
}
.text-centered {
text-align: center;
}
.text-italic {
font-style: italic;
}
.text-small {
font-size: 0.8em;
}
.mt {
margin-top: 1em;
display: block;
}
.app-link {
display: block;
background: white;
border-radius: 5px;
width: 100px;
height: 100px;
box-shadow: 1px 1px 3px #ccc;
color: #333;
text-decoration: none;
text-align: center;
padding-top: 30px;
}
.app-link:hover {
background-color: #abdbdb;
box-shadow: 1px 1px 3px #aaa;
text-shadow: 1px 1px white;
}

View File

@ -0,0 +1,10 @@
{
"title": {
"fr": "Contrôle à distance",
"en": "Remote control"
},
"description": {
"fr": "Contrôler l'afficheur numérique",
"en": "Control the cast player"
}
}

View File

@ -0,0 +1,10 @@
{
"title": {
"fr": "Partage d'écran",
"en": "Screen sharing"
},
"description": {
"fr": "Partager son écran",
"en": "Share your screen"
}
}

View File

@ -9,6 +9,7 @@ import (
"strconv" "strconv"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api" "gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
@ -34,11 +35,36 @@ func init() {
func (s *Server) startHTTPServer(ctx context.Context) error { func (s *Server) startHTTPServer(ctx context.Context) error {
router := chi.NewRouter() router := chi.NewRouter()
if s.webApps {
ips, err := getLANIPv4Addrs()
if err != nil {
return errors.WithStack(err)
}
allowedOrigins := make([]string, len(ips))
for idx, ip := range ips {
allowedOrigins[idx] = fmt.Sprintf("http://%s:%d", ip, s.port)
}
router.Use(cors.Handler(cors.Options{
AllowedOrigins: allowedOrigins,
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
AllowCredentials: false,
}))
}
router.Get("/", s.handleHome) router.Get("/", s.handleHome)
router.Post("/api/v1/cast", s.handleCast) router.Post("/api/v1/cast", s.handleCast)
router.Delete("/api/v1/cast", s.handleReset) router.Delete("/api/v1/cast", s.handleReset)
router.Get("/api/v1/status", s.handleStatus) router.Get("/api/v1/status", s.handleStatus)
if s.webApps {
router.Get("/apps", s.handleDefaultApp)
router.Get("/api/v1/apps", s.handleApps)
router.Handle("/apps/*", http.FileServer(http.FS(appsFS)))
}
server := http.Server{ server := http.Server{
Addr: s.address, Addr: s.address,
Handler: router, Handler: router,
@ -167,9 +193,10 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) { func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
type templateData struct { type templateData struct {
IPs []string IPs []string
Port int Port int
ID string ID string
WebApps bool
} }
ips, err := getLANIPv4Addrs() ips, err := getLANIPv4Addrs()
@ -180,9 +207,10 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
} }
d := templateData{ d := templateData{
ID: s.instanceID, ID: s.instanceID,
IPs: ips, IPs: ips,
Port: s.port, Port: s.port,
WebApps: s.webApps,
} }
if err := idleTemplate.Execute(w, d); err != nil { if err := idleTemplate.Execute(w, d); err != nil {

View File

@ -20,6 +20,8 @@ type Options struct {
InstanceID string InstanceID string
Address string Address string
DisableServiceDiscovery bool DisableServiceDiscovery bool
WebApps bool
DefaultWebApp string
} }
type OptionFunc func(opts *Options) type OptionFunc func(opts *Options)
@ -29,6 +31,8 @@ func NewOptions(funcs ...OptionFunc) *Options {
InstanceID: NewRandomInstanceID(), InstanceID: NewRandomInstanceID(),
Address: ":", Address: ":",
DisableServiceDiscovery: false, DisableServiceDiscovery: false,
WebApps: false,
DefaultWebApp: "home",
} }
for _, fn := range funcs { for _, fn := range funcs {
@ -38,6 +42,12 @@ func NewOptions(funcs ...OptionFunc) *Options {
return opts return opts
} }
func WithWebApps(enabled bool) OptionFunc {
return func(opts *Options) {
opts.WebApps = enabled
}
}
func WithAddress(addr string) OptionFunc { func WithAddress(addr string) OptionFunc {
return func(opts *Options) { return func(opts *Options) {
opts.Address = addr opts.Address = addr

View File

@ -16,6 +16,9 @@ type Server struct {
port int port int
disableServiceDiscovery bool disableServiceDiscovery bool
webApps bool
defaultWebApp string
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
} }
@ -71,6 +74,8 @@ func New(browser browser.Browser, funcs ...OptionFunc) *Server {
browser: browser, browser: browser,
instanceID: opts.InstanceID, instanceID: opts.InstanceID,
address: opts.Address, address: opts.Address,
webApps: opts.WebApps,
defaultWebApp: opts.DefaultWebApp,
disableServiceDiscovery: opts.DisableServiceDiscovery, disableServiceDiscovery: opts.DisableServiceDiscovery,
} }
} }

View File

@ -76,13 +76,18 @@
.text-small { .text-small {
font-size: 0.8em; font-size: 0.8em;
} }
.mt {
margin-top: 1em;
display: block;
}
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="panel"> <div class="panel">
<h1>Arcast</h1> <h1>Arcast - Idle</h1>
<p>Instance ID:</p> <p>Instance ID:</p>
<p class="text-centered text-small"> <p class="text-centered text-small">
<code>{{ .ID }}</code> <code>{{ .ID }}</code>
@ -94,6 +99,15 @@
<li><code>{{ . }}:{{ $port }}</code></li> <li><code>{{ . }}:{{ $port }}</code></li>
{{end}} {{end}}
</ul> </ul>
{{if .WebApps }}
<p>Apps:</p>
<ul class="text-italic text-small">
{{ $port := .Port }}
{{range .IPs}}
<li><a href="http://{{ . }}:{{ $port }}/apps">http://{{ . }}:{{ $port }}/apps</a></li>
{{end}}
</ul>
{{end}}
</div> </div>
</div> </div>
</body> </body>