diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index fff3889..2f3d310 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: '1.23' diff --git a/api/logic.go b/api/logic.go index 1ff8722..5803598 100644 --- a/api/logic.go +++ b/api/logic.go @@ -42,7 +42,7 @@ var ( func (p Props) Expired() bool { const lag = 20 * time.Millisecond // for refresh synchronization var d = time.Since(p.last) - return d < Cfg.PropUpdateTick-lag + return d > Cfg.PropUpdateTick-lag } func (u *User) GetProps(cid uint64) (p Props, ok bool) { diff --git a/ui/frame.go b/ui/frame.go index 7c1af7e..222f4f2 100644 --- a/ui/frame.go +++ b/ui/frame.go @@ -9,6 +9,8 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/data/validation" + "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" "github.com/slotopol/balance/api" cfg "github.com/slotopol/balance/config" @@ -27,63 +29,6 @@ var ( Cfg = cfg.Cfg // shortcut ) -// Label compatible with ToolbarItem interface to insert into Toolbar. -type ToolbarLabel struct { - widget.Label -} - -func NewToolbarLabel(text string) *ToolbarLabel { - var l = &ToolbarLabel{ - Label: widget.Label{ - Text: text, - Alignment: fyne.TextAlignLeading, - TextStyle: fyne.TextStyle{}, - }, - } - l.ExtendBaseWidget(l) - return l -} - -func (tl *ToolbarLabel) ToolbarObject() fyne.CanvasObject { - tl.Label.Importance = widget.LowImportance - return tl -} - -// Layout that fits the images to whole space and cuts edges if it needs. -type FitLayout struct { -} - -func (l FitLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { - var ratiofit = size.Width / size.Height - for _, child := range objects { - var newsize = size - var pos = fyne.NewPos(0, 0) - if img, ok := child.(*canvas.Image); ok { - var ratioimg = img.Aspect() - if ratiofit > ratioimg { - newsize.Height = size.Width / ratioimg - pos.Y = (size.Height - newsize.Height) / 2 - } else { - newsize.Width = size.Height * ratioimg - pos.Y = (size.Width - newsize.Width) / 2 - } - } - child.Resize(newsize) - child.Move(pos) - } -} - -func (l FitLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { - var minSize = fyne.NewSize(0, 0) - for _, child := range objects { - if !child.Visible() { - continue - } - minSize = minSize.Max(child.MinSize()) - } - return minSize -} - func GetProp(cid uint64, user *api.User) (p api.Props, err error) { if p, _ = user.GetProps(curcid); !p.Expired() { return // return cached @@ -117,9 +62,81 @@ func FormatAL(al api.AL) string { type Frame struct { fyne.Window + SigninPage MainPage } +type SigninPage struct { + // Backgroud image + underlay *canvas.Image + + // Form widgets + host *widget.Entry + email *widget.Entry + secret *widget.Entry + form *widget.Form + errmsg *widget.Label + + // Page frame + signinPage *fyne.Container +} + +const descrmd = `# SLOTOPOL credentials + +To be able to view and change balance of users, the account must have the administrator access permission for working with *users*. To be able to view and change contents of the club bank, deposit and jackpot fund, the access permission for working with the *club* is required. +` + +const ( + hostRx = `^((http|https|ftp):\/\/)?(\w[\w_\-]*(\.\w[\w_\-]*)*)(:\d+)?$` + emailRx = `^\w[\w_\-\.]*@\w+\.\w{1,4}$` +) + +func (p *SigninPage) Create() { + // Backgroud image + p.underlay = &canvas.Image{ + Resource: AnyUnderlay(), + FillMode: canvas.ImageFillContain, + Translucency: 0.85, + } + + // Description + var descr = widget.NewRichTextFromMarkdown(descrmd) + descr.Wrapping = fyne.TextWrapWord + + // Form widgets + p.host = widget.NewEntry() + p.host.SetPlaceHolder("http://example.com:8080") + p.host.Validator = validation.NewRegexp(hostRx, "not a valid host") + p.host.Text = cfg.Credentials.Addr + p.email = widget.NewEntry() + p.email.SetPlaceHolder("test@example.com") + p.email.Validator = validation.NewRegexp(emailRx, "not a valid email") + p.email.Text = cfg.Credentials.Email + p.secret = widget.NewPasswordEntry() + p.secret.SetPlaceHolder("password") + p.secret.Text = cfg.Credentials.Secret + p.errmsg = widget.NewLabel("") + p.errmsg.Wrapping = fyne.TextWrapWord + p.form = &widget.Form{ + Items: []*widget.FormItem{ + {Text: "Host", Widget: p.host, HintText: "Host address of server"}, + {Text: "Email", Widget: p.email, HintText: "A valid registered email address"}, + {Text: "Secret", Widget: p.secret, HintText: "Password for authorization"}, + }, + } + + p.signinPage = container.NewStack( + NewImageFit(p.underlay), + container.NewVBox( + layout.NewSpacer(), + descr, + p.form, + layout.NewSpacer(), + p.errmsg, + ), + ) +} + type MainPage struct { // Backgroud image underlay *canvas.Image @@ -244,7 +261,7 @@ func (p *MainPage) Create() { // Main page p.mainPage = container.NewStack( - container.New(FitLayout{}, p.underlay), + NewImageFit(p.underlay), container.NewBorder( container.NewVBox(p.toolbar, p.clubTabs), nil, nil, nil, diff --git a/ui/startup.go b/ui/startup.go index 29e6f98..fa98dbe 100644 --- a/ui/startup.go +++ b/ui/startup.go @@ -13,20 +13,6 @@ import ( cfg "github.com/slotopol/balance/config" ) -func (f *Frame) MakeSignIn() (err error) { - if err = cfg.ReadCredentials(); err != nil { - log.Printf("failure on reading credentials, using default: %s\n", err.Error()) - err = nil // skip this error - return - } - if api.Admin, err = api.ReqSignIn(cfg.Credentials.Email, cfg.Credentials.Secret); err != nil { - return - } - f.loginTxt.SetText(fmt.Sprintf(cfg.Credentials.Email)) - log.Printf("signed as '%s'", cfg.Credentials.Email) - return -} - func (f *Frame) MakeClubList() (err error) { var cl api.RetClubList if cl, err = api.ReqClubList(); err != nil { @@ -113,7 +99,6 @@ func WaitToken() (err error) { func (f *Frame) StartupChain() { var chain = [](func() error){ - f.MakeSignIn, f.MakeClubList, f.MakeUserList, } @@ -126,19 +111,51 @@ func (f *Frame) StartupChain() { } func (f *Frame) CreateWindow(a fyne.App) { + var errCred error + if errCred = cfg.ReadCredentials(); errCred != nil { + log.Printf("failure on reading credentials, using default: %s\n", errCred.Error()) + return + } + f.MainPage.Create() + f.SigninPage.Create() - go f.StartupChain() go WaitToken() var w = a.NewWindow("Balance") w.Resize(fyne.NewSize(540, 640)) - w.SetContent(f.mainPage) + w.SetContent(f.signinPage) f.Window = w + var submit = func() { + f.loginTxt.SetText(cfg.Credentials.Email) + w.SetContent(f.mainPage) + go f.StartupChain() + f.SigninPage.form.OnCancel = func() { + w.SetContent(f.mainPage) + } + f.SigninPage.form.Refresh() + } + f.SigninPage.form.OnSubmit = func() { + var err error + if api.Admin, err = api.ReqSignIn(f.email.Text, f.secret.Text); err != nil { + f.errmsg.SetText(fmt.Sprintf("can not sign in with given credentials, %s", err.Error())) + return + } + cfg.Credentials.Addr = f.host.Text + cfg.Credentials.Email = f.email.Text + cfg.Credentials.Secret = f.secret.Text + log.Printf("signed as '%s'", cfg.Credentials.Email) + submit() + } + f.SigninPage.form.Refresh() f.userTable.SetColumnWidth(0, 180) // email f.userTable.SetColumnWidth(1, 100) // wallet f.userTable.SetColumnWidth(2, 50) // mtrp f.userTable.SetColumnWidth(3, 150) // access f.userTable.ExtendBaseWidget(f.userTable) + + if cfg.Credentials.Addr != "" && cfg.Credentials.Email != "" { + //submit() + } } diff --git a/ui/widgets.go b/ui/widgets.go new file mode 100644 index 0000000..68b9132 --- /dev/null +++ b/ui/widgets.go @@ -0,0 +1,72 @@ +package ui + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" +) + +// Label compatible with ToolbarItem interface to insert into Toolbar. +type ToolbarLabel struct { + widget.Label +} + +func NewToolbarLabel(text string) *ToolbarLabel { + var l = &ToolbarLabel{ + Label: widget.Label{ + Text: text, + Alignment: fyne.TextAlignLeading, + TextStyle: fyne.TextStyle{}, + }, + } + l.ExtendBaseWidget(l) + return l +} + +func (tl *ToolbarLabel) ToolbarObject() fyne.CanvasObject { + tl.Label.Importance = widget.LowImportance + return tl +} + +type CenterVBoxLayout struct { +} + +// Layout that fits the images to whole space and cuts edges if it needs. +type ImageFitLayout struct { +} + +func NewImageFit(objects ...fyne.CanvasObject) *fyne.Container { + return container.New(ImageFitLayout{}, objects...) +} + +func (l ImageFitLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + var ratiofit = size.Width / size.Height + for _, child := range objects { + var newsize = size + var pos = fyne.NewPos(0, 0) + if img, ok := child.(*canvas.Image); ok { + var ratioimg = img.Aspect() + if ratiofit > ratioimg { + newsize.Height = size.Width / ratioimg + pos.Y = (size.Height - newsize.Height) / 2 + } else { + newsize.Width = size.Height * ratioimg + pos.Y = (size.Width - newsize.Width) / 2 + } + } + child.Resize(newsize) + child.Move(pos) + } +} + +func (l ImageFitLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + var minSize = fyne.NewSize(0, 0) + for _, child := range objects { + if !child.Visible() { + continue + } + minSize = minSize.Max(child.MinSize()) + } + return minSize +}