Added websocket control of lower thirds to improve ease of use.

This commit is contained in:
Patrick Fairbank
2015-08-22 18:27:31 -07:00
parent 6ff087546d
commit bd098de716
9 changed files with 273 additions and 72 deletions

View File

@@ -2,7 +2,8 @@
CREATE TABLE lower_thirds (
id INTEGER PRIMARY KEY,
toptext VARCHAR(255),
bottomtext VARCHAR(255)
bottomtext VARCHAR(255),
displayorder int
);
-- +goose Down

View File

@@ -6,9 +6,10 @@
package main
type LowerThird struct {
Id int
TopText string
BottomText string
Id int
TopText string
BottomText string
DisplayOrder int
}
func (database *Database) CreateLowerThird(lowerThird *LowerThird) error {
@@ -41,6 +42,6 @@ func (database *Database) TruncateLowerThirds() error {
func (database *Database) GetAllLowerThirds() ([]LowerThird, error) {
var lowerThirds []LowerThird
err := database.teamMap.Select(&lowerThirds, "SELECT * FROM lower_thirds ORDER BY id")
err := database.lowerThirdMap.Select(&lowerThirds, "SELECT * FROM lower_thirds ORDER BY displayorder")
return lowerThirds, err
}

View File

@@ -27,7 +27,7 @@ func TestLowerThirdCrud(t *testing.T) {
assert.Nil(t, err)
defer db.Close()
lowerThird := LowerThird{0, "Top Text", "Bottom Text"}
lowerThird := LowerThird{0, "Top Text", "Bottom Text", 0}
db.CreateLowerThird(&lowerThird)
lowerThird2, err := db.GetLowerThirdById(1)
assert.Nil(t, err)
@@ -52,7 +52,7 @@ func TestTruncateLowerThirds(t *testing.T) {
assert.Nil(t, err)
defer db.Close()
lowerThird := LowerThird{0, "Top Text", "Bottom Text"}
lowerThird := LowerThird{0, "Top Text", "Bottom Text", 0}
db.CreateLowerThird(&lowerThird)
db.TruncateLowerThirds()
lowerThird2, err := db.GetLowerThirdById(1)

View File

@@ -6,9 +6,12 @@
package main
import (
"fmt"
"github.com/mitchellh/mapstructure"
"html/template"
"io"
"log"
"net/http"
"strconv"
)
// Shows the lower third configuration page.
@@ -34,45 +37,161 @@ func LowerThirdsGetHandler(w http.ResponseWriter, r *http.Request) {
}
}
// Saves the new or modified lower third to the database and triggers showing it on the audience display.
func LowerThirdsPostHandler(w http.ResponseWriter, r *http.Request) {
lowerThirdId, _ := strconv.Atoi(r.PostFormValue("id"))
lowerThird, err := db.GetLowerThirdById(lowerThirdId)
// The websocket endpoint for the lower thirds client to send control commands.
func LowerThirdsWebsocketHandler(w http.ResponseWriter, r *http.Request) {
websocket, err := NewWebsocket(w, r)
if err != nil {
handleWebErr(w, err)
return
}
if r.PostFormValue("action") == "delete" {
err := db.DeleteLowerThird(lowerThird)
defer websocket.Close()
// Loop, waiting for commands and responding to them, until the client closes the connection.
for {
messageType, data, err := websocket.Read()
if err != nil {
handleWebErr(w, err)
return
}
} else {
// Save the lower third even if the show or hide buttons were clicked.
if lowerThird == nil {
lowerThird = &LowerThird{TopText: r.PostFormValue("topText"),
BottomText: r.PostFormValue("bottomText")}
err = db.CreateLowerThird(lowerThird)
} else {
lowerThird.TopText = r.PostFormValue("topText")
lowerThird.BottomText = r.PostFormValue("bottomText")
err = db.SaveLowerThird(lowerThird)
}
if err != nil {
handleWebErr(w, err)
if err == io.EOF {
// Client has closed the connection; nothing to do here.
return
}
log.Printf("Websocket error: %s", err)
return
}
if r.PostFormValue("action") == "show" {
switch messageType {
case "saveLowerThird":
var lowerThird LowerThird
err = mapstructure.Decode(data, &lowerThird)
if err != nil {
websocket.WriteError(err.Error())
continue
}
saveLowerThird(&lowerThird)
case "deleteLowerThird":
var lowerThird LowerThird
err = mapstructure.Decode(data, &lowerThird)
if err != nil {
websocket.WriteError(err.Error())
continue
}
err = db.DeleteLowerThird(&lowerThird)
if err != nil {
websocket.WriteError(err.Error())
continue
}
case "showLowerThird":
var lowerThird LowerThird
err = mapstructure.Decode(data, &lowerThird)
if err != nil {
websocket.WriteError(err.Error())
continue
}
saveLowerThird(&lowerThird)
mainArena.lowerThirdNotifier.Notify(lowerThird)
mainArena.audienceDisplayScreen = "lowerThird"
mainArena.audienceDisplayNotifier.Notify(nil)
} else if r.PostFormValue("action") == "hide" {
continue
case "hideLowerThird":
var lowerThird LowerThird
err = mapstructure.Decode(data, &lowerThird)
if err != nil {
websocket.WriteError(err.Error())
continue
}
saveLowerThird(&lowerThird)
mainArena.audienceDisplayScreen = "blank"
mainArena.audienceDisplayNotifier.Notify(nil)
continue
case "reorderLowerThird":
args := struct {
Id int
MoveUp bool
}{}
err = mapstructure.Decode(data, &args)
if err != nil {
websocket.WriteError(err.Error())
continue
}
err = reorderLowerThird(args.Id, args.MoveUp)
if err != nil {
websocket.WriteError(err.Error())
continue
}
default:
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
continue
}
// Force a reload of the client to render the updated lower thirds list.
err = websocket.Write("reload", nil)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
}
http.Redirect(w, r, "/setup/lower_thirds", 302)
}
func saveLowerThird(lowerThird *LowerThird) error {
oldLowerThird, err := db.GetLowerThirdById(lowerThird.Id)
if err != nil {
return err
}
// Create or update lower third.
if oldLowerThird == nil {
err = db.CreateLowerThird(lowerThird)
} else {
err = db.SaveLowerThird(lowerThird)
}
if err != nil {
return err
}
return nil
}
func reorderLowerThird(id int, moveUp bool) error {
lowerThird, err := db.GetLowerThirdById(id)
if err != nil {
return err
}
// Get the lower third to swap positions with.
lowerThirds, err := db.GetAllLowerThirds()
if err != nil {
return err
}
var lowerThirdIndex int
for i, third := range lowerThirds {
if third.Id == lowerThird.Id {
lowerThirdIndex = i
break
}
}
if moveUp {
lowerThirdIndex--
} else {
lowerThirdIndex++
}
if lowerThirdIndex < 0 || lowerThirdIndex == len(lowerThirds) {
// The one to move is already at the limit; return an error to prevent a page reload.
return fmt.Errorf("Already at the limit.")
}
adjacentLowerThird, err := db.GetLowerThirdById(lowerThirds[lowerThirdIndex].Id)
if err != nil {
return err
}
// Swap their display orders and save.
lowerThird.DisplayOrder, adjacentLowerThird.DisplayOrder =
adjacentLowerThird.DisplayOrder, lowerThird.DisplayOrder
err = db.SaveLowerThird(lowerThird)
if err != nil {
return err
}
err = db.SaveLowerThird(adjacentLowerThird)
if err != nil {
return err
}
return nil
}

View File

@@ -4,8 +4,10 @@
package main
import (
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestSetupLowerThirds(t *testing.T) {
@@ -18,38 +20,48 @@ func TestSetupLowerThirds(t *testing.T) {
eventSettings, _ = db.GetEventSettings()
mainArena.Setup()
db.CreateLowerThird(&LowerThird{0, "Top Text 1", "Bottom Text 1"})
db.CreateLowerThird(&LowerThird{0, "Top Text 2", "Bottom Text 2"})
db.CreateLowerThird(&LowerThird{0, "Top Text 1", "Bottom Text 1", 0})
db.CreateLowerThird(&LowerThird{0, "Top Text 2", "Bottom Text 2", 1})
db.CreateLowerThird(&LowerThird{0, "Top Text 3", "Bottom Text 3", 2})
recorder := getHttpResponse("/setup/lower_thirds")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Top Text 1")
assert.Contains(t, recorder.Body.String(), "Bottom Text 2")
recorder = postHttpResponse("/setup/lower_thirds", "action=delete&id=1")
assert.Equal(t, 302, recorder.Code)
recorder = getHttpResponse("/setup/lower_thirds")
assert.Equal(t, 200, recorder.Code)
assert.NotContains(t, recorder.Body.String(), "Top Text 1")
assert.Contains(t, recorder.Body.String(), "Bottom Text 2")
server, wsUrl := startTestServer()
defer server.Close()
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/setup/lower_thirds/websocket", nil)
assert.Nil(t, err)
defer conn.Close()
ws := &Websocket{conn}
recorder = postHttpResponse("/setup/lower_thirds", "action=save&topText=Text 3&bottomText=")
assert.Equal(t, 302, recorder.Code)
recorder = getHttpResponse("/setup/lower_thirds")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Text 3")
lowerThird, _ := db.GetLowerThirdById(3)
assert.NotNil(t, lowerThird)
ws.Write("saveLowerThird", LowerThird{1, "Top Text 4", "Bottom Text 1", 0})
time.Sleep(time.Millisecond * 10) // Allow some time for the command to be processed.
lowerThird, _ := db.GetLowerThirdById(1)
assert.Equal(t, "Top Text 4", lowerThird.TopText)
recorder = postHttpResponse("/setup/lower_thirds", "action=show&topText=Text 4&bottomText=&id=3")
assert.Equal(t, 302, recorder.Code)
recorder = getHttpResponse("/setup/lower_thirds")
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Text 4")
lowerThird, _ = db.GetLowerThirdById(3)
assert.Equal(t, "Text 4", lowerThird.TopText)
assert.Equal(t, "", lowerThird.BottomText)
ws.Write("deleteLowerThird", LowerThird{1, "Top Text 4", "Bottom Text 1", 0})
time.Sleep(time.Millisecond * 10)
lowerThird, _ = db.GetLowerThirdById(1)
assert.Nil(t, lowerThird)
recorder = postHttpResponse("/setup/lower_thirds", "action=hide&id=3")
assert.Equal(t, 302, recorder.Code)
assert.Equal(t, "blank", mainArena.audienceDisplayScreen)
ws.Write("showLowerThird", LowerThird{2, "Top Text 5", "Bottom Text 1", 0})
time.Sleep(time.Millisecond * 10)
lowerThird, _ = db.GetLowerThirdById(2)
assert.Equal(t, "Top Text 5", lowerThird.TopText)
assert.Equal(t, "lowerThird", mainArena.audienceDisplayScreen)
ws.Write("hideLowerThird", LowerThird{2, "Top Text 6", "Bottom Text 1", 0})
time.Sleep(time.Millisecond * 10)
lowerThird, _ = db.GetLowerThirdById(2)
assert.Equal(t, "Top Text 6", lowerThird.TopText)
assert.Equal(t, "blank", mainArena.audienceDisplayScreen)
ws.Write("reorderLowerThird", map[string]interface{}{"Id": 2, "moveUp": false})
time.Sleep(time.Millisecond * 100)
lowerThirds, _ := db.GetAllLowerThirds()
assert.Equal(t, 3, lowerThirds[0].Id)
assert.Equal(t, 2, lowerThirds[1].Id)
}

View File

@@ -370,12 +370,17 @@ html {
float: left;
}
#lowerThirdTop {
position: relative;
top: 10px;
display: none;
font-family: "FuturaLTBold";
}
#lowerThirdBottom {
display: none;
font-family: "FuturaLT";
font-size: 23px;
position: relative;
top: 5px;
}
#lowerThirdSingle {
display: none;

43
static/js/lower_thirds.js Normal file
View File

@@ -0,0 +1,43 @@
// Copyright 2015 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Client-side logic for the lower thirds management interface.
var websocket;
// Sends a websocket message to save the text for the given lower third.
var saveLowerThird = function(button) {
console.log(button.form.topText.value);
websocket.send("saveLowerThird", constructLowerThird(button));
};
// Sends a websocket message to delete the given lower third.
var deleteLowerThird = function(button) {
websocket.send("deleteLowerThird", constructLowerThird(button));
};
// Sends a websocket message to show the given lower third.
var showLowerThird = function(button) {
websocket.send("showLowerThird", constructLowerThird(button));
};
// Sends a websocket message to hide the lower third.
var hideLowerThird = function(button) {
websocket.send("hideLowerThird", constructLowerThird(button));
};
// Sends a websocket message to reorder the given the lower third.
var reorderLowerThird = function(button, moveUp) {
websocket.send("reorderLowerThird", { Id: parseInt(button.form.id.value), MoveUp: moveUp })
};
// Gathers the lower third info and constructs a JSON object.
var constructLowerThird = function(button) {
return { Id: parseInt(button.form.id.value), TopText: button.form.topText.value,
BottomText: button.form.bottomText.value, DisplayOrder: parseInt(button.form.displayOrder.value) }
}
$(function() {
// Set up the websocket back to the server.
websocket = new CheesyWebsocket("/setup/lower_thirds/websocket", {});
});

View File

@@ -10,34 +10,53 @@
<div class="col-lg-6 col-lg-offset-3">
<div class="well">
<legend>Lower Thirds</legend>
{{range $lowerThird := .LowerThirds}}
<form class="form-horizontal" action="/setup/lower_thirds" method="POST">
{{range $i, $lowerThird := .LowerThirds}}
<form class="form-horizontal">
<div class="form-group">
<div class="col-lg-7">
<div class="col-lg-6">
<input type="hidden" name="id" value="{{$lowerThird.Id}}" />
<input type="text" class="form-control" name="topText" value="{{$lowerThird.TopText}}"
placeholder="Top Text"/>
<input type="text" class="form-control" name="bottomText" value="{{$lowerThird.BottomText}}"
placeholder="Bottom Text"/>
<input type="hidden" name="displayOrder" value="{{$i}}" />
</div>
<div class="col-lg-5">
<button type="submit" class="btn btn-info btn-lower-third" name="action" value="save">Save</button>
<button type="submit" class="btn btn-success btn-lower-third" name="action" value="show">Show</button>
<div class="col-lg-6">
<button type="button" class="btn btn-info btn-lower-third" onclick="saveLowerThird(this);">
Save
</button>
<button type="button" class="btn btn-success btn-lower-third" onclick="showLowerThird(this);">
Show
</button>
<button type="button" class="btn btn-info" onclick="reorderLowerThird(this, true);">
<i class="glyphicon glyphicon-arrow-up"></i>
</button>
<br />
<button type="submit" class="btn btn-primary btn-lower-third" name="action" value="delete">Delete</button>
<button type="submit" class="btn btn-default btn-lower-third" name="action" value="hide">Hide</button>
<button type="button" class="btn btn-primary btn-lower-third" onclick="deleteLowerThird(this);">
Delete
</button>
<button type="button" class="btn btn-default btn-lower-third" onclick="hideLowerThird(this);">
Hide
</button>
<button type="button" class="btn btn-info" onclick="reorderLowerThird(this, false);">
<i class="glyphicon glyphicon-arrow-down"></i>
</button>
</div>
</div>
</form>
{{end}}
<form class="form-horizontal" action="/setup/lower_thirds" method="POST">
<form class="form-horizontal">
<div class="form-group">
<div class="col-lg-7">
<div class="col-lg-6">
<input type="hidden" name="id" value="0" />
<input type="text" class="form-control" name="topText" placeholder="Top or Solo Text" />
<input type="text" class="form-control" name="bottomText" placeholder="Bottom Text" />
<input type="hidden" name="displayOrder" value="{{len .LowerThirds}}" />
</div>
<div class="col-lg-5">
<button type="submit" class="btn btn-info btn-lower-third" name="action" value="save">Save</button>
<div class="col-lg-6">
<button type="button" class="btn btn-info btn-lower-third" name="save" onclick="saveLowerThird(this);">
Save
</button>
</div>
</div>
</form>
@@ -46,4 +65,5 @@
</div>
{{end}}
{{define "script"}}
<script src="/static/js/lower_thirds.js"></script>
{{end}}

2
web.go
View File

@@ -134,7 +134,7 @@ func newHandler() http.Handler {
router.HandleFunc("/setup/field/reload_displays", FieldReloadDisplaysHandler).Methods("GET")
router.HandleFunc("/setup/field/lights", FieldLightsPostHandler).Methods("POST")
router.HandleFunc("/setup/lower_thirds", LowerThirdsGetHandler).Methods("GET")
router.HandleFunc("/setup/lower_thirds", LowerThirdsPostHandler).Methods("POST")
router.HandleFunc("/setup/lower_thirds/websocket", LowerThirdsWebsocketHandler).Methods("GET")
router.HandleFunc("/setup/sponsor_slides", SponsorSlidesGetHandler).Methods("GET")
router.HandleFunc("/setup/sponsor_slides", SponsorSlidesPostHandler).Methods("POST")
router.HandleFunc("/api/sponsor_slides", SponsorSlidesApiHandler).Methods("GET")