From: Fl_GUI Date: Sat, 28 Dec 2024 15:49:51 +0000 (+0100) Subject: multiwindow setup X-Git-Url: https://git.openfl.eu/?a=commitdiff_plain;h=2dc8133d277ec76daacd074a5126a1d76ffd4f88;p=chat-scroll.git multiwindow setup --- diff --git a/chat/create.go b/chat/create.go index 814013a..c1d2424 100644 --- a/chat/create.go +++ b/chat/create.go @@ -1,6 +1,7 @@ package chat import ( + "fmt" "sync" chatMessages "fl-gui.name/twitchchat/twitch/core/messages" @@ -8,10 +9,13 @@ import ( "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 } @@ -29,16 +33,33 @@ func (c *ChatWindow) OnMessage(m chatMessages.Message) { 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 diff --git a/fynepatch/keyvaluelayout.go b/fynepatch/keyvaluelayout.go new file mode 100644 index 0000000..d2a042c --- /dev/null +++ b/fynepatch/keyvaluelayout.go @@ -0,0 +1,66 @@ +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 +} diff --git a/main.go b/main.go index e1a0ef9..d8642c4 100644 --- a/main.go +++ b/main.go @@ -8,11 +8,13 @@ import ( "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" @@ -56,7 +58,14 @@ func main() { 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() } diff --git a/model/model.go b/model/model.go index 519d7c0..181d363 100644 --- a/model/model.go +++ b/model/model.go @@ -7,7 +7,8 @@ import ( ) type Model struct { - Twitch TwitchModel + Twitch TwitchModel + windows map[*Window]struct{} } type TwitchModel struct { @@ -17,6 +18,16 @@ 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 @@ -25,6 +36,11 @@ func init() { tw.Token = binding.NewString() } + { + windows := make(map[*Window]struct{}) + Application.windows = windows + } + debug := true if debug { Application.Twitch.TokenState.AddListener(binding.NewDataListener(func() { diff --git a/model/tokenState.go b/model/tokenState.go new file mode 100644 index 0000000..d7c9439 --- /dev/null +++ b/model/tokenState.go @@ -0,0 +1,9 @@ +package model + +type TokenState int + +const ( + Valid TokenState = iota + Invalid + Absent +) diff --git a/model/window.go b/model/window.go new file mode 100644 index 0000000..b2a8d09 --- /dev/null +++ b/model/window.go @@ -0,0 +1,52 @@ +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)) } diff --git a/persistence/file.go b/persistence/file.go new file mode 100644 index 0000000..d667a34 --- /dev/null +++ b/persistence/file.go @@ -0,0 +1,21 @@ +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 +} diff --git a/persistence/io.go b/persistence/io.go new file mode 100644 index 0000000..2af37ae --- /dev/null +++ b/persistence/io.go @@ -0,0 +1,38 @@ +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 +} diff --git a/persistence/types.go b/persistence/types.go new file mode 100644 index 0000000..0e130eb --- /dev/null +++ b/persistence/types.go @@ -0,0 +1,11 @@ +package persistence + +type PersistedStorage struct { + Windows Windows +} + +type Windows []ChatWindow + +type ChatWindow struct { + width, height int +} diff --git a/status/create.go b/status/create.go index 5fbf979..90f0293 100644 --- a/status/create.go +++ b/status/create.go @@ -2,7 +2,6 @@ package status 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" @@ -25,7 +24,7 @@ var ( ) func spawnChatWindow(app fyne.App) { - window := chat.NewChat(app) + window := chat.NewChat(app, model.Application.AddWindow()) window.Show() } @@ -38,7 +37,7 @@ func ableSpawnButton() { } } -func CreateSetupWindow(app fyne.App) { +func CreateSetupWindow(app fyne.App) fyne.CanvasObject { // init but after app is created createSpinner() @@ -50,15 +49,6 @@ func CreateSetupWindow(app fyne.App) { 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 } diff --git a/status/twitchconn/twitchConnection.go b/status/twitchconn/twitchConnection.go index b9ccc90..50a1dde 100644 --- a/status/twitchconn/twitchConnection.go +++ b/status/twitchconn/twitchConnection.go @@ -48,6 +48,5 @@ func CreateContent() { tokenLabel, ) - //token.AddListener(binding.NewDataListener(showContent)) tokenState.AddListener(binding.NewDataListener(updateLabel)) } diff --git a/todo b/todo index 4bdbb4d..ca0bbac 100644 --- a/todo +++ b/todo @@ -1,6 +1,4 @@ --TODO -better emotes showing -chat settings persistence log levels? diff --git a/token/authenticate.go b/token/authenticate.go new file mode 100644 index 0000000..a44114f --- /dev/null +++ b/token/authenticate.go @@ -0,0 +1,48 @@ +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) + } + } + }() +} diff --git a/token/authhandler.go b/token/authhandler.go new file mode 100644 index 0000000..ac2c981 --- /dev/null +++ b/token/authhandler.go @@ -0,0 +1,16 @@ +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) +} diff --git a/token/page.html b/token/page.html new file mode 100644 index 0000000..f6a1c09 --- /dev/null +++ b/token/page.html @@ -0,0 +1,20 @@ + + + + + + + +

+ Successfully authenticated with twitch. You can close this page now. +

+

+ Or you can show your love through Ko-fi: + +

+ + diff --git a/windowconfig/windowconfig.go b/windowconfig/windowconfig.go new file mode 100644 index 0000000..b0bae48 --- /dev/null +++ b/windowconfig/windowconfig.go @@ -0,0 +1,30 @@ +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 +} diff --git a/windowconfig/windowconfigitem.go b/windowconfig/windowconfigitem.go new file mode 100644 index 0000000..bf56133 --- /dev/null +++ b/windowconfig/windowconfigitem.go @@ -0,0 +1,59 @@ +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 +}