archive
This commit is contained in:
279
MovieNight/wasm/main.go
Executable file
279
MovieNight/wasm/main.go
Executable file
@@ -0,0 +1,279 @@
|
||||
// +build js,wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"syscall/js"
|
||||
|
||||
"github.com/zorchenhimer/MovieNight/common"
|
||||
)
|
||||
|
||||
var (
|
||||
timestamp bool
|
||||
color string
|
||||
auth common.CommandLevel
|
||||
global js.Value
|
||||
)
|
||||
|
||||
func getElement(s string) js.Value {
|
||||
return global.Get("document").Call("getElementById", s)
|
||||
}
|
||||
|
||||
func join(v []js.Value) {
|
||||
color := global.Call("getCookie", "color").String()
|
||||
if color == "" {
|
||||
// If a color is not set, do a random color
|
||||
color = common.RandomColor()
|
||||
} else if !common.IsValidColor(color) {
|
||||
// Don't show the user the error, just clear the cookie
|
||||
common.LogInfof("%#v is not a valid color, clearing cookie", color)
|
||||
global.Call("deleteCookie", "color")
|
||||
}
|
||||
|
||||
joinData, err := json.Marshal(common.JoinData{
|
||||
Name: getElement("name").Get("value").String(),
|
||||
Color: color,
|
||||
})
|
||||
if err != nil {
|
||||
notify("Error prepping data for join")
|
||||
common.LogErrorf("Could not prep data: %#v\n", err)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(common.ClientData{
|
||||
Type: common.CdJoin,
|
||||
Message: string(joinData),
|
||||
})
|
||||
if err != nil {
|
||||
common.LogErrorf("Could not marshal data: %v", err)
|
||||
}
|
||||
|
||||
global.Call("websocketSend", string(data))
|
||||
}
|
||||
|
||||
func recieve(v []js.Value) {
|
||||
if len(v) == 0 {
|
||||
fmt.Println("No data received")
|
||||
return
|
||||
}
|
||||
|
||||
chatJSON, err := common.DecodeData(v[0].String())
|
||||
if err != nil {
|
||||
fmt.Printf("Error decoding data: %s\n", err)
|
||||
global.Call("appendMessages", fmt.Sprintf("<div>%v</div>", v))
|
||||
return
|
||||
}
|
||||
|
||||
chat, err := chatJSON.ToData()
|
||||
if err != nil {
|
||||
fmt.Printf("Error converting ChatDataJSON to ChatData of type %d: %v", chatJSON.Type, err)
|
||||
}
|
||||
|
||||
switch chat.Type {
|
||||
case common.DTHidden:
|
||||
h := chat.Data.(common.HiddenMessage)
|
||||
switch h.Type {
|
||||
case common.CdUsers:
|
||||
names = nil
|
||||
for _, i := range h.Data.([]interface{}) {
|
||||
names = append(names, i.(string))
|
||||
}
|
||||
sort.Strings(names)
|
||||
case common.CdAuth:
|
||||
auth = h.Data.(common.CommandLevel)
|
||||
case common.CdColor:
|
||||
color = h.Data.(string)
|
||||
global.Get("document").Set("cookie", fmt.Sprintf("color=%s; expires=Fri, 31 Dec 9999 23:59:59 GMT", color))
|
||||
case common.CdEmote:
|
||||
data := h.Data.(map[string]interface{})
|
||||
emoteNames = make([]string, 0, len(data))
|
||||
emotes = make(map[string]string)
|
||||
for k, v := range data {
|
||||
emoteNames = append(emoteNames, k)
|
||||
emotes[k] = v.(string)
|
||||
}
|
||||
sort.Strings(emoteNames)
|
||||
case common.CdJoin:
|
||||
notify("")
|
||||
global.Call("openChat")
|
||||
case common.CdNotify:
|
||||
notify(h.Data.(string))
|
||||
}
|
||||
case common.DTEvent:
|
||||
d := chat.Data.(common.DataEvent)
|
||||
// A server message is the only event that doesn't deal with names.
|
||||
if d.Event != common.EvServerMessage {
|
||||
websocketSend("", common.CdUsers)
|
||||
}
|
||||
// on join or leave, update list of possible user names
|
||||
fallthrough
|
||||
case common.DTChat:
|
||||
msg := chat.Data.HTML()
|
||||
if d, ok := chat.Data.(common.DataMessage); ok {
|
||||
if timestamp && (d.Type == common.MsgChat || d.Type == common.MsgAction) {
|
||||
h, m, _ := time.Now().Clock()
|
||||
msg = fmt.Sprintf(`<span class="time">%02d:%02d</span> %s`, h, m, msg)
|
||||
}
|
||||
}
|
||||
|
||||
appendMessage(msg)
|
||||
case common.DTCommand:
|
||||
d := chat.Data.(common.DataCommand)
|
||||
|
||||
switch d.Command {
|
||||
case common.CmdPlaying:
|
||||
if d.Arguments == nil || len(d.Arguments) == 0 {
|
||||
global.Call("setPlaying", "", "")
|
||||
|
||||
} else if len(d.Arguments) == 1 {
|
||||
global.Call("setPlaying", d.Arguments[0], "")
|
||||
|
||||
} else if len(d.Arguments) == 2 {
|
||||
global.Call("setPlaying", d.Arguments[0], d.Arguments[1])
|
||||
}
|
||||
case common.CmdRefreshPlayer:
|
||||
global.Call("initPlayer", nil)
|
||||
case common.CmdPurgeChat:
|
||||
global.Call("purgeChat", nil)
|
||||
appendMessage(d.HTML())
|
||||
case common.CmdHelp:
|
||||
url := "/help"
|
||||
if d.Arguments != nil && len(d.Arguments) > 0 {
|
||||
url = d.Arguments[0]
|
||||
}
|
||||
appendMessage(d.HTML())
|
||||
global.Get("window").Call("open", url, "_blank", "menubar=0,status=0,toolbar=0,width=300,height=600")
|
||||
case common.CmdEmotes:
|
||||
url := "/emotes"
|
||||
appendMessage(d.HTML())
|
||||
global.Get("window").Call("open", url, "_blank", "menubar=0,status=0,toolbar=0,width=300,height=600")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appendMessage(msg string) {
|
||||
global.Call("appendMessages", "<div>"+msg+"</div>")
|
||||
}
|
||||
|
||||
func websocketSend(msg string, dataType common.ClientDataType) error {
|
||||
if strings.TrimSpace(msg) == "" && dataType == common.CdMessage {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := json.Marshal(common.ClientData{
|
||||
Type: dataType,
|
||||
Message: msg,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not marshal data: %v", err)
|
||||
}
|
||||
|
||||
global.Call("websocketSend", string(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
func send(this js.Value, v []js.Value) interface{} {
|
||||
if len(v) != 1 {
|
||||
showChatError(fmt.Errorf("expected 1 parameter, got %d", len(v)))
|
||||
return false
|
||||
}
|
||||
|
||||
err := websocketSend(v[0].String(), common.CdMessage)
|
||||
if err != nil {
|
||||
showChatError(err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func showChatError(err error) {
|
||||
if err != nil {
|
||||
fmt.Printf("Could not send: %v\n", err)
|
||||
global.Call("appendMessages", `<div><span style="color: red;">Could not send message</span></div>`)
|
||||
}
|
||||
}
|
||||
|
||||
func notify(msg string) {
|
||||
global.Call("setNotifyBox", msg)
|
||||
}
|
||||
|
||||
func showTimestamp(v []js.Value) {
|
||||
if len(v) != 1 {
|
||||
// Don't bother with returning a value
|
||||
return
|
||||
}
|
||||
timestamp = v[0].Bool()
|
||||
}
|
||||
|
||||
func isValidColor(this js.Value, v []js.Value) interface{} {
|
||||
if len(v) != 1 {
|
||||
return false
|
||||
}
|
||||
return common.IsValidColor(v[0].String())
|
||||
}
|
||||
|
||||
func debugValues(v []js.Value) {
|
||||
for k, v := range map[string]interface{}{
|
||||
"timestamp": timestamp,
|
||||
"auth": auth,
|
||||
"color": color,
|
||||
"current suggestion": currentSug,
|
||||
"current suggestion type": currentSugType,
|
||||
"filtered suggestions": filteredSug,
|
||||
"user names": names,
|
||||
"emote names": emoteNames,
|
||||
} {
|
||||
fmt.Printf("%s: %#v\n", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
global = js.Global()
|
||||
|
||||
common.SetupLogging(common.LLDebug, "")
|
||||
|
||||
global.Set("processMessageKey", js.FuncOf(processMessageKey))
|
||||
global.Set("sendMessage", js.FuncOf(send))
|
||||
global.Set("isValidColor", js.FuncOf(isValidColor))
|
||||
|
||||
global.Set("recieveMessage", jsCallbackOf(recieve))
|
||||
global.Set("processMessage", jsCallbackOf(processMessage))
|
||||
global.Set("debugValues", jsCallbackOf(debugValues))
|
||||
global.Set("showTimestamp", jsCallbackOf(showTimestamp))
|
||||
global.Set("join", jsCallbackOf(join))
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Second * 1)
|
||||
inner := `<option value=""></option>`
|
||||
for _, c := range common.Colors {
|
||||
inner += fmt.Sprintf(`<option value="%s">%s</option>\n`, c, c)
|
||||
}
|
||||
|
||||
global.Get("colorSelect").Set("innerHTML", inner)
|
||||
}()
|
||||
|
||||
// This is needed so the goroutine does not end
|
||||
for {
|
||||
// heatbeat to keep connection alive to deal with nginx
|
||||
if global.Get("inChat").Bool() {
|
||||
websocketSend("", common.CdPing)
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}
|
||||
|
||||
func jsCallbackOf(fnc func(v []js.Value)) js.Func {
|
||||
return js.FuncOf(func(this js.Value, refs []js.Value) interface{} {
|
||||
vals := make([]js.Value, 0, len(refs))
|
||||
for _, ref := range refs {
|
||||
vals = append(vals, ref)
|
||||
}
|
||||
fnc(vals)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
195
MovieNight/wasm/suggestions.go
Executable file
195
MovieNight/wasm/suggestions.go
Executable file
@@ -0,0 +1,195 @@
|
||||
// +build js,wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"syscall/js"
|
||||
|
||||
"github.com/zorchenhimer/MovieNight/common"
|
||||
)
|
||||
|
||||
const (
|
||||
keyTab = 9
|
||||
keyEnter = 13
|
||||
keyEsc = 27
|
||||
keySpace = 32
|
||||
keyUp = 38
|
||||
keyDown = 40
|
||||
suggestionName = '@'
|
||||
suggestionEmote = ':'
|
||||
)
|
||||
|
||||
var (
|
||||
currentSugType rune
|
||||
currentSug string
|
||||
filteredSug []string
|
||||
names []string
|
||||
emoteNames []string
|
||||
emotes map[string]string
|
||||
)
|
||||
|
||||
// The returned value is a bool deciding to prevent the event from propagating
|
||||
func processMessageKey(this js.Value, v []js.Value) interface{} {
|
||||
startIdx := v[0].Get("target").Get("selectionStart").Int()
|
||||
keyCode := v[0].Get("keyCode").Int()
|
||||
ctrl := v[0].Get("ctrlKey").Bool()
|
||||
|
||||
if ctrl && keyCode == keySpace {
|
||||
processMessage(nil)
|
||||
return true
|
||||
}
|
||||
|
||||
if len(filteredSug) == 0 || currentSug == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
switch keyCode {
|
||||
case keyEsc:
|
||||
filteredSug = nil
|
||||
currentSug = ""
|
||||
currentSugType = 0
|
||||
case keyUp, keyDown:
|
||||
newidx := 0
|
||||
for i, n := range filteredSug {
|
||||
if n == currentSug {
|
||||
newidx = i
|
||||
if keyCode == keyDown {
|
||||
newidx = i + 1
|
||||
if newidx == len(filteredSug) {
|
||||
newidx--
|
||||
}
|
||||
} else if keyCode == keyUp {
|
||||
newidx = i - 1
|
||||
if newidx < 0 {
|
||||
newidx = 0
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
currentSug = filteredSug[newidx]
|
||||
case keyTab, keyEnter:
|
||||
msg := global.Get("msg")
|
||||
val := msg.Get("value").String()
|
||||
newval := val[:startIdx]
|
||||
|
||||
if i := strings.LastIndex(newval, string(currentSugType)); i != -1 {
|
||||
var offset int
|
||||
if currentSugType == suggestionName {
|
||||
offset = 1
|
||||
}
|
||||
|
||||
newval = newval[:i+offset] + currentSug
|
||||
}
|
||||
|
||||
endVal := val[startIdx:]
|
||||
if len(val) == startIdx || val[startIdx:][0] != ' ' {
|
||||
// insert a space into val so selection indexing can be one line
|
||||
endVal = " " + endVal
|
||||
}
|
||||
msg.Set("value", newval+endVal)
|
||||
msg.Set("selectionStart", len(newval)+1)
|
||||
msg.Set("selectionEnd", len(newval)+1)
|
||||
|
||||
// Clear out filtered names since it is no longer needed
|
||||
filteredSug = nil
|
||||
default:
|
||||
// We only want to handle the caught keys, so return early
|
||||
return false
|
||||
}
|
||||
|
||||
updateSuggestionDiv()
|
||||
return true
|
||||
}
|
||||
|
||||
func processMessage(v []js.Value) {
|
||||
msg := global.Get("msg")
|
||||
text := strings.ToLower(msg.Get("value").String())
|
||||
startIdx := msg.Get("selectionStart").Int()
|
||||
|
||||
filteredSug = nil
|
||||
if len(text) != 0 {
|
||||
if len(names) > 0 || len(emoteNames) > 0 {
|
||||
var caretIdx int
|
||||
textParts := strings.Split(text, " ")
|
||||
|
||||
for i, word := range textParts {
|
||||
// Increase caret index at beginning if not first word to account for spaces
|
||||
if i != 0 {
|
||||
caretIdx++
|
||||
}
|
||||
|
||||
// It is possible to have a double space " ", which will lead to an
|
||||
// empty string element in the slice. Also check that the index of the
|
||||
// cursor is between the start of the word and the end
|
||||
if len(word) > 0 && caretIdx <= startIdx && startIdx <= caretIdx+len(word) {
|
||||
var suggestions []string
|
||||
if word[0] == suggestionName {
|
||||
currentSugType = suggestionName
|
||||
suggestions = names
|
||||
} else if word[0] == suggestionEmote {
|
||||
suggestions = emoteNames
|
||||
currentSugType = suggestionEmote
|
||||
}
|
||||
|
||||
for _, s := range suggestions {
|
||||
if len(word) == 1 || strings.Contains(strings.ToLower(s), word[1:]) {
|
||||
filteredSug = append(filteredSug, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(filteredSug) > 0 {
|
||||
currentSug = ""
|
||||
break
|
||||
}
|
||||
|
||||
caretIdx += len(word)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateSuggestionDiv()
|
||||
}
|
||||
|
||||
func updateSuggestionDiv() {
|
||||
const selectedClass = ` class="selectedName"`
|
||||
|
||||
var divs []string
|
||||
if len(filteredSug) > 0 {
|
||||
// set current name to first if not set already
|
||||
if currentSug == "" {
|
||||
currentSug = filteredSug[len(filteredSug)-1]
|
||||
}
|
||||
|
||||
var hascurrentSuggestion bool
|
||||
divs = make([]string, len(filteredSug))
|
||||
|
||||
// Create inner body of html
|
||||
for i := range filteredSug {
|
||||
divs[i] = "<div"
|
||||
|
||||
sug := filteredSug[i]
|
||||
if sug == currentSug {
|
||||
hascurrentSuggestion = true
|
||||
divs[i] += selectedClass
|
||||
}
|
||||
divs[i] += ">"
|
||||
|
||||
if currentSugType == suggestionEmote {
|
||||
divs[i] += common.EmoteToHtml(emotes[sug], sug)
|
||||
}
|
||||
|
||||
divs[i] += sug + "</div>"
|
||||
}
|
||||
|
||||
if !hascurrentSuggestion {
|
||||
divs[0] = divs[0][:4] + selectedClass + divs[0][4:]
|
||||
}
|
||||
}
|
||||
// The \n is so it's easier to read th source in web browsers for the dev
|
||||
global.Get("suggestions").Set("innerHTML", strings.Join(divs, "\n"))
|
||||
global.Call("updateSuggestionScroll")
|
||||
}
|
||||
Reference in New Issue
Block a user