internal/client (new): a typed Go HTTP client for the API - login, register, logout, me, and full CRUD for games/platforms - shared by the TUI and the Fyne GUI so request-building, auth and error handling (an APIError with the HTTP status) live in one tested place instead of being duplicated. 8 tests against an httptest server covering success, 401, 403 and that the token is stored/cleared at the right times. cmd/tui (rewrite): drops the old unauthenticated read-only viewer. Now a login screen (bubbles/textinput) -> a scrollable games table (bubbles/table) with 'r' to refresh, built as an explicit state machine (screenLogin/screenLoading/screenGames) so Update's transitions are testable without a real terminal. 9 tests driving Update() directly. cmd/gui (rewrite): drops raylib for Fyne. Same shape as the TUI - a login form, then a games table, with networking on a goroutine and fyne.Do for the UI updates. The table-cell and login-validation logic are plain functions with no Fyne/display dependency, so they're tested directly (7 tests) rather than needing Fyne's headless test driver. internal/api: tagged every operation (Auth/Platforms/Games/Misc) so the generated OpenAPI spec groups into named services instead of one bucket; api/openapi.json regenerated. Added CORS middleware (wildcard origin, no credentials needed since clients use a bearer token, not the cookie) - needed for the new web client's browser requests to the API's a different origin. web/ (new): an Angular 22 + Material app, API client generated from api/openapi.json via ng-openapi-gen (services: true for per-resource classes - AuthService/GamesService/PlatformsService - instead of one generic Api facade). A Session service holds the token/user as signals, persists the token to localStorage, and is the only thing that calls the generated AuthService; an HttpInterceptorFn attaches the bearer token to every request; a CanActivateFn guard protects /games. Login and Games are standalone, lazy-loaded route components. 21 tests (Vitest, Angular's new default - no browser/Karma needed) covering the service, interceptor, guard and both components, run via `npm test` exactly as CI will call it. Justfile: fixed run-tui/run-gui, which used `go run .../main.go` - broken now that both packages span multiple files, since that only compiles the one named file. Added web-install/web-dev/web-build/web-test. .gitea/workflows/ci.yml (moved from .github, which Gitea doesn't read): on push to main/develop - gofmt, go vet, go build, go test, go mod tidy diff (with the Fyne build's system deps installed), sqlc diff, and the web app's build+test. .gitea/workflows/release.yml (new): on a vX.Y.Z tag, cross-compiles api+tui for linux/darwin/windows (amd64+arm64, pure Go, no cgo), builds the Fyne GUI for linux/amd64 only (cgo + its OpenGL/X11 link means it can't cross-compile from one Linux runner the way api/tui do, and this repo has no registered macOS/Windows runner to build it on natively - documented in the workflow's comments), builds the web app, and creates a Gitea release with all of it attached via the Gitea API. Written to Gitea's documented GitHub-Actions-compatible syntax but not executable from here - there's no Gitea server in this environment to run it against, unlike everything else in this commit, which was. Verified: every Go package builds/vets/tests clean; sqlc diff and go mod tidy -diff are clean; the Angular app builds for production and its test suite passes via `npm test`/`npm run build` (the exact commands CI runs), including after a clean `npm ci` from the committed lockfile; the new CORS config was checked with real preflight and GET requests against a running server, not just read. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
118 lines
3.0 KiB
Go
118 lines
3.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/dialog"
|
|
"fyne.io/fyne/v2/widget"
|
|
|
|
"completed/internal/client"
|
|
"completed/internal/model"
|
|
)
|
|
|
|
// guiApp holds the window and the single client all screens share. It's a
|
|
// small base: log in, see your games in a table, refresh. Adding/editing
|
|
// games is for a later pass.
|
|
type guiApp struct {
|
|
client *client.Client
|
|
window fyne.Window
|
|
}
|
|
|
|
func newGUIApp(c *client.Client, w fyne.Window) *guiApp {
|
|
return &guiApp{client: c, window: w}
|
|
}
|
|
|
|
// showLogin builds and displays the login form. It's the window's initial
|
|
// content.
|
|
func (a *guiApp) showLogin() {
|
|
email := widget.NewEntry()
|
|
email.SetPlaceHolder("email")
|
|
password := widget.NewPasswordEntry()
|
|
password.SetPlaceHolder("password")
|
|
status := widget.NewLabel("")
|
|
|
|
form := widget.NewForm(
|
|
widget.NewFormItem("Email", email),
|
|
widget.NewFormItem("Password", password),
|
|
)
|
|
form.SubmitText = "Log in"
|
|
form.OnSubmit = func() {
|
|
if err := validateLogin(email.Text, password.Text); err != nil {
|
|
status.SetText(err.Error())
|
|
return
|
|
}
|
|
status.SetText("Logging in...")
|
|
a.login(email.Text, password.Text, status)
|
|
}
|
|
|
|
a.window.SetContent(container.NewVBox(
|
|
widget.NewLabelWithStyle("Completed", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
|
form,
|
|
status,
|
|
))
|
|
}
|
|
|
|
// login runs the login request off the UI goroutine, then either shows the
|
|
// games screen or reports the error back on the login form - both via
|
|
// fyne.Do, since Fyne widgets may only be touched from the UI goroutine.
|
|
func (a *guiApp) login(email, password string, status *widget.Label) {
|
|
go func() {
|
|
_, err := a.client.Login(context.Background(), email, password)
|
|
if err != nil {
|
|
fyne.Do(func() { status.SetText(err.Error()) })
|
|
return
|
|
}
|
|
fyne.Do(a.showGames)
|
|
}()
|
|
}
|
|
|
|
// showGames displays the games table and fetches the current list.
|
|
func (a *guiApp) showGames() {
|
|
var games []model.Game
|
|
|
|
table := widget.NewTable(
|
|
func() (int, int) { return gameTableSize(games) },
|
|
func() fyne.CanvasObject { return widget.NewLabel("") },
|
|
func(id widget.TableCellID, obj fyne.CanvasObject) {
|
|
obj.(*widget.Label).SetText(gameCellText(games, id.Row, id.Col))
|
|
},
|
|
)
|
|
for i := range gameColumns {
|
|
table.SetColumnWidth(i, 120)
|
|
}
|
|
|
|
status := widget.NewLabel("Loading...")
|
|
refresh := widget.NewButton("Refresh", func() {
|
|
a.fetchGames(table, &games, status)
|
|
})
|
|
|
|
a.window.SetContent(container.NewBorder(
|
|
container.NewVBox(
|
|
widget.NewLabelWithStyle("Completed", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
|
container.NewHBox(refresh, status),
|
|
),
|
|
nil, nil, nil,
|
|
table,
|
|
))
|
|
|
|
a.fetchGames(table, &games, status)
|
|
}
|
|
|
|
func (a *guiApp) fetchGames(table *widget.Table, games *[]model.Game, status *widget.Label) {
|
|
go func() {
|
|
result, err := a.client.ListGames(context.Background())
|
|
fyne.Do(func() {
|
|
if err != nil {
|
|
status.SetText("Error: " + err.Error())
|
|
dialog.ShowError(err, a.window)
|
|
return
|
|
}
|
|
*games = result
|
|
status.SetText("")
|
|
table.Refresh()
|
|
})
|
|
}()
|
|
}
|