cchat-gtk/internal/ui/service/auth/entries.go

272 lines
5.8 KiB
Go

package auth
import (
"html"
"strings"
"unicode"
"unicode/utf8"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/singlestack"
"github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango"
)
type RequestStack struct {
*singlestack.Stack
request *Request
iconBox *gtk.Box
icon *gtk.Image
}
func NewRequestStack() *RequestStack {
icon, _ := gtk.ImageNewFromIconName("document-edit-symbolic", gtk.ICON_SIZE_DIALOG)
icon.SetHAlign(gtk.ALIGN_CENTER)
icon.SetVAlign(gtk.ALIGN_CENTER)
icon.SetHExpand(true)
icon.SetVExpand(true)
icon.SetOpacity(0.5)
icon.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.SetHExpand(true)
box.SetVExpand(true)
box.Add(icon)
box.Show()
stack := singlestack.NewStack()
stack.SetTransitionDuration(50)
stack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE)
stack.SetHExpand(true)
stack.Add(box)
return &RequestStack{
Stack: stack,
iconBox: box,
icon: icon,
}
}
// SetRequest sets the request into the stack. If auther is nil, then the
// placeholder icon is displayed. If auther is not nil, then Show() will be
// called.
func (rs *RequestStack) SetRequest(auther cchat.Authenticator, done func()) {
if auther == nil {
rs.request = nil
rs.Stack.Add(rs.iconBox)
} else {
rs.request = NewRequest(auther, done)
rs.request.Show()
rs.Stack.Add(rs.request)
}
}
func (rs *RequestStack) Request() *Request {
return rs.request
}
// Request is a single page of authenticator fields.
type Request struct {
*gtk.ScrolledWindow
Box *gtk.Box
Grid *gtk.Grid
ErrRev *gtk.Revealer
ErrLabel *gtk.Label
auther cchat.Authenticator
labels []*gtk.Label
texts []Texter
}
func NewRequest(auther cchat.Authenticator, done func()) *Request {
authEntries := auther.AuthenticateForm()
errLabel, _ := gtk.LabelNew("")
errLabel.SetUseMarkup(true)
errLabel.SetMarginTop(8)
errLabel.SetMarginStart(8)
errLabel.SetMarginEnd(8)
errLabel.SetLineWrap(true)
errLabel.SetLineWrapMode(pango.WRAP_WORD_CHAR)
errLabel.Show()
errRev, _ := gtk.RevealerNew()
errRev.SetTransitionDuration(50)
errRev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_DOWN)
errRev.Add(errLabel)
errRev.SetRevealChild(false)
errRev.Show()
grid, _ := gtk.GridNew()
grid.SetRowSpacing(7)
grid.SetColumnHomogeneous(false)
grid.SetColumnSpacing(5)
grid.SetMarginStart(12)
grid.SetMarginEnd(12)
grid.SetMarginTop(8)
grid.Show()
continueBtn, _ := gtk.ButtonNewWithLabel("Continue")
continueBtn.SetHAlign(gtk.ALIGN_CENTER)
continueBtn.Connect("clicked", func(*gtk.Button) { done() })
continueBtn.SetBorderWidth(12)
continueBtn.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.PackStart(errRev, false, false, 0)
box.PackStart(grid, true, true, 0)
box.PackStart(continueBtn, false, false, 0)
box.Show()
sw, _ := gtk.ScrolledWindowNew(nil, nil)
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
sw.SetHExpand(true)
sw.SetVExpand(true)
sw.Add(box)
req := &Request{
ScrolledWindow: sw,
Box: box,
Grid: grid,
ErrRev: errRev,
ErrLabel: errLabel,
auther: auther,
labels: make([]*gtk.Label, len(authEntries)),
texts: make([]Texter, len(authEntries)),
}
for i, authEntry := range authEntries {
label, texter := newEntry(authEntry)
req.labels[i] = label
req.texts[i] = texter
grid.Attach(label, 0, i, 1, 1)
grid.Attach(texter, 1, i, 1, 1)
}
return req
}
// SetError prints the error. If err is nil, then the label is cleared.
func (r *Request) SetError(err error) {
var markup string
if err != nil {
builder := strings.Builder{}
builder.WriteString(`<span color="red"><b>Error!</b>`)
builder.WriteByte('\n')
builder.WriteString(html.EscapeString(capitalizeFirst(err.Error())))
builder.WriteString(`</span>`)
markup = builder.String()
}
// Reveal if err is not nil.
r.ErrRev.SetRevealChild(err != nil)
r.ErrLabel.SetMarkup(markup)
}
// capitalizeFirst capitalizes the first letter.
func capitalizeFirst(str string) string {
r, l := utf8.DecodeRuneInString(str)
if l > 0 {
return string(unicode.ToUpper(r)) + str[l:]
}
return str
}
func (r *Request) values() []string {
var values = make([]string, len(r.texts))
for i, texter := range r.texts {
values[i] = texter.GetText()
}
return values
}
func newEntry(authEntry cchat.AuthenticateEntry) (*gtk.Label, Texter) {
label, _ := gtk.LabelNew(authEntry.Name)
label.SetXAlign(1) // right align
label.SetJustify(gtk.JUSTIFY_RIGHT)
label.SetLineWrap(true)
label.Show()
var texter Texter
if authEntry.Multiline {
texter = NewMultilineInput()
} else {
var input = NewEntryInput()
if authEntry.Secret {
input.SetInputPurpose(gtk.INPUT_PURPOSE_PASSWORD)
input.SetVisibility(false)
input.SetInvisibleChar('●')
} else {
// usually; this is just an assumption
input.SetInputPurpose(gtk.INPUT_PURPOSE_EMAIL)
}
texter = input
}
return label, texter
}
type Texter interface {
gtk.IWidget
GetText() string
SetText(string)
}
type EntryInput struct {
*gtk.Entry
}
var _ Texter = (*EntryInput)(nil)
func NewEntryInput() EntryInput {
input, _ := gtk.EntryNew()
input.SetVAlign(gtk.ALIGN_CENTER)
input.SetHExpand(true)
input.Show()
return EntryInput{
input,
}
}
func (i EntryInput) GetText() (text string) {
text, _ = i.Entry.GetText()
return
}
type MultilineInput struct {
*gtk.TextView
Buffer *gtk.TextBuffer
}
var _ Texter = (*MultilineInput)(nil)
func NewMultilineInput() MultilineInput {
view, _ := gtk.TextViewNew()
view.SetWrapMode(gtk.WRAP_WORD_CHAR)
view.SetEditable(true)
view.SetHExpand(true)
view.Show()
buf, _ := view.GetBuffer()
return MultilineInput{view, buf}
}
func (i MultilineInput) GetText() (text string) {
start, end := i.Buffer.GetBounds()
text, _ = i.Buffer.GetText(start, end, true)
return
}
func (i MultilineInput) SetText(text string) {
i.Buffer.SetText(text)
}