package chat
import (
+ "fmt"
"sync"
chatMessages "fl-gui.name/twitchchat/twitch/core/messages"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
"go.openfl.eu/chat-scroller/messages"
+ "go.openfl.eu/chat-scroller/model"
)
type ChatWindow struct {
content *scrollCanvas
+ window fyne.Window
+ windowModel *model.Window
messagesSync sync.Mutex
textSize float32
}
c.content.Add(co)
}
-func NewChat(app fyne.App) fyne.Window {
- w := app.NewWindow("chat")
+func (c *ChatWindow) DataChanged() {
+ name, err := c.windowModel.Name.Get()
+ if err != nil {
+ fmt.Printf("Could not set chat name: %s\n", err)
+ } else {
+ c.window.SetTitle(name)
+ }
+}
+
+func NewChat(app fyne.App, window *model.Window) fyne.Window {
+ windowName, err := window.Name.Get()
+ if err != nil {
+ fmt.Printf("Could not open new window: %s\n", err)
+ return nil
+ }
+ w := app.NewWindow(windowName)
content := newScrollingCanvas()
- cw := ChatWindow{content, sync.Mutex{}, app.Settings().Theme().Size(theme.SizeNameText)}
+ cw := ChatWindow{content, w, window, sync.Mutex{}, app.Settings().Theme().Size(theme.SizeNameText)}
+
messages.Listen(&cw)
+ window.Name.AddListener(&cw)
w.SetContent(content)
w.SetOnClosed(func() {
messages.StopListening(&cw)
+ window.Name.RemoveListener(&cw)
})
return w
--- /dev/null
+package fynepatch
+
+import "fyne.io/fyne/v2"
+
+type KeyValueLayout struct {
+}
+
+// Places children in a two column table with
+// row major order. The first column will
+// be as wide as the widest child in that column,
+// the second will fill the remaining space.
+func NewKeyValueLayout() fyne.Layout {
+ return &KeyValueLayout{}
+}
+
+func (kv *KeyValueLayout) Layout(objects []fyne.CanvasObject, totalSize fyne.Size) {
+ var keyWidth float32 = 0
+ for i := 0; i < len(objects); i += 2 {
+ kWidth := objects[i].MinSize().Width
+ if kWidth > keyWidth {
+ keyWidth = kWidth
+ }
+ }
+
+ var h float32 = 0
+ var k fyne.CanvasObject
+ for i := 0; i < len(objects); i += 2 {
+ k = objects[i]
+ // row height
+ dh := k.MinSize().Height
+ if i+1 < len(objects) {
+ valueHeight := objects[i+1].MinSize().Height
+ if valueHeight > dh {
+ dh = valueHeight
+ }
+ }
+ // place key
+ k.Resize(fyne.Size{keyWidth, dh})
+ k.Move(fyne.Position{0, h})
+
+ // place value
+ if i+1 < len(objects) {
+ value := objects[i+1]
+ value.Resize(fyne.Size{totalSize.Width - keyWidth, dh})
+ value.Move(fyne.Position{keyWidth, h})
+ }
+ // calculate height offset
+ h += dh
+ }
+}
+
+func (kv *KeyValueLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
+ minsize := fyne.Size{0, 0}
+ for i := 0; i < len(objects); i += 2 {
+ key := objects[i]
+ rowSize := key.MinSize()
+ if i+1 < len(objects) {
+ valueHeight := objects[i+1].MinSize().Height
+ if valueHeight > rowSize.Height {
+ rowSize.Height = valueHeight
+ }
+ }
+ minsize = minsize.Add(rowSize)
+ }
+ return minsize
+}
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
+ "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/theme"
"go.openfl.eu/chat-scroller/model"
"go.openfl.eu/chat-scroller/status"
"go.openfl.eu/chat-scroller/token"
+ "go.openfl.eu/chat-scroller/windowconfig"
// for init purposes
_ "go.openfl.eu/chat-scroller/decorations"
scroller.Settings().SetTheme(chattheme{theme.LightTheme()})
- go status.CreateSetupWindow(scroller)
+ statusWindow := scroller.NewWindow("Fl_GUI's chat scroller")
+ statusWindow.SetMaster()
+
+ statusWindow.Show()
+ statusWindow.SetContent(container.NewVBox(
+ status.CreateSetupWindow(scroller),
+ windowconfig.CreateWindowConfig(scroller),
+ ))
scroller.Run()
}
)
type Model struct {
- Twitch TwitchModel
+ Twitch TwitchModel
+ windows map[*Window]struct{}
}
type TwitchModel struct {
var Application Model
+func (a Model) AddWindow() *Window {
+ w := newWindow()
+ a.windows[&w] = struct{}{}
+ return &w
+}
+
+func (a Model) RemoveWindow(w *Window) {
+ delete(a.windows, w)
+}
+
func init() {
{
tw := &Application.Twitch
tw.Token = binding.NewString()
}
+ {
+ windows := make(map[*Window]struct{})
+ Application.windows = windows
+ }
+
debug := true
if debug {
Application.Twitch.TokenState.AddListener(binding.NewDataListener(func() {
--- /dev/null
+package model
+
+type TokenState int
+
+const (
+ Valid TokenState = iota
+ Invalid
+ Absent
+)
--- /dev/null
+package model
+
+import (
+ "image/color"
+
+ "fyne.io/fyne/v2/data/binding"
+)
+
+// Window is an Untyped containing a *Window
+type Window struct {
+ Name binding.String
+ Channel binding.String
+ FontSize binding.Float
+ Width, Height binding.Int
+ Palette binding.Untyped // color.Palette
+}
+
+func newWindow() Window {
+ w := Window{
+ binding.NewString(),
+ binding.NewString(),
+ binding.NewFloat(),
+ binding.NewInt(), binding.NewInt(),
+ binding.NewUntyped(),
+ }
+ w.Name.Set("chat")
+ w.Channel.Set("")
+ w.FontSize.Set(20)
+ w.Width.Set(200)
+ w.Height.Set(200)
+ w.Palette.Set(make([]color.Color, 0))
+ return w
+}
+
+func (w *Window) GetPalette() (color.Palette, error) {
+ a, err := w.Palette.Get()
+ if err != nil {
+ return nil, err
+ }
+ return a.(color.Palette), nil
+}
+
+func (w *Window) AddPaletteColor(col color.Color) error {
+ p, err := w.GetPalette()
+ if err != nil {
+ return err
+ }
+ p = append(p, col)
+ return w.Palette.Set(p)
+}
+
+func (w *Window) ClearPalette() { w.Palette.Set(make([]color.Color, 0)) }
--- /dev/null
+package persistence
+
+import (
+ "os"
+ "path"
+)
+
+var filepath string
+
+func getFilePath() string {
+ if filepath == "" {
+ configDir, err := os.UserConfigDir()
+ if err != nil {
+ panic(err)
+ }
+
+ filepath = path.Join(configDir, "chat-scroller.config.json")
+ }
+
+ return filepath
+}
--- /dev/null
+package persistence
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "go.openfl.eu/chat-scroller/model"
+)
+
+func must[T any](v T, err error) T {
+ return v
+}
+
+func Write(m *model.Model) error {
+ _ = PersistedStorage{
+ Windows{},
+ }
+ return nil
+}
+
+func Read() PersistedStorage {
+ p := getFilePath()
+ f, err := os.Open(p)
+ if err != nil {
+ fmt.Println(err)
+ return PersistedStorage{}
+ }
+ defer f.Close()
+
+ var storageContent PersistedStorage
+ err = json.NewDecoder(f).Decode(&storageContent)
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ return storageContent
+}
--- /dev/null
+package persistence
+
+type PersistedStorage struct {
+ Windows Windows
+}
+
+type Windows []ChatWindow
+
+type ChatWindow struct {
+ width, height int
+}
import (
"fyne.io/fyne/v2"
- "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/widget"
"go.openfl.eu/chat-scroller/chat"
)
func spawnChatWindow(app fyne.App) {
- window := chat.NewChat(app)
+ window := chat.NewChat(app, model.Application.AddWindow())
window.Show()
}
}
}
-func CreateSetupWindow(app fyne.App) {
+func CreateSetupWindow(app fyne.App) fyne.CanvasObject {
// init but after app is created
createSpinner()
spawnButton.Disable()
}
- statusWindow := app.NewWindow("Fl_GUI's chat scroller")
- statusWindow.SetMaster()
-
- statusWindow.SetContent(container.NewVBox(
- //spinner,
- twitchconn.Content,
- spawnButton,
- ))
- statusWindow.Show()
-
state.AddListener(binding.NewDataListener(ableSpawnButton))
+ return twitchconn.Content
}
tokenLabel,
)
- //token.AddListener(binding.NewDataListener(showContent))
tokenState.AddListener(binding.NewDataListener(updateLabel))
}
--TODO
-better emotes showing
-chat settings
persistence
log levels?
--- /dev/null
+package token
+
+import (
+ "fmt"
+ "net/url"
+ "sync"
+
+ "fyne.io/fyne/v2"
+ "go.openfl.eu/chat-scroller/model"
+ "go.openfl.eu/twitch-auth/implicit"
+)
+
+var authMutex sync.Mutex
+
+func Authenticate(a *fyne.App) {
+ authMutex.Lock()
+ defer authMutex.Unlock()
+ state, _ := model.Application.Twitch.TokenState.Get()
+ if state == model.Valid {
+ return
+ }
+
+ authUrl, tokenChannel := implicit.AuthenticateWithHandler(&implicit.Config{
+ "9mcopb33ssgli53u6cor8ou2pvyb0g",
+ ":4567",
+ []string{"user:read:chat", "user:write:chat", "chat:read"},
+ false,
+ }, &OAuthSuccessHandler{})
+
+ app := fyne.CurrentApp()
+ u, err := url.Parse(authUrl)
+ if err != nil {
+ panic(err)
+ }
+ app.OpenURL(u)
+
+ go func() {
+ for token := range tokenChannel {
+ if token.Err == nil {
+ model.Application.Twitch.Token.Set(token.AccessCode)
+ } else {
+ model.Application.Twitch.TokenState.Set(model.Invalid)
+ err := token.Err
+ fmt.Println(err)
+ }
+ }
+ }()
+}
--- /dev/null
+package token
+
+import (
+ _ "embed"
+ "net/http"
+)
+
+//go:embed page.html
+var oathSuccessPage []byte
+
+type OAuthSuccessHandler struct {
+}
+
+func (*OAuthSuccessHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ w.Write(oathSuccessPage)
+}
--- /dev/null
+<!DOCTYPE html>
+<html>
+<head>
+<style>
+p {
+ font-family: sans-serif;
+}
+</style>
+</head>
+
+<body>
+ <p>
+ Successfully authenticated with twitch. You can close this page now.
+ </p>
+ <p>
+ Or you can show your love through Ko-fi:
+ <script type='text/javascript' src='https://storage.ko-fi.com/cdn/widget/Widget_2.js'></script><script type='text/javascript'>kofiwidget2.init('Support me on Ko-fi', '#69c2a7', 'N4N2XG5FJ');kofiwidget2.draw();</script>
+ </p>
+</body>
+</html>
--- /dev/null
+package windowconfig
+
+import (
+ "log"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/container"
+ "go.openfl.eu/chat-scroller/model"
+)
+
+type windowConfig struct {
+ *container.DocTabs
+}
+
+func CreateWindowConfig(app fyne.App) fyne.CanvasObject {
+ createTab := func() *container.TabItem {
+ window := model.Application.AddWindow()
+ windowItem, err := newWindowConfigItem(app, window)
+ if err != nil {
+ log.Printf("Could not create new window setup: %s\n", err)
+ return nil
+ }
+ return windowItem.TabItem
+ }
+ w := windowConfig{container.NewDocTabs(createTab())}
+ w.CreateTab = createTab
+
+ // todo intersect OnClosed
+ return w
+}
--- /dev/null
+package windowconfig
+
+import (
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/widget"
+ "go.openfl.eu/chat-scroller/chat"
+ "go.openfl.eu/chat-scroller/fynepatch"
+ "go.openfl.eu/chat-scroller/model"
+)
+
+type WindowConfigItem struct {
+ *container.TabItem
+ Window *model.Window
+}
+
+func newWindowConfigItem(app fyne.App, window *model.Window) (WindowConfigItem, error) {
+ content := fyne.NewContainerWithLayout(
+ fynepatch.NewKeyValueLayout(),
+ widget.NewLabel("window title"),
+ windowTitleEntry(window),
+
+ widget.NewLabel("channel"),
+ channelEntry(window),
+ )
+
+ name, err := window.Name.Get()
+
+ return WindowConfigItem{
+ container.NewTabItem(name,
+ container.NewVBox(
+ content,
+ widget.NewButton("open", func() {
+ w := chat.NewChat(app, window)
+ w.Show()
+ }),
+ ),
+ ),
+ window,
+ }, err
+}
+
+func windowTitleEntry(window *model.Window) *widget.Entry {
+ entry := widget.NewEntry()
+ entry.Text = "chat"
+ entry.OnChanged = func(l string) {
+ window.Name.Set(l)
+ }
+ return entry
+}
+
+func channelEntry(window *model.Window) *widget.Entry {
+ entry := widget.NewEntry()
+ entry.PlaceHolder = "Leave blank for own channel. TODO implement"
+ entry.OnSubmitted = func(l string) {
+ window.Channel.Set(l)
+ }
+ return entry
+}