Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
diamondburned
GitHub Repository: diamondburned/gtkcord4
Path: blob/main/internal/window/login/component.go
450 views
1
package login
2
3
import (
4
"context"
5
"strings"
6
7
"github.com/diamondburned/adaptive"
8
"github.com/diamondburned/arikawa/v3/session"
9
"github.com/diamondburned/chatkit/components/secretdialog"
10
"github.com/diamondburned/chatkit/kits/secret"
11
"github.com/diamondburned/gotk4/pkg/gtk/v4"
12
"github.com/diamondburned/gotkit/app"
13
"github.com/diamondburned/gotkit/gtkutil"
14
"github.com/diamondburned/gotkit/gtkutil/cssutil"
15
"github.com/pkg/errors"
16
"libdb.so/dissent/internal/window/login/loading"
17
)
18
19
// LoginComponent is the main component in the login page.
20
type Component struct {
21
*gtk.Box
22
Inner *gtk.Box
23
24
Loading *loading.PulsatingBar
25
Methods *Methods
26
Bottom *gtk.Box
27
Remember *rememberMeBox
28
ErrorRev *gtk.Revealer
29
LogIn *gtk.Button
30
31
ctx context.Context
32
page *Page
33
}
34
35
var componentCSS = cssutil.Applier("login-component", `
36
.login-component {
37
background: mix(@theme_bg_color, @theme_fg_color, 0.05);
38
border-radius: 12px;
39
min-width: 250px;
40
margin: 12px;
41
padding: 0;
42
}
43
.login-component > *:not(.osd) {
44
margin: 0 8px;
45
}
46
.login-component > *:nth-child(2) {
47
margin-top: 6px;
48
}
49
.login-component > *:first-child {
50
margin-top: 8px;
51
}
52
.login-component > *:not(:first-child) {
53
margin-bottom: 4px;
54
}
55
.login-component > *:last-child {
56
margin-bottom: 8px;
57
}
58
.login-component > notebook {
59
background: none;
60
}
61
.login-component .adaptive-errorlabel {
62
margin-bottom: 8px;
63
}
64
.login-button {
65
background-color: #7289DA;
66
color: #FFFFFF;
67
}
68
.login-with {
69
font-weight: bold;
70
margin-bottom: 2px;
71
}
72
.login-decrypt-button {
73
margin-left: 4px;
74
}
75
`)
76
77
const decryptMsg = `You've previously chosen to remember the token and may have
78
used a password to encrypt it. This button unlocks that encrypted token and logs
79
in using it.`
80
81
// NewComponent creates a new login Component.
82
func NewComponent(ctx context.Context, p *Page) *Component {
83
c := Component{
84
ctx: ctx,
85
page: p,
86
}
87
88
c.Loading = loading.NewPulsatingBar(loading.PulseFast | loading.PulseBarOSD)
89
90
loginWith := gtk.NewLabel("Login using:")
91
loginWith.AddCSSClass("login-with")
92
loginWith.SetXAlign(0)
93
94
c.Methods = NewMethods(&c)
95
96
c.Remember = newRememberMeBox(ctx)
97
98
c.ErrorRev = gtk.NewRevealer()
99
c.ErrorRev.SetTransitionType(gtk.RevealerTransitionTypeSlideDown)
100
c.ErrorRev.SetRevealChild(false)
101
102
c.LogIn = gtk.NewButtonWithLabel("Log In")
103
c.LogIn.AddCSSClass("suggested-action")
104
c.LogIn.AddCSSClass("login-button")
105
c.LogIn.SetHExpand(true)
106
c.LogIn.ConnectClicked(c.login)
107
108
decrypt := gtk.NewButtonWithLabel("Decrypt (?)")
109
decrypt.AddCSSClass("login-decrypt-button")
110
decrypt.SetSensitive(false)
111
decrypt.SetTooltipText(strings.ReplaceAll(decryptMsg, "\n", " "))
112
decrypt.ConnectClicked(c.askDecrypt)
113
114
buttonBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
115
buttonBox.Append(c.LogIn)
116
buttonBox.Append(decrypt)
117
118
gtkutil.Async(ctx, func() func() {
119
if secret.IsEncrypted(ctx) {
120
return func() { decrypt.SetSensitive(true) }
121
} else {
122
return func() { decrypt.Hide() }
123
}
124
})
125
126
c.Inner = gtk.NewBox(gtk.OrientationVertical, 0)
127
c.Inner.Append(loginWith)
128
c.Inner.Append(c.Methods)
129
c.Inner.Append(c.Remember)
130
c.Inner.Append(c.ErrorRev)
131
c.Inner.Append(buttonBox)
132
componentCSS(c.Inner)
133
134
c.Box = gtk.NewBox(gtk.OrientationVertical, 0)
135
c.Box.AddCSSClass("login-component-outer")
136
c.Box.SetHAlign(gtk.AlignCenter)
137
c.Box.SetVAlign(gtk.AlignCenter)
138
c.Box.Append(c.Loading)
139
c.Box.Append(c.Inner)
140
141
return &c
142
}
143
144
// ShowError reveals the error label and shows it to the user.
145
func (c *Component) ShowError(err error) {
146
errLabel := adaptive.NewErrorLabel(err)
147
c.ErrorRev.SetChild(errLabel)
148
c.ErrorRev.SetRevealChild(true)
149
}
150
151
// HideError hides the error label.
152
func (c *Component) HideError() {
153
c.ErrorRev.SetRevealChild(false)
154
}
155
156
// Login presses the Login button.
157
func (c *Component) Login() {
158
c.LogIn.Activate()
159
}
160
161
func (c *Component) login() {
162
switch {
163
case c.Methods.IsEmail():
164
c.loginEmail(
165
c.Methods.Email.Email.Text(),
166
c.Methods.Email.Password.Text(),
167
c.Methods.Email.TOTP.Text(),
168
)
169
case c.Methods.IsToken():
170
c.loginToken(
171
c.Methods.Token.Token.Text(),
172
)
173
}
174
}
175
176
func (c *Component) SetBusy() {
177
c.SetSensitive(false)
178
c.Loading.Show()
179
}
180
181
func (c *Component) SetDone() {
182
c.SetSensitive(true)
183
c.Loading.Hide()
184
}
185
186
func (c *Component) loginEmail(email, password, totp string) {
187
c.SetBusy()
188
189
gtkutil.Async(c.ctx, func() func() {
190
u, err := session.Login(c.ctx, email, password, totp)
191
if err != nil {
192
return func() {
193
c.ShowError(errors.Wrap(err, "cannot login"))
194
c.SetDone()
195
}
196
}
197
198
return func() {
199
c.loginToken(u.Token)
200
c.SetDone()
201
}
202
})
203
}
204
205
func (c *Component) loginToken(token string) {
206
go func() {
207
driver := c.Remember.SecretDriver()
208
if driver == nil {
209
return
210
}
211
212
if err := driver.Set("account", []byte(token)); err != nil {
213
app.Error(c.ctx, errors.Wrap(err, "cannot store account as secret"))
214
}
215
}()
216
217
c.page.asyncUseToken(token)
218
}
219
220
func (c *Component) askDecrypt() {
221
secretdialog.PromptPassword(
222
c.ctx, secretdialog.PromptDecrypt,
223
func(ok bool, enc *secret.EncryptedFile) {
224
if ok {
225
c.page.asyncLoadFromSecrets(enc)
226
}
227
},
228
)
229
}
230
231
// Methods is the notebook containing entries for different login methods.
232
type Methods struct {
233
*gtk.Notebook
234
Email struct { // Username and Password
235
*gtk.Box
236
Email *FormEntry
237
Password *FormEntry
238
TOTP *FormEntry
239
}
240
Token struct { // Token
241
*gtk.Box
242
Token *FormEntry
243
}
244
}
245
246
var methodsCSS = cssutil.Applier("login-methods", `
247
.login-methods > * {
248
margin: 0;
249
}
250
.login-methods > header > tabs > tab {
251
min-width: 0;
252
padding-left: 8px;
253
padding-right: 8px;
254
}
255
.login-methods > stack {
256
padding: 0 4px;
257
}
258
.login-methods .login-formentry {
259
margin-top: 8px;
260
}
261
.login-methods header tab:checked {
262
background-color: @accent_color;
263
}
264
.login-form-2fa {
265
margin-left: 6px;
266
}
267
.login-form-2fa entry {
268
font-family: monospace;
269
}
270
`)
271
272
// NewMethods creates a new Methods widget.
273
func NewMethods(c *Component) *Methods {
274
m := Methods{}
275
276
m.Email.Email = NewFormEntry("Email")
277
m.Email.Email.AddCSSClass("login-form-email")
278
m.Email.Email.FocusNextOnActivate()
279
m.Email.Email.Entry.SetInputPurpose(gtk.InputPurposeEmail)
280
281
m.Email.Password = NewFormEntry("Password")
282
m.Email.Password.AddCSSClass("login-form-password")
283
m.Email.Password.SetHExpand(true)
284
m.Email.Password.FocusNextOnActivate()
285
m.Email.Password.Entry.SetInputPurpose(gtk.InputPurposePassword)
286
m.Email.Password.Entry.SetVisibility(false)
287
288
m.Email.TOTP = NewFormEntry("TOTP")
289
m.Email.TOTP.AddCSSClass("login-form-2fa")
290
m.Email.TOTP.ConnectActivate(c.Login)
291
m.Email.TOTP.Entry.SetInputPurpose(gtk.InputPurposePIN)
292
m.Email.TOTP.Entry.SetPlaceholderText("000000")
293
m.Email.TOTP.Entry.SetMaxLength(6)
294
m.Email.TOTP.Entry.SetWidthChars(6)
295
296
// Hack to collapse the TOTP entry.
297
if text, ok := m.Email.TOTP.Entry.FirstChild().(*gtk.Text); ok {
298
text.SetPropagateTextWidth(true)
299
}
300
301
// [ 0 | 1 | 2 | 3 ]
302
// 0 [ Email ]
303
// 1 [ Password ][TOTP]
304
passwordBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
305
passwordBox.Append(m.Email.Password)
306
passwordBox.Append(m.Email.TOTP)
307
308
m.Email.Box = gtk.NewBox(gtk.OrientationVertical, 0)
309
m.Email.Append(m.Email.Email)
310
m.Email.Append(passwordBox)
311
312
m.Token.Token = NewFormEntry("Token")
313
m.Token.Token.AddCSSClass("login-form-token")
314
m.Token.Token.ConnectActivate(c.Login)
315
m.Token.Token.Entry.SetInputPurpose(gtk.InputPurposePassword)
316
m.Token.Token.Entry.SetVisibility(false)
317
318
m.Token.Box = gtk.NewBox(gtk.OrientationVertical, 0)
319
m.Token.SetVAlign(gtk.AlignStart)
320
m.Token.Append(m.Token.Token)
321
322
m.Notebook = gtk.NewNotebook()
323
m.Notebook.SetShowBorder(false)
324
m.Notebook.AppendPage(m.Token, gtk.NewLabel("Token"))
325
m.Notebook.AppendPage(m.Email, gtk.NewLabel("Email"))
326
m.Notebook.SetCurrentPage(0)
327
328
if stack, ok := m.Notebook.LastChild().(*gtk.Stack); ok {
329
stack.SetTransitionType(gtk.StackTransitionTypeSlideLeftRight)
330
}
331
332
methodsCSS(m)
333
return &m
334
}
335
336
func (m *Methods) IsToken() bool { return m.CurrentPage() == 0 }
337
func (m *Methods) IsEmail() bool { return m.CurrentPage() == 1 }
338
339
// FormEntry is a widget containing a label and an entry.
340
type FormEntry struct {
341
*gtk.Box
342
Label *gtk.Label
343
Entry *gtk.Entry
344
}
345
346
var formEntryCSS = cssutil.Applier("login-formentry", ``)
347
348
// NewFormEntry creates a new FormEntry.
349
func NewFormEntry(label string) *FormEntry {
350
e := FormEntry{}
351
e.Label = gtk.NewLabel(label)
352
e.Label.SetXAlign(0)
353
354
e.Entry = gtk.NewEntry()
355
e.Entry.SetVExpand(true)
356
e.Entry.SetHasFrame(true)
357
358
e.Box = gtk.NewBox(gtk.OrientationVertical, 0)
359
e.Box.Append(e.Label)
360
e.Box.Append(e.Entry)
361
formEntryCSS(e)
362
363
return &e
364
}
365
366
// Text gets the value entry.
367
func (e *FormEntry) Text() string { return e.Entry.Text() }
368
369
// FocusNext navigates to the next widget.
370
func (e *FormEntry) FocusNext() {
371
e.Entry.Emit("move-focus", gtk.DirTabForward)
372
}
373
374
// FocusNextOnActivate binds Enter to navigate to the next widget when it's
375
// pressed.
376
func (e *FormEntry) FocusNextOnActivate() {
377
e.Entry.ConnectActivate(e.FocusNext)
378
}
379
380
// ConnectActivate connects the activate signal hanlder to the Entry.
381
func (e *FormEntry) ConnectActivate(f func()) {
382
e.Entry.ConnectActivate(f)
383
}
384
385