Compare commits

4 Commits

Author SHA1 Message Date
977864073f feat: use goreleaser to generate linux packages 2024-04-26 15:09:11 +02:00
768393adc8 feat: allow homepage customization
Some checks reported warnings
arcad/arcast/pipeline/head This commit is unstable
2024-04-26 12:12:15 +02:00
35585959f5 feat(android): do not use sync.Mutex
Some checks reported warnings
arcad/arcast/pipeline/head This commit is unstable
2024-04-26 09:35:16 +02:00
0fcf0b6cc0 fix: typo
Some checks reported warnings
arcad/arcast/pipeline/head This commit is unstable
2024-04-25 16:06:06 +02:00
28 changed files with 558 additions and 325 deletions

98
.goreleaser.yml Normal file
View File

@ -0,0 +1,98 @@
project_name: arcast
before:
hooks:
- go mod tidy
builds:
- id: arcast-player
binary: arcast-player
env:
- CGO_ENABLED=0
ldflags:
- -s
- -w
- -X 'main.CommitRef={{ .Commit }}'
- -X 'main.Version={{ .Version }}'
gcflags:
- -trimpath="${PWD}"
asmflags:
- -trimpath="${PWD}"
goos:
- linux
goarch:
- amd64
- arm64
main: ./cmd/desktop
- id: arcast-client
binary: arcast-client
env:
- CGO_ENABLED=0
ldflags:
- -s
- -w
- -X 'main.CommitRef={{ .Commit }}'
- -X 'main.Version={{ .Version }}'
gcflags:
- -trimpath="${PWD}"
asmflags:
- -trimpath="${PWD}"
goos:
- linux
goarch:
- amd64
- arm64
main: ./cmd/client
archives:
- id: arcast-client
builds: ["arcast-client"]
name_template: '{{ .ProjectName }}-client_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
files:
- README.md
- id: arcast-player
builds: ["arcast-player"]
name_template: '{{ .ProjectName }}-player_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
files:
- README.md
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ .Version }}"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
nfpms:
- id: arcast-player
builds:
- "arcast-player"
package_name: arcast-player
homepage: https://forge.cadoles.com/arcad/arcast
maintainer: Cadoles <contact@cadoles.com>
description: |-
Web diffusion player
license: AGPL-3.0
formats:
- apk
- deb
dependencies:
- chromium | chromium-browser | google-chrome-stable
contents:
- src: misc/packaging/player.desktop
dst: /usr/share/applications/arcast-player.desktop
type: config
- src: misc/logo/icon.png
dst: /usr/share/pixmaps/arcast-player.png
type: config
- id: arcast-client
builds:
- "arcast-client"
package_name: arcast-client
homepage: https://forge.cadoles.com/arcad/arcast
maintainer: Cadoles <contact@cadoles.com>
description: |-
Arcast player command-line client
license: AGPL-3.0
formats:
- apk
- deb

View File

