diff --git a/go.mod b/go.mod
index 47958bf..94741b5 100644
--- a/go.mod
+++ b/go.mod
@@ -6,12 +6,11 @@ replace github.com/diamondburned/cchat-mock => ../cchat-mock/
require (
github.com/Xuanwo/go-locale v0.2.0
- github.com/davecgh/go-spew v1.1.1 // indirect
github.com/diamondburned/cchat v0.0.15
- github.com/diamondburned/cchat-mock v0.0.0-20200604043646-de5384bd320d
+ github.com/diamondburned/cchat-mock v0.0.0-20200605224934-31a53c555ea2
github.com/goodsign/monday v1.0.0
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194
+ github.com/markbates/pkger v0.17.0
github.com/pkg/errors v0.9.1
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717
- golang.org/x/tools v0.0.0-20200529172331-a64b76657301 // indirect
)
diff --git a/go.sum b/go.sum
index 4a71b48..f83441c 100644
--- a/go.sum
+++ b/go.sum
@@ -7,16 +7,12 @@ github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3E
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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.10 h1:aiUVgGre5E/HV+Iw6tmBVbuGctQI+JndV9nIDoYuRPY=
-github.com/diamondburned/cchat v0.0.10/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
-github.com/diamondburned/cchat v0.0.13 h1:p8SyFjiRVCTjvwSJ4FsICGVYVZ3g0Iu02FrwmLuKiKE=
-github.com/diamondburned/cchat v0.0.13/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
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/cchat-mock v0.0.0-20200529184140-47fa2491d2fc h1:xYSN3re1QOd5af5zG15pLKfBM+fergw7Rg62UHmE22g=
-github.com/diamondburned/cchat-mock v0.0.0-20200529184140-47fa2491d2fc/go.mod h1:rQm5EKhNyBRYHKtirSkf+Db23nr3mTs2bnOThfTfzec=
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=
+github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/goodsign/monday v1.0.0 h1:Yyk/s/WgudMbAJN6UWSU5xAs8jtNewfqtVblAlw0yoc=
@@ -27,6 +23,13 @@ github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194 h1:bB6XWpxMt2isCWqzj
github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
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=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+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/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=
@@ -39,35 +42,22 @@ 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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
-golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20200529172331-a64b76657301 h1:G6CNEgFU8/XwexSnuFw+Jq/WePjRitgy6ofBcPnAIPo=
-golang.org/x/tools v0.0.0-20200529172331-a64b76657301/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
+gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/internal/gts/css.go b/internal/gts/css.go
new file mode 100644
index 0000000..9bc7c81
--- /dev/null
+++ b/internal/gts/css.go
@@ -0,0 +1,64 @@
+package gts
+
+import (
+ "bytes"
+
+ "github.com/diamondburned/cchat-gtk/internal/log"
+ "github.com/gotk3/gotk3/gdk"
+ "github.com/gotk3/gotk3/gtk"
+ "github.com/markbates/pkger"
+ "github.com/pkg/errors"
+)
+
+var cssRepos = map[string]*gtk.CssProvider{}
+
+func getDefaultScreen() *gdk.Screen {
+ d, _ := gdk.DisplayGetDefault()
+ s, _ := d.GetDefaultScreen()
+ return s
+}
+
+func loadProviders(screen *gdk.Screen) {
+ for file, repo := range cssRepos {
+ gtk.AddProviderForScreen(
+ screen, repo,
+ uint(gtk.STYLE_PROVIDER_PRIORITY_APPLICATION),
+ )
+ // mark as done
+ delete(cssRepos, file)
+ }
+}
+
+func LoadCSS(files ...string) {
+ var buf bytes.Buffer
+ for _, file := range files {
+ buf.Reset()
+
+ if err := readFile(&buf, file); err != nil {
+ log.Error(errors.Wrap(err, "Failed to load a CSS file"))
+ continue
+ }
+
+ prov, _ := gtk.CssProviderNew()
+ if err := prov.LoadFromData(buf.String()); err != nil {
+ log.Error(errors.Wrap(err, "Failed to parse CSS "+file))
+ continue
+ }
+
+ cssRepos[file] = prov
+ }
+}
+
+func readFile(buf *bytes.Buffer, file string) error {
+ f, err := pkger.Open(file)
+ if err != nil {
+ return errors.Wrap(err, "Failed to load a CSS file")
+ }
+ defer f.Close()
+
+ if _, err := buf.ReadFrom(f); err != nil {
+ return errors.Wrap(err, "Failed to read file")
+ }
+
+ return nil
+}
diff --git a/internal/gts/gts.go b/internal/gts/gts.go
index eee4302..357a389 100644
--- a/internal/gts/gts.go
+++ b/internal/gts/gts.go
@@ -9,6 +9,8 @@ import (
"github.com/gotk3/gotk3/gtk"
)
+const AppID = "com.github.diamondburned.cchat-gtk"
+
var Args = append([]string{}, os.Args...)
var recvPool *sync.Pool
@@ -27,7 +29,7 @@ func init() {
},
}
- App.Application, _ = gtk.ApplicationNew("com.github.diamondburned.cchat-gtk", 0)
+ App.Application, _ = gtk.ApplicationNew(AppID, 0)
}
type Windower interface {
@@ -47,10 +49,16 @@ type WindowHeaderer interface {
func Main(wfn func() WindowHeaderer) {
App.Application.Connect("activate", func() {
+ // Load all CSS onto the default screen.
+ loadProviders(getDefaultScreen())
+
App.Header, _ = gtk.HeaderBarNew()
App.Header.SetShowCloseButton(true)
App.Header.Show()
+ b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
+ App.Header.SetCustomTitle(b)
+
App.Window, _ = gtk.ApplicationWindowNew(App.Application)
App.Window.SetDefaultSize(1000, 500)
App.Window.SetTitlebar(App.Header)
diff --git a/internal/log/log.go b/internal/log/log.go
index e94c22e..c8d2ed6 100644
--- a/internal/log/log.go
+++ b/internal/log/log.go
@@ -2,6 +2,7 @@ package log
import (
"fmt"
+ "log"
"os"
"sync"
"time"
@@ -59,3 +60,7 @@ func WriteEntry(entry Entry) {
}
}()
}
+
+func Println(v ...interface{}) {
+ log.Println(v...)
+}
diff --git a/internal/ui/header.go b/internal/ui/header.go
index 5ec342d..915974a 100644
--- a/internal/ui/header.go
+++ b/internal/ui/header.go
@@ -1,14 +1,73 @@
package ui
-import "github.com/gotk3/gotk3/gtk"
+import (
+ "html"
+ "strings"
+
+ "github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb"
+ "github.com/gotk3/gotk3/gtk"
+)
type header struct {
*gtk.Box
+ left *gtk.Box // TODO
+ right *headerRight
}
func newHeader() *header {
- box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
+ left, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
+ left.SetSizeRequest(LeftWidth, -1)
+ left.Show()
+
+ right := newHeaderRight()
+ right.Show()
+
+ separator, _ := gtk.SeparatorNew(gtk.ORIENTATION_VERTICAL)
+ separator.Show()
+
+ box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
+ box.PackStart(left, false, false, 0)
+ box.PackStart(separator, false, false, 0)
+ box.PackStart(right, true, true, 0)
box.Show()
+
// TODO
- return &header{box}
+ return &header{
+ box,
+ left,
+ right,
+ }
+}
+
+const BreadcrumbSlash = `/`
+
+func (h *header) SetBreadcrumb(b breadcrumb.Breadcrumb) {
+ for i := range b {
+ b[i] = html.EscapeString(b[i])
+ }
+
+ h.right.breadcrumb.SetMarkup(
+ BreadcrumbSlash + " " + strings.Join(b, " "+BreadcrumbSlash+" "),
+ )
+}
+
+type headerRight struct {
+ *gtk.Box
+ breadcrumb *gtk.Label
+}
+
+func newHeaderRight() *headerRight {
+ bc, _ := gtk.LabelNew(BreadcrumbSlash)
+ bc.SetUseMarkup(true)
+ bc.SetXAlign(0.0)
+ bc.Show()
+
+ box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
+ box.PackStart(bc, true, true, 14)
+ box.Show()
+
+ return &headerRight{
+ Box: box,
+ breadcrumb: bc,
+ }
}
diff --git a/internal/ui/message/compact/message.go b/internal/ui/message/compact/message.go
index b45cff8..0c034b1 100644
--- a/internal/ui/message/compact/message.go
+++ b/internal/ui/message/compact/message.go
@@ -5,6 +5,7 @@ import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/humanize"
+ "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser"
"github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango"
@@ -104,10 +105,9 @@ func (m *Message) UpdateAuthor(author cchat.MessageAuthor) {
}
func (m *Message) updateAuthorName(name text.Rich) {
- m.Username.SetLabel(name.Content)
- m.Username.SetTooltipText(name.Content)
+ m.Username.SetMarkup(parser.RenderMarkup(name))
}
func (m *Message) UpdateContent(content text.Rich) {
- m.Content.SetLabel(content.Content)
+ m.Content.SetMarkup(parser.RenderMarkup(content))
}
diff --git a/internal/ui/message/cozy/cozy.go b/internal/ui/message/cozy/cozy.go
new file mode 100644
index 0000000..2284c72
--- /dev/null
+++ b/internal/ui/message/cozy/cozy.go
@@ -0,0 +1 @@
+package cozy
diff --git a/internal/ui/message/input/input.go b/internal/ui/message/input/input.go
index 041a569..6281e71 100644
--- a/internal/ui/message/input/input.go
+++ b/internal/ui/message/input/input.go
@@ -10,11 +10,48 @@ import (
"github.com/pkg/errors"
)
+type usernameContainer struct {
+ *gtk.Revealer
+ label *rich.Label
+}
+
+func newUsernameContainer() *usernameContainer {
+ label := rich.NewLabel(text.Rich{})
+ label.SetMaxWidthChars(35)
+ label.SetVAlign(gtk.ALIGN_START)
+ label.SetMarginTop(inputmargin)
+ label.SetMarginBottom(inputmargin)
+ label.SetMarginStart(10)
+ label.SetMarginEnd(10)
+ label.Show()
+
+ rev, _ := gtk.RevealerNew()
+ rev.SetRevealChild(false)
+ rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
+ rev.SetTransitionDuration(50)
+ rev.Add(label)
+
+ return &usernameContainer{rev, label}
+}
+
+// GetLabel is not thread-safe.
+func (u *usernameContainer) GetLabel() text.Rich {
+ return u.label.GetLabel()
+}
+
+// SetLabel is thread-safe.
+func (u *usernameContainer) SetLabel(content text.Rich) {
+ gts.ExecAsync(func() {
+ u.label.SetLabelUnsafe(content)
+
+ // Reveal if the name is not empty.
+ u.SetRevealChild(!u.label.GetLabel().Empty())
+ })
+}
+
type Field struct {
*gtk.Box
-
- namerev *gtk.Revealer
- username *rich.Label // TODO
+ username *usernameContainer
TextScroll *gtk.ScrolledWindow
text *gtk.TextView
@@ -30,25 +67,12 @@ type Controller interface {
PresendMessage(msg PresendMessage) (onErr func(error))
}
-const inputmargin = 3
+const inputmargin = 4
func NewField(ctrl Controller) *Field {
- username := rich.NewLabel(text.Rich{})
- username.SetMaxWidthChars(35)
- username.SetVAlign(gtk.ALIGN_START)
- username.SetMarginTop(inputmargin)
- username.SetMarginBottom(inputmargin)
- username.SetMarginStart(10)
- username.SetMarginEnd(10)
+ username := newUsernameContainer()
username.Show()
- namerev, _ := gtk.RevealerNew()
- namerev.SetRevealChild(false)
- namerev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
- namerev.SetTransitionDuration(50)
- namerev.Add(username)
- namerev.Show()
-
text, _ := gtk.TextViewNew()
text.SetSensitive(false)
text.SetWrapMode(gtk.WRAP_WORD_CHAR)
@@ -68,13 +92,12 @@ func NewField(ctrl Controller) *Field {
sw.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
- box.PackStart(namerev, false, false, 0)
+ box.PackStart(username, false, false, 0)
box.PackStart(sw, true, true, 0)
box.Show()
field := &Field{
Box: box,
- namerev: namerev,
username: username,
TextScroll: sw,
text: text,
@@ -107,9 +130,6 @@ func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSende
log.Warn(err)
}
- // Reveal if the name is not empty.
- f.namerev.SetRevealChild(!f.username.GetLabel().Empty())
-
// Set the sender.
f.sender = sender
f.text.SetSensitive(sender != nil) // grey if sender is nil
diff --git a/internal/ui/rich/parser/parser.go b/internal/ui/rich/parser/parser.go
index 0bfe2c2..a7ba7bf 100644
--- a/internal/ui/rich/parser/parser.go
+++ b/internal/ui/rich/parser/parser.go
@@ -1 +1,83 @@
package parser
+
+import (
+ "bytes"
+ "fmt"
+ "html"
+ "sort"
+
+ "github.com/diamondburned/cchat/text"
+)
+
+type attrAppendMap struct {
+ appended map[int]string
+ indices []int
+}
+
+func newAttrAppendedMap() attrAppendMap {
+ return attrAppendMap{
+ appended: make(map[int]string),
+ indices: []int{},
+ }
+}
+
+func (a *attrAppendMap) add(ind int, attr string) {
+ if _, ok := a.appended[ind]; ok {
+ a.appended[ind] += attr
+ return
+ }
+
+ a.appended[ind] = attr
+ a.indices = append(a.indices, ind)
+}
+
+func (a attrAppendMap) get(ind int) string {
+ return a.appended[ind]
+}
+
+func (a *attrAppendMap) finalize(strlen int) []int {
+ // make sure there's always a closing tag at the end so the entire string
+ // gets flushed.
+ a.add(strlen, "")
+ sort.Ints(a.indices)
+ return a.indices
+}
+
+func RenderMarkup(content text.Rich) string {
+ buf := bytes.Buffer{}
+ buf.Grow(len(content.Content))
+
+ // // Sort so that all starting points are sorted incrementally.
+ // sort.Slice(content.Segments, func(i, j int) bool {
+ // i, _ = content.Segments[i].Bounds()
+ // j, _ = content.Segments[j].Bounds()
+ // return i < j
+ // })
+
+ // map to append strings to indices
+ var appended = newAttrAppendedMap()
+
+ // Parse all segments.
+ for _, segment := range content.Segments {
+ start, end := segment.Bounds()
+
+ switch segment := segment.(type) {
+ case text.Colorer:
+ appended.add(start, fmt.Sprintf("", segment.Color()))
+ appended.add(end, "")
+ }
+ }
+
+ var lastIndex = 0
+
+ for _, index := range appended.finalize(len(content.Content)) {
+ // Write the content.
+ buf.WriteString(html.EscapeString(content.Content[lastIndex:index]))
+ // Write the tags.
+ buf.WriteString(appended.get(index))
+ // Set the last index.
+ lastIndex = index
+ }
+
+ return buf.String()
+}
diff --git a/internal/ui/rich/parser/parser_test.go b/internal/ui/rich/parser/parser_test.go
new file mode 100644
index 0000000..995164c
--- /dev/null
+++ b/internal/ui/rich/parser/parser_test.go
@@ -0,0 +1,55 @@
+package parser
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/diamondburned/cchat-mock/segments"
+ "github.com/diamondburned/cchat/text"
+)
+
+func TestRenderMarkup(t *testing.T) {
+ content := text.Rich{Content: "astolfo is the best trap"}
+ content.Segments = []text.Segment{
+ segments.NewColored(content.Content, 0x55CDFC),
+ }
+ expect := `` + content.Content + ""
+
+ if text := RenderMarkup(content); text != expect {
+ t.Fatal("Unexpected text:", text)
+ }
+}
+
+func TestRenderMarkupPartial(t *testing.T) {
+ content := text.Rich{Content: "random placeholder text go brrr"}
+ content.Segments = []text.Segment{
+ // This is absolutely jankery that should not work at all, but we'll try
+ // it anyway.
+ coloredSegment{0, 4, 0x55CDFC},
+ coloredSegment{2, 6, 0xFFFFFF}, // naive parsing, so spans close unexpectedly.
+ coloredSegment{4, 6, 0xF7A8B8},
+ }
+ const expect = "" +
+ `rand` +
+ `om`
+
+ if text := RenderMarkup(content); !strings.HasPrefix(text, expect) {
+ t.Fatal("Unexpected text:", text)
+ }
+}
+
+type coloredSegment struct {
+ start int
+ end int
+ color uint32
+}
+
+var _ text.Colorer = (*coloredSegment)(nil)
+
+func (c coloredSegment) Bounds() (start, end int) {
+ return c.start, c.end
+}
+
+func (c coloredSegment) Color() uint32 {
+ return c.color
+}
diff --git a/internal/ui/rich/rich.go b/internal/ui/rich/rich.go
index e1eba80..f49decc 100644
--- a/internal/ui/rich/rich.go
+++ b/internal/ui/rich/rich.go
@@ -4,6 +4,7 @@ import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
+ "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser"
"github.com/diamondburned/cchat/text"
"github.com/gotk3/gotk3/gtk"
)
@@ -13,6 +14,7 @@ import (
type Labeler interface {
cchat.LabelContainer // thread-safe
GetLabel() text.Rich // not thread-safe
+ GetText() string
}
type Label struct {
@@ -26,7 +28,8 @@ var (
)
func NewLabel(content text.Rich) *Label {
- label, _ := gtk.LabelNew(content.Content)
+ label, _ := gtk.LabelNew("")
+ label.SetMarkup(parser.RenderMarkup(content))
label.SetHAlign(gtk.ALIGN_START)
return &Label{*label, content}
}
@@ -34,16 +37,27 @@ func NewLabel(content text.Rich) *Label {
// SetLabel is thread-safe.
func (l *Label) SetLabel(content text.Rich) {
gts.ExecAsync(func() {
- l.current = content
- l.SetText(content.Content)
+ l.SetLabelUnsafe(content)
})
}
+// SetLabelUnsafe sets the label in the current thread, meaning it's not
+// thread-safe.
+func (l *Label) SetLabelUnsafe(content text.Rich) {
+ l.current = content
+ l.SetMarkup(parser.RenderMarkup(content))
+}
+
// GetLabel is NOT thread-safe.
func (l *Label) GetLabel() text.Rich {
return l.current
}
+// GetText is NOT thread-safe.
+func (l *Label) GetText() string {
+ return l.current.Content
+}
+
type ToggleButton struct {
gtk.ToggleButton
Label
diff --git a/internal/ui/service/breadcrumb/breadcrumb.go b/internal/ui/service/breadcrumb/breadcrumb.go
new file mode 100644
index 0000000..596ba17
--- /dev/null
+++ b/internal/ui/service/breadcrumb/breadcrumb.go
@@ -0,0 +1,21 @@
+package breadcrumb
+
+import "strings"
+
+type Breadcrumb []string
+
+func (b Breadcrumb) String() string {
+ return strings.Join([]string(b), "/")
+}
+
+type Breadcrumber interface {
+ Breadcrumb() Breadcrumb
+}
+
+// Try accepts a nilable breadcrumber and handles it appropriately.
+func Try(i Breadcrumber, appended ...string) []string {
+ if i == nil {
+ return appended
+ }
+ return append(i.Breadcrumb(), appended...)
+}
diff --git a/internal/ui/service/service.go b/internal/ui/service/service.go
index 72874eb..4f4f27b 100644
--- a/internal/ui/service/service.go
+++ b/internal/ui/service/service.go
@@ -4,6 +4,7 @@ import (
"github.com/diamondburned/cchat"
"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"
@@ -91,7 +92,7 @@ func NewContainer(svc cchat.Service, ctrl Controller) *Container {
}
func (c *Container) AddSession(ses cchat.Session) {
- srow := session.New(ses, c.rowctrl)
+ srow := session.New(c, ses, c.rowctrl)
c.children.addSessionRow(srow)
}
@@ -103,6 +104,10 @@ func (c *Container) Sessions() []cchat.Session {
return sessions
}
+func (c *Container) Breadcrumb() breadcrumb.Breadcrumb {
+ return breadcrumb.Try(nil, c.header.reveal.GetText())
+}
+
type header struct {
*gtk.Box
reveal *rich.ToggleButtonImage // no rich text here but it's left aligned
diff --git a/internal/ui/service/session/server/server.go b/internal/ui/service/session/server/server.go
new file mode 100644
index 0000000..85bfcc7
--- /dev/null
+++ b/internal/ui/service/session/server/server.go
@@ -0,0 +1,177 @@
+package server
+
+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/primitives"
+ "github.com/diamondburned/cchat-gtk/internal/ui/rich"
+ "github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb"
+ "github.com/diamondburned/cchat/text"
+ "github.com/gotk3/gotk3/gtk"
+ "github.com/pkg/errors"
+)
+
+const ChildrenMargin = 24
+
+type Controller interface {
+ MessageRowSelected(*Row, cchat.ServerMessage)
+}
+
+type Row struct {
+ *gtk.Box
+ Button *rich.ToggleButtonImage
+ Server cchat.Server
+ Parent breadcrumb.Breadcrumber
+
+ ctrl Controller
+
+ // enum 1
+ message cchat.ServerMessage
+
+ // enum 2
+ children *Children
+}
+
+func NewRow(parent breadcrumb.Breadcrumber, server cchat.Server, ctrl Controller) *Row {
+ button := rich.NewToggleButtonImage(text.Rich{}, "")
+ button.Box.SetHAlign(gtk.ALIGN_START)
+ 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"})
+ }
+
+ box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
+ box.PackStart(button, false, false, 0)
+ box.Show()
+
+ primitives.AddClass(box, "server")
+
+ // TODO: images
+
+ var row = &Row{
+ Box: box,
+ Button: button,
+ Server: server,
+ Parent: parent,
+ ctrl: ctrl,
+ }
+
+ switch server := server.(type) {
+ case cchat.ServerList:
+ row.children = NewChildren(row, server, ctrl)
+ box.PackStart(row.children, false, false, 0)
+
+ primitives.AddClass(box, "server-list")
+
+ case cchat.ServerMessage:
+ row.message = server
+
+ primitives.AddClass(box, "server-message")
+ }
+
+ button.Connect("clicked", row.onClick)
+
+ return row
+}
+
+func (row *Row) GetActive() bool {
+ return row.Button.GetActive()
+}
+
+func (row *Row) onClick() {
+ switch {
+
+ // If the server is a message server. We're only selected if the button is
+ // pressed.
+ case row.message != nil && row.GetActive():
+ row.ctrl.MessageRowSelected(row, row.message)
+
+ // If the server is a list of smaller servers.
+ case row.children != nil:
+ row.children.SetRevealChild(!row.children.GetRevealChild())
+ }
+}
+
+func (r *Row) Breadcrumb() breadcrumb.Breadcrumb {
+ return breadcrumb.Try(r.Parent, r.Button.GetText())
+}
+
+// Children is a children server with a reference to the parent.
+type Children struct {
+ *gtk.Revealer
+ Main *gtk.Box
+ List cchat.ServerList
+
+ rowctrl Controller
+
+ Rows []*Row
+ Parent breadcrumb.Breadcrumber
+}
+
+func NewChildren(parent breadcrumb.Breadcrumber, list cchat.ServerList, ctrl Controller) *Children {
+ main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
+ main.SetMarginStart(ChildrenMargin)
+ main.Show()
+
+ rev, _ := gtk.RevealerNew()
+ rev.SetRevealChild(false)
+ rev.Add(main)
+ rev.Show()
+
+ children := &Children{
+ Revealer: rev,
+ Main: main,
+ List: list,
+ rowctrl: ctrl,
+ Parent: parent,
+ }
+
+ if err := list.Servers(children); err != nil {
+ log.Error(errors.Wrap(err, "Failed to get servers"))
+ }
+
+ return children
+}
+
+func (c *Children) SetServers(servers []cchat.Server) {
+ gts.ExecAsync(func() {
+ // Save the current state.
+ var oldID string
+ for _, row := range c.Rows {
+ if row.GetActive() {
+ oldID = row.Server.ID()
+ break
+ }
+ }
+
+ // Update the server list.
+ for _, row := range c.Rows {
+ c.Main.Remove(row)
+ }
+
+ c.Rows = make([]*Row, len(servers))
+
+ for i, server := range servers {
+ row := NewRow(c, server, c.rowctrl)
+ c.Rows[i] = row
+ c.Main.Add(row)
+ }
+
+ // Update parent reference? Only if it's activated.
+ if oldID != "" {
+ for _, row := range c.Rows {
+ if row.Server.ID() == oldID {
+ row.Button.SetActive(true)
+ }
+ }
+ }
+ })
+}
+
+func (c *Children) Breadcrumb() breadcrumb.Breadcrumb {
+ return breadcrumb.Try(c.Parent)
+}
diff --git a/internal/ui/service/session/server/server_children.go b/internal/ui/service/session/server/server_children.go
deleted file mode 100644
index c89f981..0000000
--- a/internal/ui/service/session/server/server_children.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package server
-
-import (
- "github.com/diamondburned/cchat"
- "github.com/diamondburned/cchat-gtk/internal/gts"
- "github.com/diamondburned/cchat-gtk/internal/log"
- "github.com/gotk3/gotk3/gtk"
- "github.com/pkg/errors"
-)
-
-const ChildrenMargin = 24
-
-type Controller interface {
- MessageRowSelected(*Row, cchat.ServerMessage)
-}
-
-// Children is a children server with a reference to the parent.
-type Children struct {
- *gtk.Revealer
- Main *gtk.Box
- List cchat.ServerList
-
- rowctrl Controller
-
- Rows []*Row
- ParentRow *Row
-}
-
-func NewChildren(parent *Row, list cchat.ServerList, ctrl Controller) *Children {
- main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
- main.SetMarginStart(ChildrenMargin)
- main.Show()
-
- rev, _ := gtk.RevealerNew()
- rev.SetRevealChild(false)
- rev.Add(main)
- rev.Show()
-
- children := &Children{
- Revealer: rev,
- Main: main,
- List: list,
- rowctrl: ctrl,
- ParentRow: parent,
- }
-
- if err := list.Servers(children); err != nil {
- log.Error(errors.Wrap(err, "Failed to get servers"))
- }
-
- return children
-}
-
-func (c *Children) SetServers(servers []cchat.Server) {
- gts.ExecAsync(func() {
- // Save the current state.
- var oldID string
- for _, row := range c.Rows {
- if row.GetActive() {
- oldID = row.Server.ID()
- break
- }
- }
-
- // Update the server list.
- for _, row := range c.Rows {
- c.Main.Remove(row)
- }
-
- c.Rows = make([]*Row, len(servers))
-
- for i, server := range servers {
- row := NewRow(c, server, c.rowctrl)
- c.Rows[i] = row
- c.Main.Add(row)
- }
-
- // Update parent reference? Only if it's activated.
- if oldID != "" {
- for _, row := range c.Rows {
- if row.Server.ID() == oldID {
- row.Button.SetActive(true)
- }
- }
- }
- })
-}
diff --git a/internal/ui/service/session/server/server_row.go b/internal/ui/service/session/server/server_row.go
deleted file mode 100644
index e4f6703..0000000
--- a/internal/ui/service/session/server/server_row.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package server
-
-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/text"
- "github.com/gotk3/gotk3/gtk"
- "github.com/pkg/errors"
-)
-
-type Row struct {
- *gtk.Box
- Button *rich.ToggleButtonImage
- Server cchat.Server
- Parent *Children
-
- ctrl Controller
-
- // enum 1
- message cchat.ServerMessage
-
- // enum 2
- children *Children
-}
-
-func NewRow(parent *Children, server cchat.Server, ctrl Controller) *Row {
- button := rich.NewToggleButtonImage(text.Rich{}, "")
- button.Box.SetHAlign(gtk.ALIGN_START)
- 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"})
- }
-
- box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
- box.PackStart(button, false, false, 0)
- box.Show()
-
- primitives.AddClass(box, "server")
-
- // TODO: images
-
- var row = &Row{
- Box: box,
- Button: button,
- Server: server,
- Parent: parent,
- ctrl: ctrl,
- }
-
- switch server := server.(type) {
- case cchat.ServerList:
- row.children = NewChildren(row, server, ctrl)
- box.PackStart(row.children, false, false, 0)
-
- primitives.AddClass(box, "server-list")
-
- case cchat.ServerMessage:
- row.message = server
-
- primitives.AddClass(box, "server-message")
- }
-
- button.Connect("clicked", row.onClick)
-
- return row
-}
-
-func (row *Row) GetActive() bool {
- return row.Button.GetActive()
-}
-
-func (row *Row) onClick() {
- switch {
-
- // If the server is a message server. We're only selected if the button is
- // pressed.
- case row.message != nil && row.GetActive():
- row.ctrl.MessageRowSelected(row, row.message)
-
- // If the server is a list of smaller servers.
- case row.children != nil:
- row.children.SetRevealChild(!row.children.GetRevealChild())
- }
-}
-
-func (r *Row) Breadcrumb() string {
- var label = r.Button.GetLabel().Content
-
- // Does the row have a parent?
- if r.Parent != nil {
- return r.Parent.ParentRow.Breadcrumb() + "/" + label
- }
-
- return label
-}
diff --git a/internal/ui/service/session/session.go b/internal/ui/service/session/session.go
index c985ffe..49e94e9 100644
--- a/internal/ui/service/session/session.go
+++ b/internal/ui/service/session/session.go
@@ -5,6 +5,7 @@ import (
"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"
@@ -25,15 +26,17 @@ type Row struct {
Servers *server.Children
- ctrl Controller
+ ctrl Controller
+ parent breadcrumb.Breadcrumber
}
-func New(ses cchat.Session, ctrl Controller) *Row {
+func New(parent breadcrumb.Breadcrumber, ses cchat.Session, ctrl Controller) *Row {
row := &Row{
Session: ses,
ctrl: ctrl,
+ parent: parent,
}
- row.Servers = server.NewChildren(ses, row)
+ row.Servers = server.NewChildren(row, ses, row)
row.Button = rich.NewToggleButtonImage(text.Rich{}, "")
row.Button.Box.SetHAlign(gtk.ALIGN_START)
@@ -67,3 +70,7 @@ func New(ses cchat.Session, ctrl Controller) *Row {
func (r *Row) MessageRowSelected(server *server.Row, smsg cchat.ServerMessage) {
r.ctrl.MessageRowSelected(r, server, smsg)
}
+
+func (r *Row) Breadcrumb() breadcrumb.Breadcrumb {
+ return breadcrumb.Try(r.parent, r.Button.GetLabel().Content)
+}
diff --git a/internal/ui/style.css b/internal/ui/style.css
new file mode 100644
index 0000000..b78e073
--- /dev/null
+++ b/internal/ui/style.css
@@ -0,0 +1 @@
+headerbar { padding: 0; }
diff --git a/internal/ui/ui.go b/internal/ui/ui.go
index 3f9f454..87e10b9 100644
--- a/internal/ui/ui.go
+++ b/internal/ui/ui.go
@@ -10,8 +10,14 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
"github.com/gotk3/gotk3/gtk"
+ "github.com/markbates/pkger"
)
+func init() {
+ // Load the local CSS.
+ gts.LoadCSS(pkger.Include("/internal/ui/style.css"))
+}
+
const LeftWidth = 220
type App struct {
@@ -54,7 +60,7 @@ func (app *App) MessageRowSelected(ses *session.Row, srv *server.Row, smsg cchat
app.lastRowHighlighter = srv.Button.SetActive
app.lastRowHighlighter(true)
- log.Println("Breadcrumb:")
+ app.header.SetBreadcrumb(srv.Breadcrumb())
// Show the messages.
app.window.MessageView.JoinServer(ses.Session, smsg)
diff --git a/pkged.go b/pkged.go
new file mode 100644
index 0000000..358f41f
--- /dev/null
+++ b/pkged.go
@@ -0,0 +1,12 @@
+// Code generated by pkger; DO NOT EDIT.
+
+// +build !skippkger
+
+package main
+
+import (
+ "github.com/markbates/pkger"
+ "github.com/markbates/pkger/pkging/mem"
+)
+
+var _ = pkger.Apply(mem.UnmarshalEmbed([]byte(`1f8b08000000000000ffec58dd8fdb380eff570a3da7a366da29ae792eaeb88739146d713860512c688996b5d1d7ea639a7430fffb42b293c8f99ccc2eb058c00fb145f2474aa4258ae12391a6b5812c1e8990b14bcd0db39a7209da1ade246f9053c63a88af455c66d447e9c982d0ce6adcc0e857e6a58ba10756f019f98f76d6c7cf103bb278ce0433f25fd0481644833464463e5a461684ccc837f002e3c1cc37c2d2469a91852fd61e024f2ff11e22ebc8e2177243becfc8d7080ac922fa8403f105215843162464ea15478786a361ebc5abca21619bd4b6a02cedd02399914ff6df5261c886b32f37c266f37d3c0af77234a83411bd0145450c64769586b2e24a8d242f2b68cb96175134a07f900c4376f823bae26d935a69c98c34eb88d91766b5f318026d1544ac19e2a774853611a4414f950c7160e0aa8cfcda45bb1d50e82df60493ae43bfa3792de4017604b231c96fefeee61f0e18bb0821ff019e877d9852d245c9769c4e43456dd53d189ea2544744213551e14ea0f9dd8ec87a15c5de5544ed40e8603ea26eefde8fe8bbf96d45ef4d195515a7d5dd9b0f638abaa55c911941c32c974654430ac1cc6bba8180efdf8d38d2805fd71c619b9aecb0364e7fcb27aea21dea4c7a6f7d5e65abe378177e06a5a4e1e8a9b0af73bcace610610cfa7f02f3c36684b20c8aefe737f29507e8fa43da250d46febcbc92b1da12d7beff047f7f46d853c898974c44358600e2da48ec1429a46803f356a997dbc83908d8b5dfbd32208d4b2f50775e6a19e5035ebb8192a45eb2ee7aad2145bf583187fbe5d3d2c62370e6936e5e6e236008b2a4893f67a0d0e5c6f82b2ebf82a2018546f38c7cb0bb2b2f2287cbef5ccd3192f126059a1ffb02cb831486e60960bd2f8ccbb7c39381f4f6b458f0e519a192cd19693ca3eac088bd6935f865031103754bb1ffa5f6844742b18fd8ee050d2e9c87baa53848b4c73134446ef7acb9a5a0db3babe2ff0495afa87c131d4de587721a90798cbf56c7d62a30e2c67a4157656b54891e8c48432ebd8caa93de697404711450cdd541e88617659ebdcdb54617754ec75b33b9fa03256a1673a9265b1d83f5b166198cd143f17acbb3a104b666b93ef557e5d658c563ab904525e3881da4110a5b2545379a35ac0303a528ae90a17938264aa69444bb206188fde5574a5d69a9b443c1d7b33594a4995fb49162372c99a28c87624f4b8dc38beaa4a27450825218bf271b913b2f4d84a6d4310663ffa45d8cae1a96c7267a5be666c5032f7f49e76d29a9339d7c9694dd6c4309403f4aa11c3ed77b915fb4950a077a886f19095cb9ed8086b5899023e59389bd63c388b272d637d4369210ad2e15f5816408e1013facf37287ad13a267b67cb310f3f129a2b561c36b677ef8926446ca6e0e0c8c293e0ecb4c4632cbab114db19dbf1fd3ff2a648036e31ed0709bcbd0eaa46cfe9974c03ab87df33c94b36a3d7ffbe6ee02babcf2b17a2e6e53ad9f0327ff809b7f396770dd92b7e711877f70ce802f789c77263721ff76c5e229e076ef8b54ced6459cf376b5be00bca59d8352039c42496ee08438acc390eb8e49fbed872c79a48de4d2a793d12ad0e8c184d67a7d0eb4d9a3d9e0737026dbfb3e23df30c46dfbc224a57ad6b677d1b3ee2dcf8b5c3c926bda3bf720cda6bd725d2be993bdb7fcb9782aec8db6bca8fd0f7d291417647e337f479e9e9e66a4ed9d7b7c9a9152304c8dada9b13535b6a6c6d6d4d89a1a5b53636b6a6c4d8dada9b13535b6a6c6d6d4d89a1a5b53636b6a6c4d8dada9b1f50f686cfd010000ffff010000ffff0d1f6f41ce250000`)))