diff --git a/.env.dist b/.env.dist
index df10f3e..f8a5fca 100644
--- a/.env.dist
+++ b/.env.dist
@@ -1,2 +1,3 @@
ARCAST_DESKTOP_ADDITIONAL_CHROME_ARGS=
-ARCAST_DESKTOP_INSTANCE_ID=
\ No newline at end of file
+ARCAST_DESKTOP_INSTANCE_ID=
+ARCAST_DESKTOP_APPS=true
\ No newline at end of file
diff --git a/Makefile b/Makefile
index ca19aca..c0e2d4e 100644
--- a/Makefile
+++ b/Makefile
@@ -36,9 +36,14 @@ build-client: deps ## Build executable
build-android: tools/gogio/bin/gogio deps ## Build executable
mkdir -p dist
- GOOS=android CGO_CFLAGS="-I${JDK_PATH}/include -I${JDK_PATH}/include/linux -w" tools/gogio/bin/gogio -target android -buildmode archive -o android/app/libs/mobile.aar -x ./cmd/mobile
+ CGO_ENABLED=1 GOOS=android CGO_CFLAGS="-I${JDK_PATH}/include -I${JDK_PATH}/include/linux -w" tools/gogio/bin/gogio -target android -buildmode archive -o android/app/libs/mobile.aar -x ./cmd/mobile
( cd android && ./gradlew assembleDebug )
+release-android: tools/gogio/bin/gogio deps ## Build executable
+ mkdir -p dist
+ CGO_ENABLED=1 GOOS=android CGO_CFLAGS="-I${JDK_PATH}/include -I${JDK_PATH}/include/linux -w" tools/gogio/bin/gogio -target android -buildmode archive -o android/app/libs/mobile.aar -x ./cmd/mobile
+ ( cd android && ./gradlew assemble )
+
install-android: build-android
adb install android/app/build/outputs/apk/debug/app-debug.apk
adb shell monkey -p com.cadoles.arcast_player -c android.intent.category.LAUNCHER 1
diff --git a/apps/home/app.js b/apps/home/app.js
new file mode 100644
index 0000000..d164015
--- /dev/null
+++ b/apps/home/app.js
@@ -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 || app.hidden) 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);
+ });
diff --git a/apps/home/index.html b/apps/home/index.html
new file mode 100644
index 0000000..2671835
--- /dev/null
+++ b/apps/home/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
Screen sharing
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/screen-sharing/manifest.json b/apps/screen-sharing/manifest.json
new file mode 100644
index 0000000..a621713
--- /dev/null
+++ b/apps/screen-sharing/manifest.json
@@ -0,0 +1,10 @@
+{
+ "title": {
+ "fr": "Partage d'écran",
+ "en": "Screen sharing"
+ },
+ "description": {
+ "fr": "Partager son écran",
+ "en": "Share your screen"
+ }
+}
\ No newline at end of file
diff --git a/cmd/mobile/main.go b/cmd/mobile/main.go
index ca85e5a..ecbef83 100644
--- a/cmd/mobile/main.go
+++ b/cmd/mobile/main.go
@@ -8,6 +8,7 @@ import (
"os"
"sync"
+ "forge.cadoles.com/arcad/arcast"
"forge.cadoles.com/arcad/arcast/pkg/browser/gioui"
"forge.cadoles.com/arcad/arcast/pkg/server"
"gioui.org/app"
@@ -77,7 +78,13 @@ func main() {
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.WithAppsEnabled(true),
+ server.WithDefautApp("home"),
+ server.WithApps(arcast.DefaultApps...),
+ )
if err := server.Start(); err != nil {
logger.Fatal(ctx, "could not start server", logger.CapturedE(errors.WithStack(err)))
diff --git a/default_apps.go b/default_apps.go
new file mode 100644
index 0000000..fb9a7a6
--- /dev/null
+++ b/default_apps.go
@@ -0,0 +1,70 @@
+package arcast
+
+import (
+ "embed"
+ "encoding/json"
+ "io/fs"
+ "path/filepath"
+
+ "forge.cadoles.com/arcad/arcast/pkg/server"
+ "github.com/pkg/errors"
+)
+
+var (
+ DefaultApps []server.App
+ //go:embed apps/**
+ appsFS embed.FS
+)
+
+func init() {
+ defaultApps, err := loadApps("apps/*")
+ if err != nil {
+ panic(errors.WithStack(err))
+ }
+
+ DefaultApps = defaultApps
+}
+
+func loadApps(dirPattern string) ([]server.App, error) {
+ apps := make([]server.App, 0)
+
+ files, err := fs.Glob(appsFS, dirPattern)
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ for _, f := range files {
+ stat, err := fs.Stat(appsFS, f)
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ if !stat.IsDir() {
+ continue
+ }
+
+ rawManifest, err := fs.ReadFile(appsFS, filepath.Join(f, "manifest.json"))
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ var app server.App
+
+ if err := json.Unmarshal(rawManifest, &app); err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ app.ID = filepath.Base(f)
+
+ fs, err := fs.Sub(appsFS, "apps/"+app.ID)
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ app.FS = fs
+
+ apps = append(apps, app)
+ }
+
+ return apps, nil
+}
diff --git a/go.mod b/go.mod
index 69a75ec..e0e7791 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,9 @@ go 1.21.4
require (
gioui.org v0.4.1
+ github.com/davecgh/go-spew v1.1.1
github.com/gioui-plugins/gio-plugins v0.0.0-20230625001848-8f18aae6c91c
+ github.com/go-chi/cors v1.2.1
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
diff --git a/go.sum b/go.sum
index 49184b7..a4d6c9e 100644
--- a/go.sum
+++ b/go.sum
@@ -36,6 +36,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/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/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/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
diff --git a/internal/command/player/run.go b/internal/command/player/run.go
index f5c1eae..4351d37 100644
--- a/internal/command/player/run.go
+++ b/internal/command/player/run.go
@@ -4,6 +4,7 @@ import (
"fmt"
"os"
+ "forge.cadoles.com/arcad/arcast"
"forge.cadoles.com/arcad/arcast/pkg/browser/lorca"
"forge.cadoles.com/arcad/arcast/pkg/server"
"github.com/pkg/errors"
@@ -26,11 +27,21 @@ func Run() *cli.Command {
EnvVars: []string{"ARCAST_DESKTOP_INSTANCE_ID"},
Value: "",
},
+ &cli.StringFlag{
+ Name: "address",
+ EnvVars: []string{"ARCAST_DESKTOP_ADDRESS"},
+ Value: ":",
+ },
&cli.IntFlag{
Name: "window-height",
EnvVars: []string{"ARCAST_DESKTOP_WINDOW_HEIGHT"},
Value: defaults.Height,
},
+ &cli.BoolFlag{
+ Name: "apps",
+ EnvVars: []string{"ARCAST_DESKTOP_APPS"},
+ Value: false,
+ },
&cli.IntFlag{
Name: "window-width",
EnvVars: []string{"ARCAST_DESKTOP_WINDOW_WIDTH"},
@@ -41,6 +52,8 @@ func Run() *cli.Command {
windowHeight := ctx.Int("window-height")
windowWidth := ctx.Int("window-width")
chromeArgs := addFlagsPrefix(ctx.StringSlice("additional-chrome-arg")...)
+ enableApps := ctx.Bool("apps")
+ serverAddress := ctx.String("address")
browser := lorca.NewBrowser(
lorca.WithAdditionalChromeArgs(chromeArgs...),
@@ -69,7 +82,13 @@ func Run() *cli.Command {
instanceID = server.NewRandomInstanceID()
}
- server := server.New(browser, server.WithInstanceID(instanceID))
+ server := server.New(browser,
+ server.WithInstanceID(instanceID),
+ server.WithAppsEnabled(enableApps),
+ server.WithDefaultApp("home"),
+ server.WithApps(arcast.DefaultApps...),
+ server.WithAddress(serverAddress),
+ )
if err := server.Start(); err != nil {
return errors.Wrap(err, "could not start server")
diff --git a/modd.conf b/modd.conf
index 199c707..3062cfa 100644
--- a/modd.conf
+++ b/modd.conf
@@ -1,5 +1,6 @@
**/*.go
pkg/server/templates/**.gotmpl
+apps/**
modd.conf
.env {
prep: make build-client
diff --git a/pkg/server/apps.go b/pkg/server/apps.go
new file mode 100644
index 0000000..a455c98
--- /dev/null
+++ b/pkg/server/apps.go
@@ -0,0 +1,63 @@
+package server
+
+import (
+ "io/fs"
+ "net/http"
+ "strings"
+ "sync"
+
+ "github.com/go-chi/chi/v5"
+ "gitlab.com/wpetit/goweb/api"
+
+ _ "embed"
+)
+
+type App struct {
+ ID string `json:"id"`
+ Title map[string]string `json:"title"`
+ Description map[string]string `json:"description"`
+ Icon string `json:"icon"`
+ FS fs.FS `json:"-"`
+ Hidden bool `json:"hidden"`
+}
+
+func (s *Server) handleDefaultApp(w http.ResponseWriter, r *http.Request) {
+ http.Redirect(w, r, "/apps/"+s.defaultApp+"/", http.StatusTemporaryRedirect)
+}
+
+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.defaultApp,
+ Apps: s.apps,
+ })
+}
+
+var (
+ indexedAppFilesystems map[string]fs.FS
+ indexAppsOnce sync.Once
+)
+
+func (s *Server) handleAppFilesystem(w http.ResponseWriter, r *http.Request) {
+ indexAppsOnce.Do(func() {
+ indexedAppFilesystems = make(map[string]fs.FS, len(s.apps))
+ for _, app := range s.apps {
+ indexedAppFilesystems[app.ID] = app.FS
+ }
+ })
+
+ appID := chi.URLParam(r, "appID")
+
+ fs, exists := indexedAppFilesystems[appID]
+ if !exists {
+ http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
+
+ return
+ }
+
+ name := strings.TrimPrefix(r.URL.Path, "/apps/"+appID)
+
+ http.ServeFileFS(w, r, fs, name)
+}
diff --git a/pkg/server/http.go b/pkg/server/http.go
index 36ffdc0..96d26aa 100644
--- a/pkg/server/http.go
+++ b/pkg/server/http.go
@@ -9,6 +9,7 @@ import (
"strconv"
"github.com/go-chi/chi/v5"
+ "github.com/go-chi/cors"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
@@ -34,11 +35,36 @@ func init() {
func (s *Server) startHTTPServer(ctx context.Context) error {
router := chi.NewRouter()
+ if s.appsEnabled {
+ 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.Post("/api/v1/cast", s.handleCast)
router.Delete("/api/v1/cast", s.handleReset)
router.Get("/api/v1/status", s.handleStatus)
+ if s.appsEnabled {
+ router.Get("/apps", s.handleDefaultApp)
+ router.Get("/api/v1/apps", s.handleApps)
+ router.Handle("/apps/{appID}/*", http.HandlerFunc(s.handleAppFilesystem))
+ }
+
server := http.Server{
Addr: s.address,
Handler: router,
@@ -170,6 +196,7 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
IPs []string
Port int
ID string
+ Apps bool
}
ips, err := getLANIPv4Addrs()
@@ -183,6 +210,7 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
ID: s.instanceID,
IPs: ips,
Port: s.port,
+ Apps: s.appsEnabled,
}
if err := idleTemplate.Execute(w, d); err != nil {
diff --git a/pkg/server/options.go b/pkg/server/options.go
index 4e0f632..24eb6b6 100644
--- a/pkg/server/options.go
+++ b/pkg/server/options.go
@@ -17,18 +17,24 @@ func init() {
}
type Options struct {
- InstanceID string
- Address string
- DisableServiceDiscovery bool
+ InstanceID string
+ Address string
+ EnableServiceDiscovery bool
+ EnableApps bool
+ DefaultApp string
+ Apps []App
}
type OptionFunc func(opts *Options)
func NewOptions(funcs ...OptionFunc) *Options {
opts := &Options{
- InstanceID: NewRandomInstanceID(),
- Address: ":",
- DisableServiceDiscovery: false,
+ InstanceID: NewRandomInstanceID(),
+ Address: ":",
+ EnableServiceDiscovery: true,
+ EnableApps: false,
+ DefaultApp: "",
+ Apps: make([]App, 0),
}
for _, fn := range funcs {
@@ -38,6 +44,24 @@ func NewOptions(funcs ...OptionFunc) *Options {
return opts
}
+func WithAppsEnabled(enabled bool) OptionFunc {
+ return func(opts *Options) {
+ opts.EnableApps = enabled
+ }
+}
+
+func WithDefaultApp(defaultApp string) OptionFunc {
+ return func(opts *Options) {
+ opts.DefaultApp = defaultApp
+ }
+}
+
+func WithApps(apps ...App) OptionFunc {
+ return func(opts *Options) {
+ opts.Apps = apps
+ }
+}
+
func WithAddress(addr string) OptionFunc {
return func(opts *Options) {
opts.Address = addr
@@ -50,9 +74,9 @@ func WithInstanceID(id string) OptionFunc {
}
}
-func WithServiceDiscoveryDisabled(disabled bool) OptionFunc {
+func WithServiceDiscoveryEnabled(enabled bool) OptionFunc {
return func(opts *Options) {
- opts.DisableServiceDiscovery = disabled
+ opts.EnableServiceDiscovery = enabled
}
}
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 3ea403c..0d3a1a8 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -11,10 +11,15 @@ import (
type Server struct {
browser browser.Browser
- instanceID string
- address string
- port int
- disableServiceDiscovery bool
+ instanceID string
+ address string
+ port int
+
+ serviceDiscoveryEnabled bool
+
+ appsEnabled bool
+ defaultApp string
+ apps []App
ctx context.Context
cancel context.CancelFunc
@@ -32,7 +37,7 @@ func (s *Server) Start() error {
return errors.WithStack(err)
}
- if !s.disableServiceDiscovery {
+ if s.serviceDiscoveryEnabled {
mdnsServerCtx, cancelMDNSServer := context.WithCancel(serverCtx)
if err := s.startMDNServer(mdnsServerCtx); err != nil {
cancelHTTPServer()
@@ -71,6 +76,9 @@ func New(browser browser.Browser, funcs ...OptionFunc) *Server {
browser: browser,
instanceID: opts.InstanceID,
address: opts.Address,
- disableServiceDiscovery: opts.DisableServiceDiscovery,
+ appsEnabled: opts.EnableApps,
+ defaultApp: opts.DefaultApp,
+ apps: opts.Apps,
+ serviceDiscoveryEnabled: opts.EnableServiceDiscovery,
}
}
diff --git a/pkg/server/templates/idle.html.gotmpl b/pkg/server/templates/idle.html.gotmpl
index 3ea0931..929ead7 100644
--- a/pkg/server/templates/idle.html.gotmpl
+++ b/pkg/server/templates/idle.html.gotmpl
@@ -76,13 +76,18 @@
.text-small {
font-size: 0.8em;
}
+
+ .mt {
+ margin-top: 1em;
+ display: block;
+ }