@ -1,6 +1,6 @@
LINT_ARGS ?= --timeout 5m
GORELEASER_VERSION ?= v1.13.1
GORELEASER_ARGS ?= release --snapshot --rm-dist
GORELEASER_VERSION ?= v1.25.1
GORELEASER_ARGS ?= release --snapshot --clean
GITCHLOG_ARGS ?=
SHELL := /bin/bash
JDK_PATH ?= /usr/lib/jvm/java-11-openjdk
@ -51,7 +51,7 @@ build-client: deps ## Build executable
build-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 assembleDebug )
( cd android && VERSION_NAME=$(MKT_PROJECT_VERSION)-debug ./gradlew assembleDebug )
$(ANDROID_KEYSTORE_FILE):
keytool -genkey -noprompt \
@ -65,7 +65,7 @@ $(ANDROID_KEYSTORE_FILE):
release-android: $(ANDROID_KEYSTORE_FILE) 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 )
( cd android && VERSION_NAME=$(MKT_PROJECT_VERSION) ./gradlew assemble )
rm -f android/app/build/outputs/apk/release/app-release-unsigned-aligned.apk
"$(ANDROID_HOME)/build-tools/$(ANDROID_BUILD_TOOLS_VERSION)/zipalign" -p 4 android/app/build/outputs/apk/release/app-release-unsigned.apk android/app/build/outputs/apk/release/app-release-unsigned-aligned.apk
"$(ANDROID_HOME)/build-tools/$(ANDROID_BUILD_TOOLS_VERSION)/apksigner" \
@ -105,23 +105,16 @@ tools/gogio/bin/gogio:
mkdir -p tools/gogio/bin
GOBIN=$(PWD)/tools/gogio/bin go install gioui.org/cmd/gogio@latest
release: gitea-release
gitea-release: .mktools build release-android
gitea-release: .mktools goreleaser release-android
rm -rf .gitea-release
mkdir -p .gitea-release
cp ./bin/desktop_amd64 .gitea-release/arcad_player_linux_amd64
$(MAKE) GOARCH=arm build-desktop
cp ./bin/desktop_arm .gitea-release/arcad_player_linux_arm
$(MAKE) GOARCH=arm64 build-desktop
cp ./bin/desktop_arm64 .gitea-release/arcad_player_linux_arm64
cp ./bin/client .gitea-release/arcad_client_linux_amd64
cp ./android/app/build/outputs/apk/release/app-release.apk .gitea-release/arcast_player.apk
cp ./dist/*.apk .gitea-release/
cp ./dist/*.deb .gitea-release/
cp ./dist/*.tar.gz .gitea-release/
cp ./android/app/build/outputs/apk/release/app-release.apk .gitea-release/arcast_player_$(MKT_PROJECT_VERSION).apk
MKT_GITEA_RELEASE_PROJECT="arcast" \
MKT_GITEA_RELEASE_ORG="arcad" \
@ -135,6 +128,9 @@ gitea-release: .mktools build release-android
MKT_GITEA_RELEASE_ATTACHMENTS="$$(find .gitea-release/* -type f)" \
$(MAKE) mkt-gitea-release
.PHONY: goreleaser
goreleaser: .env .mktools
( set -o allexport && source .env && set +o allexport && curl -sfL https://goreleaser.com/static/run | VERSION=$(GORELEASER_VERSION) GORELEASER_CURRENT_TAG="$(MKT_PROJECT_VERSION)" bash /dev/stdin $(GORELEASER_ARGS) )
.PHONY: mktools
mktools:

View File

@ -16,7 +16,7 @@ Le client en ligne de commande vous permet de contrôler votre flotte de serveur
#### Dernière version
- Linux: [`amd64`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcad_client_linux_amd64)
- Linux: [`amd64`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcast_client_linux_amd64)
### Application Android
@ -36,7 +36,7 @@ Avoir [Chromium](https://www.chromium.org/chromium-projects/) (ou `Google Chrome
#### Dernière version
- Linux: [`amd64`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcad_player_linux_amd64), [`arm`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcad_player_linux_arm), [`arm64`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcad_player_linux_arm64)
- Linux: [`amd64`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcast_player_linux_amd64), [`arm`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcast_player_linux_arm), [`arm64`](https://forge.cadoles.com/arcad/arcast/releases/download/latest/arcast_player_linux_arm64)
## Documentation

View File

@ -3,6 +3,16 @@ plugins {
id 'org.jetbrains.kotlin.android'
}
def gitVersion() {
def counter = 0
def process = "git rev-list master --first-parent --count".execute()
return process.text.toInteger()
}
def versionName() {
return System.env.VERSION_NAME ? System.env.VERSION_NAME : "0.0.0"
}
android {
namespace 'com.cadoles.arcast_player'
compileSdk 34
@ -11,8 +21,8 @@ android {
applicationId "com.cadoles.arcast_player"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
versionCode gitVersion()
versionName versionName()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@ -47,7 +57,6 @@ android {
}
dependencies {
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
implementation 'androidx.activity:activity-compose:1.7.0'

View File

@ -5,10 +5,13 @@ import (
"forge.cadoles.com/arcad/arcast/internal/command/client"
)
var Version = "dev"
func main() {
command.Main(
"arcast",
"Arcast cli client",
"cli client",
Version,
client.Root().Subcommands...,
)
}

View File

@ -5,10 +5,13 @@ import (
"forge.cadoles.com/arcad/arcast/internal/command/player"
)
var Version = "dev"
func main() {
command.Main(
"arcast",
"Arcast desktop player",
"desktop player",
Version,
player.Root().Subcommands...,
)
}

View File

@ -15,7 +15,6 @@ import (
"gioui.org/layout"
"gioui.org/op"
"github.com/gioui-plugins/gio-plugins/plugin"
"github.com/gioui-plugins/gio-plugins/webviewer/webview"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
@ -25,9 +24,9 @@ const packageName = "com.cadoles.arcast_player"
func main() {
ctx := context.Background()
webview.SetDebug(true)
window := app.NewWindow(
app.Fullscreen.Option(),
app.AnyOrientation.Option(),
)
browser := gioui.NewBrowser(window)
@ -37,7 +36,6 @@ func main() {
for {
evt := window.NextEvent()
plugin.Install(window, evt)
switch evt := evt.(type) {
case system.DestroyEvent:
os.Exit(0)
@ -45,7 +43,7 @@ func main() {
case system.FrameEvent:
gtx := layout.NewContext(ops, evt)
browser.Layout(gtx)
evt.Frame(gtx.Ops)
evt.Frame(ops)
}
}
}()

View File

@ -19,7 +19,10 @@ Voici un exemple commenté du fichier de configuration:
"http": {
// Couple <address>:<port> d'écoute
// Par défaut ":" i.e. toutes les adresses avec port aléatoire
"address": ":"
"address": ":",
// Répertoire de personnalisation de la page d'accueil
// Voir section "Personnalisation" ci-dessous
"customDir": "${CONFIG_DIR}/custom"
},
// Configuration du serveur HTTPS
"https": {
@ -48,3 +51,9 @@ Voici un exemple commenté du fichier de configuration:
}
}
```
## Personnalisation
Il est possible de personnaliser la page d'accueil du player Arcast en créant des fichiers dans le répertoire définit par l'attribut de configuration `http.customDir`.
Le contenu de ce répertoire doit répliquer l'arborescence embarquée par défaut (voir https://forge.cadoles.com/arcad/arcast/src/branch/develop/pkg/server/embed). Chaque fichier présent remplacera celui embarqué par défaut.

5
go.mod
View File

@ -4,8 +4,8 @@ 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/dschmidt/go-layerfs v0.1.0
github.com/gioui-plugins/gio-plugins v0.0.0-20240323070753-3331d8c2df5d
github.com/go-chi/cors v1.2.1
github.com/gorilla/websocket v1.5.1
github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1
@ -28,7 +28,6 @@ require (
github.com/go-playground/universal-translator v0.18.0 // 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/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/jedib0t/go-pretty/v6 v6.4.9 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect

11
go.sum
View File

@ -32,8 +32,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gioui-plugins/gio-plugins v0.0.0-20230625001848-8f18aae6c91c h1:naFDaf0CvDEYZ3Zpxx20DY/cCvBQqKwsV7ZzBt3M/bU=
github.com/gioui-plugins/gio-plugins v0.0.0-20230625001848-8f18aae6c91c/go.mod h1:nBuRsi6udr2x6eorarLHtRkoRaWBICt+WzaE7zQXgYY=
github.com/dschmidt/go-layerfs v0.1.0 h1:jE6aHDfjNzS/31DS48th6EkmELwTa1Uf+aO4jRkBs3U=
github.com/dschmidt/go-layerfs v0.1.0/go.mod h1:m62aff0hn23Q/tQBRiNSeLD7EUuimDvsuCvCpzBr3Gw=
github.com/gioui-plugins/gio-plugins v0.0.0-20240323070753-3331d8c2df5d h1:8b7owUJ8sNmgqEk+1d7ylr3TCH3vliCvY/6ycfize8o=
github.com/gioui-plugins/gio-plugins v0.0.0-20240323070753-3331d8c2df5d/go.mod h1:3XVleuCdPpdajFL+ASh2wmXZNskitXQQ4jhVss0VHZg=
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=
@ -62,8 +64,6 @@ github.com/inkeliz/go_inkwasm v0.0.0-20220912074516-049d3472c98a h1:uZklbtdSPrDL
github.com/inkeliz/go_inkwasm v0.0.0-20220912074516-049d3472c98a/go.mod h1:LPI3Qojj7OgTyc2R4RPB6BuMSgjoOXCObwnDzz1SOVk=
github.com/jaevor/go-nanoid v1.3.0 h1:nD+iepesZS6pr3uOVf20vR9GdGgJW1HPaR46gtrxzkg=
github.com/jaevor/go-nanoid v1.3.0/go.mod h1:SI+jFaPuddYkqkVQoNGHs81navCtH388TcrH0RqFKgY=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/jedib0t/go-pretty/v6 v6.4.9 h1:vZ6bjGg2eBSrJn365qlxGcaWu09Id+LHtrfDWlB2Usc=
github.com/jedib0t/go-pretty/v6 v6.4.9/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
@ -88,6 +88,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/psanford/memfs v0.0.0-20210214183328-a001468d78ef h1:NKxTG6GVGbfMXc2mIk+KphcH6hagbVXhcFkbTgYleTI=
github.com/psanford/memfs v0.0.0-20210214183328-a001468d78ef/go.mod h1:tcaRap0jS3eifrEEllL6ZMd9dg8IlDpi2S1oARrQ+NI=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
@ -144,7 +146,6 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -10,11 +10,12 @@ import (
"gitlab.com/wpetit/goweb/logger"
)
func Main(name string, usage string, commands ...*cli.Command) {
func Main(name string, usage string, version string, commands ...*cli.Command) {
app := &cli.App{
Name: name,
Usage: usage,
Commands: commands,
Version: version,
Before: func(ctx *cli.Context) error {
workdir := ctx.String("workdir")
// Switch to new working directory if defined

View File

@ -68,6 +68,11 @@ func Run() *cli.Command {
EnvVars: []string{"ARCAST_DESKTOP_ALLOWED_ORIGINS"},
Value: cli.NewStringSlice(),
},
&cli.StringFlag{
Name: "custom-files-dir",
EnvVars: []string{"ARCAST_DESKTOP_CUSTOM_FILES_DIR"},
Value: "",
},
&cli.BoolFlag{
Name: "dummy-browser",
EnvVars: []string{"ARCAST_DESKTOP_DUMMY_BROWSER"},
@ -114,21 +119,11 @@ func Run() *cli.Command {
conf := config.DefaultConfig()
logger.Info(ctx.Context, "loading or creating configuration file", logger.F("filename", configFile))
if err := config.LoadOrCreate(ctx.Context, configFile, conf, config.DefaultTransforms...); err != nil {
logger.Error(ctx.Context, "could not load configuration file", logger.CapturedE(errors.WithStack(err)))
}
instanceID := ctx.String("instance-id")
if instanceID != "" {
conf.InstanceID = instanceID
}
cert, err := tls.X509KeyPair(conf.HTTPS.Cert, conf.HTTPS.Key)
if err != nil {
return errors.Wrap(err, "could not parse tls cert/key pair")
}
if ctx.IsSet("apps") {
conf.Apps.Enabled = ctx.Bool("apps")
}
@ -145,6 +140,20 @@ func Run() *cli.Command {
conf.AllowedOrigins = ctx.StringSlice("allowed-origins")
}
if ctx.IsSet("custom-dir") {
conf.HTTP.CustomDir = ctx.String("custom-dir")
}
logger.Info(ctx.Context, "loading or creating configuration file", logger.F("filename", configFile))
if err := config.LoadOrCreate(ctx.Context, configFile, conf, config.DefaultTransforms...); err != nil {
logger.Error(ctx.Context, "could not load configuration file", logger.CapturedE(errors.WithStack(err)))
}
cert, err := tls.X509KeyPair(conf.HTTPS.Cert, conf.HTTPS.Key)
if err != nil {
return errors.Wrap(err, "could not parse tls cert/key pair")
}
server := server.New(browser,
server.WithInstanceID(conf.InstanceID),
server.WithAppsEnabled(conf.Apps.Enabled),
@ -154,6 +163,7 @@ func Run() *cli.Command {
server.WithTLSAddress(conf.HTTPS.Address),
server.WithTLSCertificate(&cert),
server.WithAllowedOrigins(conf.AllowedOrigins...),
server.WithUpperLayerDir(conf.HTTP.CustomDir),
)
if err := server.Start(); err != nil {

View File

@ -0,0 +1,7 @@
[Desktop Entry]
Type=Application
Version=1.0
Name=Arcast Player
Comment=Web diffusion server
Path=/usr/bin/arcast-player
Icon=arcast-player

View File

@ -3,7 +3,7 @@
}
**/*.go
pkg/server/templates/**.gotmpl
pkg/server/embed/**
modd.conf
.env {
prep: make build-client

View File

@ -2,7 +2,7 @@ package gioui
import (
"context"
"sync"
"sync/atomic"
"forge.cadoles.com/arcad/arcast/pkg/browser"
"gioui.org/app"
@ -16,26 +16,20 @@ type Browser struct {
window *app.Window
tag int
url string
changed bool
status browser.Status
title string
mutex sync.Mutex
url *atomic.Value
changed *atomic.Bool
status *atomic.Value
title *atomic.Value
}
func (b *Browser) Layout(gtx layout.Context) {
b.mutex.Lock()
defer b.mutex.Unlock()
func (b *Browser) Layout(gtx layout.Context) layout.Dimensions {
events := gtx.Events(&b.tag)
for _, evt := range events {
switch ev := evt.(type) {
case webviewer.TitleEvent:
b.title = ev.Title
b.title.Store(ev.Title)
case webviewer.NavigationEvent:
b.url = ev.URL
b.url.Store(ev.URL)
}
}
@ -58,21 +52,20 @@ func (b *Browser) Layout(gtx layout.Context) {
},
}.Add(gtx.Ops)
if b.changed {
logger.Debug(ctx, "url changed", logger.F("url", b.url))
webviewer.NavigateOp{URL: b.url}.Add(gtx.Ops)
b.changed = false
if b.changed.CompareAndSwap(true, false) {
url := b.url.Load().(string)
logger.Debug(ctx, "url changed", logger.F("url", url))
webviewer.NavigateOp{URL: url}.Add(gtx.Ops)
}
return layout.Dimensions{Size: gtx.Constraints.Max}
}
// Load implements browser.Browser.
func (b *Browser) Load(url string) error {
b.mutex.Lock()
defer b.mutex.Unlock()
b.url = url
b.changed = true
b.status = browser.StatusCasting
b.url.Store(url)
b.changed.Store(true)
b.status.Store(browser.StatusCasting)
b.window.Invalidate()
@ -81,36 +74,24 @@ func (b *Browser) Load(url string) error {
// Status implements browser.Browser.
func (b *Browser) Status() (browser.Status, error) {
b.mutex.Lock()
defer b.mutex.Unlock()
return b.status, nil
return b.status.Load().(browser.Status), nil
}
// Title implements browser.Browser.
func (b *Browser) Title() (string, error) {
b.mutex.Lock()
defer b.mutex.Unlock()
return b.title, nil
return b.title.Load().(string), nil
}
// URL implements browser.Browser.
func (b *Browser) URL() (string, error) {
b.mutex.Lock()
defer b.mutex.Unlock()
return b.url, nil
return b.url.Load().(string), nil
}
// Reset implements browser.Browser.
func (b *Browser) Reset(url string) error {
b.mutex.Lock()
defer b.mutex.Unlock()
b.url = url
b.changed = true
b.status = browser.StatusIdle
b.url.Store(url)
b.changed.Store(true)
b.status.Store(browser.StatusIdle)
b.window.Invalidate()
@ -118,12 +99,20 @@ func (b *Browser) Reset(url string) error {
}
func NewBrowser(window *app.Window) *Browser {
return &Browser{
b := &Browser{
window: window,
url: "",
changed: true,
status: browser.StatusIdle,
url: &atomic.Value{},
changed: &atomic.Bool{},
status: &atomic.Value{},
title: &atomic.Value{},
}
b.url.Store("")
b.title.Store("")
b.changed.Store(false)
b.status.Store(browser.StatusIdle)
return b
}
var _ browser.Browser = &Browser{}

View File

@ -20,7 +20,8 @@ type Config struct {
}
type HTTPConfig struct {
Address string `json:"address"`
Address string `json:"address"`
CustomDir string `json:"customDir"`
}
type HTTPSConfig struct {
@ -40,7 +41,7 @@ type AppsConfig struct {
DefaultApp string `json:"defaultApp"`
}
type TransformFunc func(ctx context.Context, conf *Config) error
type TransformFunc func(ctx context.Context, filename string, conf *Config) error
func DefaultConfigFile(ctx context.Context) string {
configDir, err := os.UserConfigDir()
@ -69,7 +70,7 @@ func LoadOrCreate(ctx context.Context, filename string, conf *Config, funcs ...T
}
for _, fn := range funcs {
if err := fn(ctx, conf); err != nil {
if err := fn(ctx, filename, conf); err != nil {
return errors.WithStack(err)
}
}
@ -99,7 +100,8 @@ func DefaultConfig() *Config {
InstanceID: server.NewRandomInstanceID(),
AllowedOrigins: []string{},
HTTP: HTTPConfig{
Address: ":45555",
Address: ":45555",
CustomDir: "",
},
HTTPS: HTTPSConfig{
Address: ":45556",

View File

@ -0,0 +1,28 @@
package config
import (
"context"
"os"
"path/filepath"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func CreateCustomDir(ctx context.Context, filename string, conf *Config) error {
if conf.HTTP.CustomDir != "" {
return nil
}
configDir := filepath.Dir(filename)
customFilesDir := filepath.Join(configDir, "custom")
if err := os.MkdirAll(customFilesDir, 0755); err != nil {
logger.Error(ctx, "could not create custom files directory", logger.CapturedE(errors.WithStack(err)))
return nil
}
conf.HTTP.CustomDir = customFilesDir
return nil
}

View File

@ -3,4 +3,5 @@ package config
var DefaultTransforms = []TransformFunc{
GenerateSelfSignedCert,
RenewExpiredSelfSignedCert,
CreateCustomDir,
}

View File

@ -15,7 +15,7 @@ import (
"gitlab.com/wpetit/goweb/logger"
)
func GenerateSelfSignedCert(ctx context.Context, conf *Config) error {
func GenerateSelfSignedCert(ctx context.Context, filename string, conf *Config) error {
if !conf.HTTPS.SelfSigned.Enabled {
return nil
}
@ -42,13 +42,13 @@ func GenerateSelfSignedCert(ctx context.Context, conf *Config) error {
return nil
}
func RenewExpiredSelfSignedCert(ctx context.Context, conf *Config) error {
func RenewExpiredSelfSignedCert(ctx context.Context, filename string, conf *Config) error {
if !conf.HTTPS.SelfSigned.Enabled {
return nil
}
if conf.HTTPS.Cert == nil || conf.HTTPS.Key == nil {
if err := GenerateSelfSignedCert(ctx, conf); err != nil {
if err := GenerateSelfSignedCert(ctx, filename, conf); err != nil {
return errors.WithStack(err)
}
}
@ -62,7 +62,7 @@ func RenewExpiredSelfSignedCert(ctx context.Context, conf *Config) error {
if err != nil {
logger.Error(ctx, "could not parse x509 certificate, regenerating one", logger.CapturedE(errors.WithStack(err)))
if err := GenerateSelfSignedCert(ctx, conf); err != nil {
if err := GenerateSelfSignedCert(ctx, filename, conf); err != nil {
return errors.WithStack(err)
}
}
@ -74,7 +74,7 @@ func RenewExpiredSelfSignedCert(ctx context.Context, conf *Config) error {
logger.Warn(ctx, "self-signed certificate has expired, regenerating one", logger.CapturedE(errors.WithStack(err)))
if err := GenerateSelfSignedCert(ctx, conf); err != nil {
if err := GenerateSelfSignedCert(ctx, filename, conf); err != nil {
return errors.WithStack(err)
}

View File

@ -0,0 +1,64 @@
{{ define "base" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/logo.png" />
<title>Ready to cast !</title>
<link rel="stylesheet" href="style.css" />
{{ block "head" .
}}{{
end
}}
</head>
<body>
<div class="container">
<div class="panel">
<div id="icon"></div>
{{ block "message" . }}
<h2 id="title" class="text-centered">Ready to cast !</h2>
{{ end }}
{{ block "info" . }}
<p><b>Instance ID</b></p>
<p class="text-small text-centered">
<code>{{ .ID }}</code>
</p>
<p><b>Addresses</b></p>
<ul class="text-italic text-small text-centered">
{{ $port := .Port }}
{{
range.IPs
}}
<li>
<code>{{ . }}:{{ $port }}</code>
</li>
{{
end
}}
</ul>
{{ end }}
{{if .Apps }}
{{ block "apps" . }}
<p><b>Apps</b></p>
<ul class="text-italic text-small text-centered">
{{ $tlsPort := .TLSPort }}
{{
range.IPs
}}
<li>
<a href="https://{{ . }}:{{ $tlsPort }}/apps">
https://{{ . }}:{{ $tlsPort }}/apps
</a>
</li>
{{
end
}}
</ul>
{{ end }}
{{ end }}
</div>
</div>
</body>
</html>
{{ end }}

View File

@ -0,0 +1,3 @@
{{ define "index" }}
{{ template "base" . }}
{{ end }}

BIN
pkg/server/embed/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

121
pkg/server/embed/style.css Normal file
View File

@ -0,0 +1,121 @@
html {
box-sizing: border-box;
font-size: 16px;
width: 100%;
height: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
width: 100%;
height: 100%;
background: rgb(76, 96, 188);
background: linear-gradient(
415deg,
rgba(4, 168, 243, 1),
rgb(76, 136, 188, 1),
rgba(76, 96, 188, 1),
rgb(115, 76, 188, 1),
rgb(87, 76, 188, 1)
);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
*,
*: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;
}
.panel {
display: block;
background-color: #fff;
border-radius: 15px;
box-shadow: 10px 10px 10px #33333361;
position: relative;
padding: 50px 30px 30px 30px;
min-width: 50%;
color: #333;
}
#title {
margin: 10px 0px 20px 0px;
}
#icon {
width: 100px;
aspect-ratio: 1/1;
background-size: contain;
background-position: center center;
background-image: url("logo.png");
position: absolute;
left: 50%;
margin-left: -50px;
margin-top: -100px;
background-repeat: no-repeat;
}
.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;
}

90
pkg/server/fs.go Normal file
View File

@ -0,0 +1,90 @@
package server
import (
"embed"
"html/template"
"io/fs"
"net/http"
"os"
"strings"
"forge.cadoles.com/arcad/arcast/pkg/network"
"github.com/dschmidt/go-layerfs"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
_ "embed"
)
var (
//go:embed embed/**
embedFS embed.FS
)
func (s *Server) initLayeredFS() error {
layers := make([]fs.FS, 0)
if s.upperLayerDir != "" {
upperLayer := os.DirFS(s.upperLayerDir)
layers = append(layers, upperLayer)
}
baseLayer, err := fs.Sub(embedFS, "embed")
if err != nil {
return errors.WithStack(err)
}
layers = append(layers, baseLayer)
s.layeredFS = layerfs.New(layers...)
return nil
}
func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if strings.HasPrefix(path, "/_templates") {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
http.ServeFileFS(w, r, s.layeredFS, path)
}
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
type templateData struct {
IPs []string
Port int
TLSPort int
ID string
Apps bool
}
ips, err := network.GetLANIPv4Addrs()
if err != nil {
logger.Error(r.Context(), "could not retrieve lan ip addresses", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
d := templateData{
ID: s.instanceID,
IPs: ips,
Port: s.port,
TLSPort: s.tlsPort,
Apps: s.appsEnabled,
}
templates, err := template.New("").ParseFS(s.layeredFS, "_partials/*.gohtml", "_templates/*.gohtml")
if err != nil {
logger.Error(r.Context(), "could not parse template", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if err := templates.ExecuteTemplate(w, "index", d); err != nil {
logger.Error(r.Context(), "could not render index page", logger.CapturedE(errors.WithStack(err)))
}
}

View File

@ -4,7 +4,6 @@ import (
"context"
"crypto/tls"
"fmt"
"html/template"
"net"
"net/http"
"strconv"
@ -14,25 +13,8 @@ import (
"github.com/go-chi/cors"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
_ "embed"
)
var (
//go:embed templates/idle.html.gotmpl
rawIdleTemplate []byte
idleTemplate *template.Template
)
func init() {
tmpl, err := template.New("").Parse(string(rawIdleTemplate))
if err != nil {
panic(errors.Wrap(err, "could not parse idle template"))
}
idleTemplate = tmpl
}
func (s *Server) startWebServers(ctx context.Context) error {
router := chi.NewRouter()
@ -50,7 +32,6 @@ func (s *Server) startWebServers(ctx context.Context) error {
}))
}
router.Get("/", s.handleHome)
router.Get("/api/v1/info", s.handleInfo)
router.Post("/api/v1/cast", s.handleCast)
router.Delete("/api/v1/cast", s.handleReset)
@ -63,6 +44,9 @@ func (s *Server) startWebServers(ctx context.Context) error {
router.Handle("/api/v1/broadcast/{channelID}", http.HandlerFunc(s.handleBroadcast))
}
router.Get("/", s.handleIndex)
router.Get("/*", s.handleStatic)
if err := s.startHTTPServer(ctx, router); err != nil {
return errors.WithStack(err)
}
@ -193,32 +177,3 @@ func (s *Server) getAllowedOrigins() ([]string, error) {
return allowedOrigins, nil
}
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
type templateData struct {
IPs []string
Port int
TLSPort int
ID string
Apps bool
}
ips, err := network.GetLANIPv4Addrs()
if err != nil {
logger.Error(r.Context(), "could not retrieve lan ip addresses", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
d := templateData{
ID: s.instanceID,
IPs: ips,
Port: s.port,
TLSPort: s.tlsPort,
Apps: s.appsEnabled,
}
if err := idleTemplate.Execute(w, d); err != nil {
logger.Error(r.Context(), "could not render idle page", logger.CapturedE(errors.WithStack(err)))
}
}

View File

@ -28,6 +28,7 @@ type Options struct {
DefaultApp string
AllowedOrigins []string
Apps []App
UpperLayerDir string
}
type OptionFunc func(opts *Options)
@ -42,6 +43,7 @@ func NewOptions(funcs ...OptionFunc) *Options {
DefaultApp: "",
AllowedOrigins: make([]string, 0),
Apps: make([]App, 0),
UpperLayerDir: "",
}
for _, fn := range funcs {
@ -105,6 +107,12 @@ func WithServiceDiscoveryEnabled(enabled bool) OptionFunc {
}
}
func WithUpperLayerDir(dir string) OptionFunc {
return func(opts *Options) {
opts.UpperLayerDir = dir
}
}
func NewRandomInstanceID() string {
return newRandomInstanceID()
}

View File

@ -3,6 +3,7 @@ package server
import (
"context"
"crypto/tls"
"io/fs"
"forge.cadoles.com/arcad/arcast/pkg/browser"
"github.com/gorilla/websocket"
@ -32,10 +33,19 @@ type Server struct {
ctx context.Context
cancel context.CancelFunc
upgrader websocket.Upgrader
layeredFS fs.FS
upperLayerDir string
}
func (s *Server) Start() error {
serverCtx, cancelServer := context.WithCancel(context.Background())
ctx := context.Background()
if err := s.initLayeredFS(); err != nil {
return errors.WithStack(err)
}
serverCtx, cancelServer := context.WithCancel(ctx)
s.cancel = cancelServer
s.ctx = serverCtx
@ -92,6 +102,7 @@ func New(browser browser.Browser, funcs ...OptionFunc) *Server {
defaultApp: opts.DefaultApp,
apps: opts.Apps,
serviceDiscoveryEnabled: opts.EnableServiceDiscovery,
upperLayerDir: opts.UpperLayerDir,
}
server.upgrader = websocket.Upgrader{

File diff suppressed because one or more lines are too long