mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-01-09 12:06:49 +00:00
ABSTRACTIONS! THEY'RE EVERYWHERE!
This commit is contained in:
parent
0171ac6b52
commit
2d628fbbb3
5
go.mod
5
go.mod
|
@ -8,9 +8,14 @@ require (
|
|||
github.com/Xuanwo/go-locale v0.2.0
|
||||
github.com/diamondburned/cchat v0.0.15
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200605224934-31a53c555ea2
|
||||
github.com/diamondburned/imgutil v0.0.0-20200606035324-63abbc0fdea6
|
||||
github.com/die-net/lrucache v0.0.0-20190707192454-883874fe3947
|
||||
github.com/goodsign/monday v1.0.0
|
||||
github.com/google/btree v1.0.0 // indirect
|
||||
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
|
||||
github.com/markbates/pkger v0.17.0
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717
|
||||
)
|
||||
|
|
16
go.sum
16
go.sum
|
@ -9,6 +9,12 @@ 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/diamondburned/cchat v0.0.15 h1:1o4OX8zw/CdSv3Idaylz7vjHVOZKEi/xkg8BpEvtsHY=
|
||||
github.com/diamondburned/cchat v0.0.15/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/imgutil v0.0.0-20200606035324-63abbc0fdea6 h1:APALM1hskCByjOVW9CoUwjg0TIJgKZ62dgFr/9soqss=
|
||||
github.com/diamondburned/imgutil v0.0.0-20200606035324-63abbc0fdea6/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
|
||||
github.com/die-net/lrucache v0.0.0-20190707192454-883874fe3947 h1:U/5Sq2nJQ0XDyks+8ATghtHSuquIGq7JYrqSrvtR2dg=
|
||||
github.com/die-net/lrucache v0.0.0-20190707192454-883874fe3947/go.mod h1:KsMcjmY1UCGl7ozPbdVPDOvLaFeXnptSvtNRczhxNto=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI=
|
||||
|
@ -17,10 +23,14 @@ github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7
|
|||
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
github.com/goodsign/monday v1.0.0 h1:Yyk/s/WgudMbAJN6UWSU5xAs8jtNewfqtVblAlw0yoc=
|
||||
github.com/goodsign/monday v1.0.0/go.mod h1:r4T4breXpoFwspQNM+u2sLxJb2zyTaxVGqUfTBjWOu8=
|
||||
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194 h1:bB6XWpxMt2isCWqzjXN8tfVazjxvD8nRJrNoKcL0xAc=
|
||||
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
|
@ -30,6 +40,9 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/markbates/pkger v0.17.0 h1:RFfyBPufP2V6cddUyyEVSHBpaAnM1WzaMNyqomeT+iY=
|
||||
github.com/markbates/pkger v0.17.0/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
|
||||
github.com/peterbourgon/diskv v1.0.0 h1:bRU92KzrX3TQ6IYobfie/PnZkFC+1opBfHpf/PHPDoo=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
@ -40,11 +53,14 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK
|
|||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717 h1:3M/uUZajYn/082wzUajekePxpUAZhMTfXvI9R+26SJ0=
|
||||
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717/go.mod h1:RaxNwUITJaHVdQ0VC7pELPZ3tOWn13nr0gZMZEhpVU0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
|
94
internal/gts/httputil/httputil.go
Normal file
94
internal/gts/httputil/httputil.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package httputil
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/die-net/lrucache"
|
||||
"github.com/gregjones/httpcache"
|
||||
"github.com/gregjones/httpcache/diskcache"
|
||||
"github.com/peterbourgon/diskv"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var dskcached *http.Client
|
||||
var memcached *http.Client
|
||||
|
||||
func init() {
|
||||
var basePath = filepath.Join(os.TempDir(), "cchat-gtk-pridemonth")
|
||||
|
||||
http.DefaultClient.Timeout = 15 * time.Second
|
||||
|
||||
dskcached = &(*http.DefaultClient)
|
||||
dskcached.Transport = httpcache.NewTransport(
|
||||
diskcache.NewWithDiskv(diskv.New(diskv.Options{
|
||||
BasePath: basePath,
|
||||
TempDir: filepath.Join(basePath, "tmp"),
|
||||
PathPerm: 0750,
|
||||
FilePerm: 0750,
|
||||
Compression: diskv.NewZlibCompressionLevel(2),
|
||||
CacheSizeMax: 25 * 1024 * 1024, // 25 MiB in memory
|
||||
})),
|
||||
)
|
||||
|
||||
memcached = &(*http.DefaultClient)
|
||||
memcached.Transport = httpcache.NewTransport(lrucache.New(
|
||||
25*1024*1024, // 25 MiB in memory
|
||||
secs(2*time.Hour), // 2 hours cache
|
||||
))
|
||||
}
|
||||
|
||||
func secs(dura time.Duration) int64 {
|
||||
return int64(dura / time.Second)
|
||||
}
|
||||
|
||||
func AsyncStreamUncached(url string, fn func(r io.Reader)) {
|
||||
gts.Async(func() (func(), error) {
|
||||
r, err := get(url, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func() {
|
||||
fn(r.Body)
|
||||
r.Body.Close()
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func AsyncStream(url string, fn func(r io.Reader)) {
|
||||
gts.Async(func() (func(), error) {
|
||||
r, err := get(url, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func() {
|
||||
fn(r.Body)
|
||||
r.Body.Close()
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func get(url string, cached bool) (r *http.Response, err error) {
|
||||
if cached {
|
||||
r, err = dskcached.Get(url)
|
||||
} else {
|
||||
r, err = memcached.Get(url)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.StatusCode < 200 || r.StatusCode > 299 {
|
||||
r.Body.Close()
|
||||
return nil, errors.Errorf("Unexpected status %d", r.StatusCode)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
85
internal/gts/httputil/image.go
Normal file
85
internal/gts/httputil/image.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package httputil
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/imgutil"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// AsyncImage loads an image. This method uses the cache.
|
||||
func AsyncImage(img *gtk.Image, url string, procs ...imgutil.Processor) {
|
||||
go asyncImage(img, url, procs...)
|
||||
}
|
||||
|
||||
func asyncImage(img *gtk.Image, url string, procs ...imgutil.Processor) {
|
||||
r, err := get(url, true)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
l, err := gdk.PixbufLoaderNew()
|
||||
if err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to make pixbuf loader"))
|
||||
return
|
||||
}
|
||||
|
||||
gif := strings.Contains(url, ".gif")
|
||||
|
||||
// This is a very important signal, so we must do it synchronously. Gotk3's
|
||||
// callback implementation requires all connects to be synchronous to a
|
||||
// certain thread.
|
||||
gts.ExecSync(func() {
|
||||
l.Connect("area-prepared", func() {
|
||||
if gif {
|
||||
p, err := l.GetPixbuf()
|
||||
if err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to get pixbuf"))
|
||||
return
|
||||
}
|
||||
img.SetFromPixbuf(p)
|
||||
} else {
|
||||
p, err := l.GetAnimation()
|
||||
if err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to get animation"))
|
||||
return
|
||||
}
|
||||
img.SetFromAnimation(p)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// If we have processors, then write directly in there.
|
||||
if len(procs) > 0 {
|
||||
if !gif {
|
||||
err = imgutil.ProcessStream(l, r.Body, procs)
|
||||
} else {
|
||||
err = imgutil.ProcessAnimationStream(l, r.Body, procs)
|
||||
}
|
||||
} else {
|
||||
// Else, directly copy the body over.
|
||||
_, err = io.Copy(l, r.Body)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error(errors.Wrap(err, "Error processing image"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := l.Close(); err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to close pixbuf"))
|
||||
}
|
||||
}
|
||||
|
||||
// AsyncImageSized resizes using GdkPixbuf. This method does not use the cache.
|
||||
func AsyncImageSized(img *gtk.Image, url string, w, h int, procs ...imgutil.Processor) {
|
||||
// TODO
|
||||
panic("TODO")
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
package compact
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/message/autoscroll"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/message/input"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
type Container struct {
|
||||
*autoscroll.ScrolledWindow
|
||||
main *gtk.Grid
|
||||
messages map[string]*Message
|
||||
nonceMsgs map[string]*Message
|
||||
|
||||
bottomed bool
|
||||
}
|
||||
|
||||
func NewContainer() *Container {
|
||||
grid, _ := gtk.GridNew()
|
||||
grid.SetColumnSpacing(10)
|
||||
grid.SetRowSpacing(5)
|
||||
grid.SetMarginStart(5)
|
||||
grid.SetMarginEnd(5)
|
||||
grid.SetMarginBottom(5)
|
||||
grid.Show()
|
||||
|
||||
sw := autoscroll.NewScrolledWindow()
|
||||
sw.Add(grid)
|
||||
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
|
||||
sw.Show()
|
||||
|
||||
container := Container{
|
||||
ScrolledWindow: sw,
|
||||
main: grid,
|
||||
messages: map[string]*Message{},
|
||||
nonceMsgs: map[string]*Message{},
|
||||
bottomed: true, // bottomed by default.
|
||||
}
|
||||
|
||||
return &container
|
||||
}
|
||||
|
||||
func (c *Container) Reset() {
|
||||
// does this actually work?
|
||||
var rows = c.len()
|
||||
for i := 0; i < rows; i++ {
|
||||
c.main.RemoveRow(i)
|
||||
}
|
||||
|
||||
c.messages = map[string]*Message{}
|
||||
c.nonceMsgs = map[string]*Message{}
|
||||
|
||||
// default to being bottomed
|
||||
c.bottomed = true
|
||||
}
|
||||
|
||||
func (c *Container) len() int {
|
||||
return len(c.messages) + len(c.nonceMsgs)
|
||||
}
|
||||
|
||||
// PresendMessage is not thread-safe.
|
||||
func (c *Container) PresendMessage(msg input.PresendMessage) func(error) {
|
||||
msgc := NewPresendMessage(msg.Content(), msg.Author(), msg.AuthorID(), msg.Nonce())
|
||||
msgc.index = c.len()
|
||||
|
||||
c.nonceMsgs[msgc.Nonce] = &msgc
|
||||
msgc.Attach(c.main, msgc.index)
|
||||
|
||||
return func(err error) {
|
||||
msgc.SetSensitive(true)
|
||||
|
||||
// Did we fail?
|
||||
if err != nil {
|
||||
msgc.Content.SetMarkup(fmt.Sprintf(
|
||||
`<span color="red">%s</span>`,
|
||||
html.EscapeString(msgc.Content.GetLabel()),
|
||||
))
|
||||
msgc.Content.SetTooltipText(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FindMessage is not thread-safe.
|
||||
func (c *Container) FindMessage(msg cchat.MessageHeader) *Message {
|
||||
// Search using the ID first.
|
||||
m, ok := c.messages[msg.ID()]
|
||||
if ok {
|
||||
return m
|
||||
}
|
||||
|
||||
// Is this an existing message?
|
||||
if noncer, ok := msg.(cchat.MessageNonce); ok {
|
||||
var nonce = noncer.Nonce()
|
||||
|
||||
m, ok := c.nonceMsgs[nonce]
|
||||
if ok {
|
||||
// Move the message outside nonceMsgs.
|
||||
delete(c.nonceMsgs, nonce)
|
||||
c.messages[msg.ID()] = m
|
||||
|
||||
// Set the right ID.
|
||||
m.ID = msg.ID()
|
||||
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
||||
gts.ExecAsync(func() {
|
||||
// Attempt update before insert (aka upsert).
|
||||
if msgc := c.FindMessage(msg); msgc != nil {
|
||||
msgc.SetSensitive(true)
|
||||
msgc.UpdateAuthor(msg.Author())
|
||||
msgc.UpdateContent(msg.Content())
|
||||
msgc.UpdateTimestamp(msg.Time())
|
||||
return
|
||||
}
|
||||
|
||||
msgc := NewMessage(msg)
|
||||
msgc.index = c.len() // unsure
|
||||
|
||||
c.messages[msgc.ID] = &msgc
|
||||
msgc.Attach(c.main, msgc.index)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Container) UpdateMessage(msg cchat.MessageUpdate) {
|
||||
gts.ExecAsync(func() {
|
||||
if msgc := c.FindMessage(msg); msgc != nil {
|
||||
if author := msg.Author(); author != nil {
|
||||
msgc.UpdateAuthor(author)
|
||||
}
|
||||
if content := msg.Content(); !content.Empty() {
|
||||
msgc.UpdateContent(content)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
||||
gts.ExecAsync(func() {
|
||||
// TODO: add nonce check.
|
||||
if m, ok := c.messages[msg.ID()]; ok {
|
||||
delete(c.messages, msg.ID())
|
||||
c.main.RemoveRow(m.index)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
package cozy
|
|
@ -5,7 +5,7 @@ import "github.com/gotk3/gotk3/gtk"
|
|||
type ScrolledWindow struct {
|
||||
gtk.ScrolledWindow
|
||||
vadj gtk.Adjustment
|
||||
bottomed bool // :floshed:
|
||||
Bottomed bool // :floshed:
|
||||
}
|
||||
|
||||
func NewScrolledWindow() *ScrolledWindow {
|
||||
|
@ -15,22 +15,18 @@ func NewScrolledWindow() *ScrolledWindow {
|
|||
sw := &ScrolledWindow{*gtksw, *gtksw.GetVAdjustment(), true} // bottomed by default
|
||||
sw.Connect("size-allocate", func(_ *gtk.ScrolledWindow) {
|
||||
// We can't really trust Gtk to be competent.
|
||||
if sw.bottomed {
|
||||
if sw.Bottomed {
|
||||
sw.ScrollToBottom()
|
||||
}
|
||||
})
|
||||
sw.vadj.Connect("value-changed", func(adj *gtk.Adjustment) {
|
||||
// Manually check if we're anchored on scroll.
|
||||
sw.bottomed = (adj.GetUpper() - adj.GetPageSize()) <= adj.GetValue()
|
||||
sw.Bottomed = (adj.GetUpper() - adj.GetPageSize()) <= adj.GetValue()
|
||||
})
|
||||
|
||||
return sw
|
||||
}
|
||||
|
||||
func (s *ScrolledWindow) Bottomed() bool {
|
||||
return s.bottomed
|
||||
}
|
||||
|
||||
// GetVAdjustment overrides gtk.ScrolledWindow's.
|
||||
func (s *ScrolledWindow) GetVAdjustment() *gtk.Adjustment {
|
||||
return &s.vadj
|
25
internal/ui/messages/container/compact/compact.go
Normal file
25
internal/ui/messages/container/compact/compact.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package compact
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
|
||||
)
|
||||
|
||||
type Container struct {
|
||||
*container.GridContainer
|
||||
}
|
||||
|
||||
func NewContainer() *Container {
|
||||
c := &Container{}
|
||||
c.GridContainer = container.NewGridContainer(c)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Container) NewMessage(msg cchat.MessageCreate) container.GridMessage {
|
||||
return NewMessage(msg)
|
||||
}
|
||||
|
||||
func (c *Container) NewPresendMessage(msg input.PresendMessage) container.PresendGridMessage {
|
||||
return NewPresendMessage(msg)
|
||||
}
|
52
internal/ui/messages/container/compact/message.go
Normal file
52
internal/ui/messages/container/compact/message.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package compact
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
type PresendMessage struct {
|
||||
*message.GenericPresendContainer
|
||||
}
|
||||
|
||||
func NewPresendMessage(msg input.PresendMessage) PresendMessage {
|
||||
return PresendMessage{
|
||||
GenericPresendContainer: message.NewPresendContainer(msg),
|
||||
}
|
||||
}
|
||||
|
||||
func (p PresendMessage) Attach(grid *gtk.Grid, row int) {
|
||||
attachGenericContainer(p.GenericContainer, grid, row)
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
*message.GenericContainer
|
||||
}
|
||||
|
||||
var _ container.GridMessage = (*Message)(nil)
|
||||
|
||||
func NewMessage(msg cchat.MessageCreate) Message {
|
||||
return Message{
|
||||
GenericContainer: message.NewContainer(msg),
|
||||
}
|
||||
}
|
||||
|
||||
func NewEmptyMessage() Message {
|
||||
return Message{
|
||||
GenericContainer: message.NewEmptyContainer(),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: fix a bug here related to new messages overlapping
|
||||
func (m Message) Attach(grid *gtk.Grid, row int) {
|
||||
attachGenericContainer(m.GenericContainer, grid, row)
|
||||
}
|
||||
|
||||
func attachGenericContainer(m *message.GenericContainer, grid *gtk.Grid, row int) {
|
||||
grid.Attach(m.Timestamp, 0, row, 1, 1)
|
||||
grid.Attach(m.Username, 1, row, 1, 1)
|
||||
grid.Attach(m.Content, 2, row, 1, 1)
|
||||
}
|
210
internal/ui/messages/container/container.go
Normal file
210
internal/ui/messages/container/container.go
Normal file
|
@ -0,0 +1,210 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/autoscroll"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GridMessage interface {
|
||||
message.Container
|
||||
Attach(grid *gtk.Grid, row int)
|
||||
}
|
||||
|
||||
type PresendGridMessage interface {
|
||||
GridMessage
|
||||
message.PresendContainer
|
||||
}
|
||||
|
||||
// gridMessage w/ required internals
|
||||
type gridMessage struct {
|
||||
GridMessage
|
||||
presend message.PresendContainer // this shouldn't be here but i'm lazy
|
||||
index int
|
||||
}
|
||||
|
||||
// Constructor is an interface for making custom message implementations which
|
||||
// allows GridContainer to generically work with.
|
||||
type Constructor interface {
|
||||
NewMessage(cchat.MessageCreate) GridMessage
|
||||
NewPresendMessage(input.PresendMessage) PresendGridMessage
|
||||
}
|
||||
|
||||
// Container is a generic messages container.
|
||||
type Container interface {
|
||||
gtk.IWidget
|
||||
cchat.MessagesContainer
|
||||
|
||||
Reset()
|
||||
ScrollToBottom()
|
||||
|
||||
// PresendMessage is for unsent messages.
|
||||
PresendMessage(input.PresendMessage) (done func(sendError error))
|
||||
}
|
||||
|
||||
// GridContainer is an implementation of Container, which allows flexible
|
||||
// message grids.
|
||||
type GridContainer struct {
|
||||
*autoscroll.ScrolledWindow
|
||||
Main *gtk.Grid
|
||||
|
||||
construct Constructor
|
||||
|
||||
messages map[string]*gridMessage
|
||||
nonceMsgs map[string]*gridMessage
|
||||
}
|
||||
|
||||
var (
|
||||
_ Container = (*GridContainer)(nil)
|
||||
_ cchat.MessagesContainer = (*GridContainer)(nil)
|
||||
)
|
||||
|
||||
func NewGridContainer(constr Constructor) *GridContainer {
|
||||
grid, _ := gtk.GridNew()
|
||||
grid.SetColumnSpacing(10)
|
||||
grid.SetRowSpacing(5)
|
||||
grid.SetMarginStart(5)
|
||||
grid.SetMarginEnd(5)
|
||||
grid.SetMarginBottom(5)
|
||||
grid.Show()
|
||||
|
||||
sw := autoscroll.NewScrolledWindow()
|
||||
sw.Add(grid)
|
||||
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
|
||||
sw.Show()
|
||||
|
||||
container := GridContainer{
|
||||
ScrolledWindow: sw,
|
||||
Main: grid,
|
||||
construct: constr,
|
||||
messages: map[string]*gridMessage{},
|
||||
nonceMsgs: map[string]*gridMessage{},
|
||||
}
|
||||
|
||||
return &container
|
||||
}
|
||||
|
||||
func (c *GridContainer) Reset() {
|
||||
// does this actually work?
|
||||
var rows = c.len()
|
||||
for i := 0; i < rows; i++ {
|
||||
c.Main.RemoveRow(i)
|
||||
}
|
||||
|
||||
c.messages = map[string]*gridMessage{}
|
||||
c.nonceMsgs = map[string]*gridMessage{}
|
||||
|
||||
c.ScrolledWindow.Bottomed = true
|
||||
}
|
||||
|
||||
func (c *GridContainer) len() int {
|
||||
return len(c.messages) + len(c.nonceMsgs)
|
||||
}
|
||||
|
||||
// PresendMessage is not thread-safe.
|
||||
func (c *GridContainer) PresendMessage(msg input.PresendMessage) func(error) {
|
||||
presend := c.construct.NewPresendMessage(msg)
|
||||
|
||||
msgc := gridMessage{
|
||||
GridMessage: presend,
|
||||
presend: presend,
|
||||
index: c.len(),
|
||||
}
|
||||
|
||||
c.nonceMsgs[presend.Nonce()] = &msgc
|
||||
msgc.Attach(c.Main, msgc.index)
|
||||
|
||||
return func(err error) {
|
||||
if err != nil {
|
||||
presend.SetSentError(err)
|
||||
log.Error(errors.Wrap(err, "Failed to send message"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FindMessage is not thread-safe. This exists for backwards compatibility.
|
||||
func (c *GridContainer) FindMessage(msg cchat.MessageHeader) GridMessage {
|
||||
if m := c.findMessage(msg); m != nil {
|
||||
return m.GridMessage
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GridContainer) findMessage(msg cchat.MessageHeader) *gridMessage {
|
||||
// Search using the ID first.
|
||||
m, ok := c.messages[msg.ID()]
|
||||
if ok {
|
||||
return m
|
||||
}
|
||||
|
||||
// Is this an existing message?
|
||||
if noncer, ok := msg.(cchat.MessageNonce); ok {
|
||||
var nonce = noncer.Nonce()
|
||||
|
||||
// Things in this map are guaranteed to have presend != nil.
|
||||
m, ok := c.nonceMsgs[nonce]
|
||||
if ok {
|
||||
// Move the message outside nonceMsgs.
|
||||
delete(c.nonceMsgs, nonce)
|
||||
c.messages[msg.ID()] = m
|
||||
|
||||
// Set the right ID.
|
||||
m.presend.SetID(msg.ID())
|
||||
m.presend.SetDone()
|
||||
// Destroy the presend struct.
|
||||
m.presend = nil
|
||||
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GridContainer) CreateMessage(msg cchat.MessageCreate) {
|
||||
gts.ExecAsync(func() {
|
||||
// Attempt update before insert (aka upsert).
|
||||
if msgc := c.FindMessage(msg); msgc != nil {
|
||||
msgc.UpdateAuthor(msg.Author())
|
||||
msgc.UpdateContent(msg.Content())
|
||||
msgc.UpdateTimestamp(msg.Time())
|
||||
return
|
||||
}
|
||||
|
||||
msgc := gridMessage{
|
||||
GridMessage: c.construct.NewMessage(msg),
|
||||
index: c.len(),
|
||||
}
|
||||
|
||||
c.messages[msgc.ID()] = &msgc
|
||||
msgc.Attach(c.Main, msgc.index)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *GridContainer) UpdateMessage(msg cchat.MessageUpdate) {
|
||||
gts.ExecAsync(func() {
|
||||
if msgc := c.FindMessage(msg); msgc != nil {
|
||||
if author := msg.Author(); author != nil {
|
||||
msgc.UpdateAuthor(author)
|
||||
}
|
||||
if content := msg.Content(); !content.Empty() {
|
||||
msgc.UpdateContent(content)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *GridContainer) DeleteMessage(msg cchat.MessageDelete) {
|
||||
gts.ExecAsync(func() {
|
||||
// TODO: add nonce check.
|
||||
if m, ok := c.messages[msg.ID()]; ok {
|
||||
delete(c.messages, msg.ID())
|
||||
c.Main.RemoveRow(m.index)
|
||||
}
|
||||
})
|
||||
}
|
13
internal/ui/messages/container/cozy/cozy.go
Normal file
13
internal/ui/messages/container/cozy/cozy.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package cozy
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/autoscroll"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
type Container struct {
|
||||
*autoscroll.ScrolledWindow
|
||||
main *gtk.Grid
|
||||
messages map[string]Message
|
||||
nonceMsgs map[string]Message
|
||||
}
|
20
internal/ui/messages/container/cozy/message.go
Normal file
20
internal/ui/messages/container/cozy/message.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package cozy
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
type Message interface {
|
||||
gtk.IWidget
|
||||
message.Container
|
||||
}
|
||||
|
||||
type FullMessage struct {
|
||||
*gtk.Box
|
||||
|
||||
Avatar *gtk.Image
|
||||
*message.GenericContainer
|
||||
}
|
||||
|
||||
func NewFullMessage()
|
|
@ -1,4 +1,4 @@
|
|||
package compact
|
||||
package message
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
@ -11,44 +11,46 @@ import (
|
|||
"github.com/gotk3/gotk3/pango"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
index int
|
||||
ID string
|
||||
AuthorID string
|
||||
Nonce string
|
||||
type Container interface {
|
||||
ID() string
|
||||
AuthorID() string
|
||||
Nonce() string
|
||||
|
||||
UpdateAuthor(cchat.MessageAuthor)
|
||||
UpdateAuthorName(text.Rich)
|
||||
UpdateContent(text.Rich)
|
||||
UpdateTimestamp(time.Time)
|
||||
}
|
||||
|
||||
// GenericContainer provides a single generic message container for subpackages
|
||||
// to use.
|
||||
type GenericContainer struct {
|
||||
id string
|
||||
authorID string
|
||||
nonce string
|
||||
|
||||
Timestamp *gtk.Label
|
||||
Username *gtk.Label
|
||||
Content *gtk.Label
|
||||
}
|
||||
|
||||
func NewMessage(msg cchat.MessageCreate) Message {
|
||||
m := NewEmptyMessage()
|
||||
m.ID = msg.ID()
|
||||
m.UpdateTimestamp(msg.Time())
|
||||
m.UpdateAuthor(msg.Author())
|
||||
m.UpdateContent(msg.Content())
|
||||
var _ Container = (*GenericContainer)(nil)
|
||||
|
||||
func NewContainer(msg cchat.MessageCreate) *GenericContainer {
|
||||
c := NewEmptyContainer()
|
||||
c.id = msg.ID()
|
||||
c.UpdateTimestamp(msg.Time())
|
||||
c.UpdateAuthor(msg.Author())
|
||||
c.UpdateContent(msg.Content())
|
||||
|
||||
if noncer, ok := msg.(cchat.MessageNonce); ok {
|
||||
m.Nonce = noncer.Nonce()
|
||||
c.nonce = noncer.Nonce()
|
||||
}
|
||||
|
||||
return m
|
||||
return c
|
||||
}
|
||||
|
||||
func NewPresendMessage(content string, author text.Rich, authorID, nonce string) Message {
|
||||
msgc := NewEmptyMessage()
|
||||
msgc.Nonce = nonce
|
||||
msgc.AuthorID = authorID
|
||||
msgc.SetSensitive(false)
|
||||
msgc.UpdateContent(text.Rich{Content: content})
|
||||
msgc.UpdateTimestamp(time.Now())
|
||||
msgc.updateAuthorName(author)
|
||||
|
||||
return msgc
|
||||
}
|
||||
|
||||
func NewEmptyMessage() Message {
|
||||
func NewEmptyContainer() *GenericContainer {
|
||||
ts, _ := gtk.LabelNew("")
|
||||
ts.SetLineWrap(true)
|
||||
ts.SetLineWrapMode(pango.WRAP_WORD)
|
||||
|
@ -75,39 +77,39 @@ func NewEmptyMessage() Message {
|
|||
content.SetSelectable(true)
|
||||
content.Show()
|
||||
|
||||
return Message{
|
||||
return &GenericContainer{
|
||||
Timestamp: ts,
|
||||
Username: user,
|
||||
Content: content,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) SetSensitive(sensitive bool) {
|
||||
m.Timestamp.SetSensitive(sensitive)
|
||||
m.Username.SetSensitive(sensitive)
|
||||
m.Content.SetSensitive(sensitive)
|
||||
func (m *GenericContainer) ID() string {
|
||||
return m.id
|
||||
}
|
||||
|
||||
func (m *Message) Attach(grid *gtk.Grid, row int) {
|
||||
grid.Attach(m.Timestamp, 0, row, 1, 1)
|
||||
grid.Attach(m.Username, 1, row, 1, 1)
|
||||
grid.Attach(m.Content, 2, row, 1, 1)
|
||||
func (m *GenericContainer) AuthorID() string {
|
||||
return m.authorID
|
||||
}
|
||||
|
||||
func (m *Message) UpdateTimestamp(t time.Time) {
|
||||
func (m *GenericContainer) Nonce() string {
|
||||
return m.nonce
|
||||
}
|
||||
|
||||
func (m *GenericContainer) UpdateTimestamp(t time.Time) {
|
||||
m.Timestamp.SetLabel(humanize.TimeAgo(t))
|
||||
m.Timestamp.SetTooltipText(t.Format(time.Stamp))
|
||||
}
|
||||
|
||||
func (m *Message) UpdateAuthor(author cchat.MessageAuthor) {
|
||||
m.AuthorID = author.ID()
|
||||
m.updateAuthorName(author.Name())
|
||||
func (m *GenericContainer) UpdateAuthor(author cchat.MessageAuthor) {
|
||||
m.authorID = author.ID()
|
||||
m.UpdateAuthorName(author.Name())
|
||||
}
|
||||
|
||||
func (m *Message) updateAuthorName(name text.Rich) {
|
||||
func (m *GenericContainer) UpdateAuthorName(name text.Rich) {
|
||||
m.Username.SetMarkup(parser.RenderMarkup(name))
|
||||
}
|
||||
|
||||
func (m *Message) UpdateContent(content text.Rich) {
|
||||
func (m *GenericContainer) UpdateContent(content text.Rich) {
|
||||
m.Content.SetMarkup(parser.RenderMarkup(content))
|
||||
}
|
57
internal/ui/messages/message/sending.go
Normal file
57
internal/ui/messages/message/sending.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package message
|
||||
|
||||
import (
|
||||
"html"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
type PresendContainer interface {
|
||||
Container
|
||||
SetID(id string)
|
||||
SetDone()
|
||||
SetSentError(err error)
|
||||
}
|
||||
|
||||
// PresendGenericContainer is the generic container with extra methods
|
||||
// implemented for mutability of the generic message container.
|
||||
type GenericPresendContainer struct {
|
||||
*GenericContainer
|
||||
}
|
||||
|
||||
var _ PresendContainer = (*GenericPresendContainer)(nil)
|
||||
|
||||
func NewPresendContainer(msg input.PresendMessage) *GenericPresendContainer {
|
||||
c := NewEmptyContainer()
|
||||
c.nonce = msg.Nonce()
|
||||
c.authorID = msg.AuthorID()
|
||||
c.UpdateContent(text.Rich{Content: msg.Content()})
|
||||
c.UpdateTimestamp(time.Now())
|
||||
c.UpdateAuthorName(msg.Author())
|
||||
|
||||
p := &GenericPresendContainer{
|
||||
GenericContainer: c,
|
||||
}
|
||||
p.SetSensitive(false)
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (m *GenericPresendContainer) SetID(id string) {
|
||||
m.id = id
|
||||
}
|
||||
|
||||
func (m *GenericPresendContainer) SetSensitive(sensitive bool) {
|
||||
m.Content.SetSensitive(sensitive)
|
||||
}
|
||||
|
||||
func (m *GenericPresendContainer) SetDone() {
|
||||
m.SetSensitive(true)
|
||||
}
|
||||
|
||||
func (m *GenericPresendContainer) SetSentError(err error) {
|
||||
m.Content.SetMarkup(`<span color="red">` + html.EscapeString(m.Content.GetLabel()) + `</span>`)
|
||||
m.Content.SetTooltipText(err.Error())
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
package message
|
||||
package messages
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/message/compact"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/message/input"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container/compact"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
@ -22,7 +23,7 @@ type Container interface {
|
|||
|
||||
type View struct {
|
||||
*gtk.Box
|
||||
Container Container
|
||||
Container container.Container
|
||||
SendInput *input.Field
|
||||
|
||||
current cchat.ServerMessage
|
||||
|
@ -32,6 +33,7 @@ type View struct {
|
|||
func NewView() *View {
|
||||
view := &View{}
|
||||
|
||||
// TODO: change
|
||||
view.Container = compact.NewContainer()
|
||||
view.SendInput = input.NewField(view)
|
||||
|
143
internal/ui/rich/image.go
Normal file
143
internal/ui/rich/image.go
Normal file
|
@ -0,0 +1,143 @@
|
|||
package rich
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/imgutil"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Icon struct {
|
||||
*gtk.Revealer
|
||||
Image *gtk.Image
|
||||
|
||||
resizer imgutil.Processor
|
||||
procs []imgutil.Processor
|
||||
url string // state
|
||||
}
|
||||
|
||||
const DefaultIconSize = 16
|
||||
|
||||
var _ cchat.IconContainer = (*Icon)(nil)
|
||||
|
||||
func NewIcon(sizepx int, procs ...imgutil.Processor) *Icon {
|
||||
if sizepx == 0 {
|
||||
sizepx = DefaultIconSize
|
||||
}
|
||||
|
||||
img, _ := gtk.ImageNew()
|
||||
img.Show()
|
||||
img.SetSizeRequest(sizepx, sizepx)
|
||||
|
||||
rev, _ := gtk.RevealerNew()
|
||||
rev.Add(img)
|
||||
rev.SetRevealChild(false)
|
||||
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
|
||||
rev.SetTransitionDuration(50)
|
||||
|
||||
return &Icon{
|
||||
Revealer: rev,
|
||||
Image: img,
|
||||
resizer: imgutil.Resize(sizepx, sizepx),
|
||||
procs: procs,
|
||||
}
|
||||
}
|
||||
|
||||
// Thread-unsafe methods should only be called right after construction.
|
||||
|
||||
// SetPlaceholderIcon is not thread-safe.
|
||||
func (i *Icon) SetPlaceholderIcon(iconName string, iconSzPx int) {
|
||||
i.SetRevealChild(true)
|
||||
i.SetSize(iconSzPx)
|
||||
|
||||
if iconName != "" {
|
||||
primitives.SetImageIcon(i.Image, iconName, iconSzPx)
|
||||
}
|
||||
}
|
||||
|
||||
// SetSize is not thread-safe.
|
||||
func (i *Icon) SetSize(szpx int) {
|
||||
i.Image.SetSizeRequest(szpx, szpx)
|
||||
i.resizer = imgutil.Resize(szpx, szpx)
|
||||
}
|
||||
|
||||
// AddProcessors is not thread-safe.
|
||||
func (i *Icon) AddProcessors(procs ...imgutil.Processor) {
|
||||
i.procs = append(i.procs, procs...)
|
||||
}
|
||||
|
||||
// SetIcon is thread-safe.
|
||||
func (i *Icon) SetIcon(url string) {
|
||||
gts.ExecAsync(func() { i.SetRevealChild(true) })
|
||||
i.url = url
|
||||
i.updateAsync()
|
||||
}
|
||||
|
||||
func (i *Icon) updateAsync() {
|
||||
httputil.AsyncImage(i.Image, i.url, imgutil.Prepend(i.resizer, i.procs)...)
|
||||
}
|
||||
|
||||
type ToggleButtonImage struct {
|
||||
gtk.ToggleButton
|
||||
Labeler
|
||||
cchat.IconContainer
|
||||
|
||||
Label *gtk.Label
|
||||
Image *Icon
|
||||
|
||||
Box *gtk.Box
|
||||
}
|
||||
|
||||
var (
|
||||
_ gtk.IWidget = (*ToggleButton)(nil)
|
||||
_ cchat.LabelContainer = (*ToggleButton)(nil)
|
||||
)
|
||||
|
||||
func NewToggleButtonImage(content text.Rich) *ToggleButtonImage {
|
||||
l := NewLabel(content)
|
||||
l.Show()
|
||||
|
||||
i := NewIcon(0)
|
||||
i.Show()
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
box.PackStart(i, false, false, 0)
|
||||
box.PackStart(l, true, true, 5)
|
||||
box.Show()
|
||||
|
||||
b, _ := gtk.ToggleButtonNew()
|
||||
b.Add(box)
|
||||
|
||||
return &ToggleButtonImage{
|
||||
ToggleButton: *b,
|
||||
Labeler: l, // easy inheritance of methods
|
||||
IconContainer: i,
|
||||
|
||||
Label: &l.Label,
|
||||
Image: i,
|
||||
Box: box,
|
||||
}
|
||||
}
|
||||
|
||||
type Namer interface {
|
||||
Name(cchat.LabelContainer) error
|
||||
}
|
||||
|
||||
// Try tries to set the name from namer. It also tries Icon.
|
||||
func (b *ToggleButtonImage) Try(namer Namer, desc string) {
|
||||
if err := namer.Name(b); err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to get name for "+desc))
|
||||
b.SetLabel(text.Rich{Content: "Unknown"})
|
||||
}
|
||||
|
||||
if iconer, ok := namer.(cchat.Icon); ok {
|
||||
if err := iconer.Icon(b); err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to get icon for "+desc))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -79,47 +79,3 @@ func NewToggleButton(content text.Rich) *ToggleButton {
|
|||
|
||||
return &ToggleButton{*b, *l}
|
||||
}
|
||||
|
||||
type ToggleButtonImage struct {
|
||||
gtk.ToggleButton
|
||||
Labeler
|
||||
|
||||
Label gtk.Label
|
||||
Image gtk.Image
|
||||
|
||||
Box gtk.Box
|
||||
}
|
||||
|
||||
var (
|
||||
_ gtk.IWidget = (*ToggleButton)(nil)
|
||||
_ cchat.LabelContainer = (*ToggleButton)(nil)
|
||||
)
|
||||
|
||||
func NewToggleButtonImage(content text.Rich, iconName string) *ToggleButtonImage {
|
||||
l := NewLabel(content)
|
||||
l.Show()
|
||||
|
||||
var i *gtk.Image
|
||||
if iconName != "" {
|
||||
i, _ = gtk.ImageNewFromIconName(iconName, gtk.ICON_SIZE_BUTTON)
|
||||
} else {
|
||||
i, _ = gtk.ImageNew()
|
||||
}
|
||||
i.Show()
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
box.PackStart(i, false, false, 0)
|
||||
box.PackStart(l, true, true, 5)
|
||||
box.Show()
|
||||
|
||||
b, _ := gtk.ToggleButtonNew()
|
||||
b.Add(box)
|
||||
|
||||
return &ToggleButtonImage{
|
||||
ToggleButton: *b,
|
||||
Labeler: l, // easy inheritance of methods
|
||||
Label: l.Label,
|
||||
Image: *i,
|
||||
Box: *box,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,14 @@ package service
|
|||
|
||||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const IconSize = 32
|
||||
|
@ -115,14 +117,18 @@ type header struct {
|
|||
}
|
||||
|
||||
func newHeader(svc cchat.Service) *header {
|
||||
reveal := rich.NewToggleButtonImage(text.Rich{Content: svc.Name()}, "")
|
||||
reveal := rich.NewToggleButtonImage(text.Rich{Content: svc.Name()})
|
||||
reveal.Box.SetHAlign(gtk.ALIGN_START)
|
||||
reveal.Image.SetPlaceholderIcon("folder-remote-symbolic", IconSize)
|
||||
reveal.SetRelief(gtk.RELIEF_NONE)
|
||||
reveal.SetMode(true)
|
||||
reveal.Show()
|
||||
|
||||
// Set a custom icon.
|
||||
primitives.SetImageIcon(&reveal.Image, "folder-remote-symbolic", IconSize)
|
||||
if iconer, ok := svc.(cchat.Icon); ok {
|
||||
if err := iconer.Icon(reveal); err != nil {
|
||||
log.Error(errors.Wrap(err, "Error getting session logo"))
|
||||
}
|
||||
}
|
||||
|
||||
add, _ := gtk.ButtonNewFromIconName("list-add-symbolic", gtk.ICON_SIZE_BUTTON)
|
||||
add.SetRelief(gtk.RELIEF_NONE)
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
)
|
||||
|
||||
const ChildrenMargin = 24
|
||||
const IconSize = 18
|
||||
|
||||
type Controller interface {
|
||||
MessageRowSelected(*Row, cchat.ServerMessage)
|
||||
|
@ -34,15 +35,12 @@ type Row struct {
|
|||
}
|
||||
|
||||
func NewRow(parent breadcrumb.Breadcrumber, server cchat.Server, ctrl Controller) *Row {
|
||||
button := rich.NewToggleButtonImage(text.Rich{}, "")
|
||||
button := rich.NewToggleButtonImage(text.Rich{})
|
||||
button.Box.SetHAlign(gtk.ALIGN_START)
|
||||
button.Image.SetSize(IconSize)
|
||||
button.SetRelief(gtk.RELIEF_NONE)
|
||||
button.Show()
|
||||
|
||||
if err := server.Name(button); err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to get the server name"))
|
||||
button.SetLabel(text.Rich{Content: "Unknown"})
|
||||
}
|
||||
button.Try(server, "server")
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
box.PackStart(button, false, false, 0)
|
||||
|
|
|
@ -2,14 +2,12 @@ package session
|
|||
|
||||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const IconSize = 32
|
||||
|
@ -38,23 +36,18 @@ func New(parent breadcrumb.Breadcrumber, ses cchat.Session, ctrl Controller) *Ro
|
|||
}
|
||||
row.Servers = server.NewChildren(row, ses, row)
|
||||
|
||||
row.Button = rich.NewToggleButtonImage(text.Rich{}, "")
|
||||
row.Button = rich.NewToggleButtonImage(text.Rich{})
|
||||
row.Button.Box.SetHAlign(gtk.ALIGN_START)
|
||||
row.Button.Image.SetPlaceholderIcon("user-available-symbolic", IconSize)
|
||||
row.Button.SetRelief(gtk.RELIEF_NONE)
|
||||
row.Button.Show()
|
||||
// On click, toggle reveal.
|
||||
row.Button.Connect("clicked", func() {
|
||||
revealed := !row.Servers.GetRevealChild()
|
||||
row.Servers.SetRevealChild(revealed)
|
||||
row.Button.SetActive(revealed)
|
||||
})
|
||||
|
||||
primitives.SetImageIcon(&row.Button.Image, "user-available-symbolic", IconSize)
|
||||
|
||||
if err := ses.Name(row.Button); err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to get the username"))
|
||||
row.Button.SetLabel(text.Rich{Content: "Unknown"})
|
||||
}
|
||||
row.Button.Show()
|
||||
row.Button.Try(ses, "session")
|
||||
|
||||
row.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
row.Box.SetMarginStart(server.ChildrenMargin)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/message"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
@ -9,13 +9,13 @@ import (
|
|||
type window struct {
|
||||
*gtk.Box
|
||||
Services *service.View
|
||||
MessageView *message.View
|
||||
MessageView *messages.View
|
||||
}
|
||||
|
||||
func newWindow() *window {
|
||||
services := service.NewView()
|
||||
services.SetSizeRequest(LeftWidth, -1)
|
||||
mesgview := message.NewView()
|
||||
mesgview := messages.NewView()
|
||||
|
||||
separator, _ := gtk.SeparatorNew(gtk.ORIENTATION_VERTICAL)
|
||||
separator.Show()
|
||||
|
|
Loading…
Reference in a new issue