Files
MrixsCraft-launcher/internal/ui/screens/screens.go
Vladimir Zagainov e1371e226a feat: implement dynamic server list loading from API
Add internal/api package to fetch server list from /api/servers.json endpoint

Replace hardcoded server list with dynamic API call

Implement loading states, error handling, and refresh capability
2026-06-10 20:12:03 +03:00

226 lines
5.8 KiB
Go

// package screens implements application screens (login, main menu, settings).
package screens
import (
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
"gitea.mrixs.me/Mrixs/MrixsCraft-launcher/internal/auth"
"gitea.mrixs.me/Mrixs/MrixsCraft-launcher/internal/ui/components"
)
// Modpack represents a single server/modpack entry from the backend.
type Modpack struct {
Slug string
Name string
}
// MainScreen builds the primary launcher window content.
func MainScreen(
w fyne.Window,
client *auth.Client,
session *auth.Session,
serverList []Modpack,
onPlay func(),
onSettings func(),
onServerListRefresh func(),
isServerListLoading bool,
serverListError error,
) fyne.CanvasObject {
// ── Sidebar (Left) ──────────────────────────────────────
var sidebar fyne.CanvasObject
if isServerListLoading {
// Show loading state
loadingLabel := widget.NewLabelWithStyle("Loading server list...", fyne.TextAlignCenter, fyne.TextStyle{})
sidebar = container.NewVScroll(container.NewVBox(loadingLabel))
} else if len(serverList) == 0 && serverListError == nil {
// Show empty state (no error, just empty list)
emptyLabel := widget.NewLabelWithStyle("No servers available", fyne.TextAlignCenter, fyne.TextStyle{})
sidebar = container.NewVScroll(container.NewVBox(emptyLabel))
} else if serverListError != nil {
// Show error state
errorLabel := widget.NewLabelWithStyle("Failed to load server list", fyne.TextAlignCenter, fyne.TextStyle{})
detailLabel := widget.NewLabel(serverListError.Error())
detailLabel.Wrapping = fyne.TextWrapWord
retryBtn := components.SettingsButton(onServerListRefresh)
retryBtn.Text = "Retry"
sidebar = container.NewVScroll(container.NewVBox(
errorLabel,
detailLabel,
retryBtn,
))
} else {
// Show normal server list
serverLabels := make([]*widget.Label, len(serverList))
serverContainer := container.NewVBox()
for i, sp := range serverList {
label := widget.NewLabel(sp.Name)
serverLabels[i] = label
index := i
card := components.ServerCard(sp.Name, index == 0, func() {
for j, l := range serverLabels {
l.TextStyle.Bold = (j == index)
l.Refresh()
}
})
serverContainer.Add(card)
}
sidebar = container.NewVScroll(serverContainer)
}
// ── Center (background + info) ──────────────────────────
bg := widget.NewRichTextFromMarkdown(
"# Welcome to MrixsCraft\n\n" +
"Select a server from the left panel and press **PLAY**.\n\n" +
"---\n\n" +
"*Version: dev*",
)
center := container.NewMax(widget.NewCard("", "", bg))
// ── Bottom bar ──────────────────────────────────────────
avatar := components.AvatarImage()
displayName := "Not logged in"
if session != nil && session.Username != "" {
displayName = session.Username
}
usernameLabel := widget.NewLabel(displayName)
logoutBtn := components.LogoutButton(func() {
dialog.ShowConfirm(
"Logout",
"Are you sure you want to log out?",
func(confirmed bool) {
if confirmed {
_ = auth.Delete()
w.Close()
}
},
w,
)
})
bottomLeft := container.NewHBox(
avatar,
usernameLabel,
layout.NewSpacer(),
logoutBtn,
)
progress := components.NewProgressBar()
playBtn := components.PlayButton(onPlay)
settingsBtn := components.SettingsButton(onSettings)
// Refresh button for server list
refreshBtn := components.SettingsButton(onServerListRefresh)
bottomRight := container.NewHBox(
settingsBtn,
widget.NewSeparator(),
refreshBtn,
widget.NewSeparator(),
playBtn,
)
bottomCenter := container.NewMax(progress)
bottom := container.NewBorder(
nil, nil,
bottomLeft,
bottomRight,
bottomCenter,
)
// ── Assemble ────────────────────────────────────────────
return container.NewBorder(
nil,
bottom,
sidebar,
nil,
center,
)
}
// LoginScreen builds a modal login form.
func LoginScreen(w fyne.Window, client *auth.Client, onLogin func(*auth.Session)) {
username := widget.NewEntry()
username.SetPlaceHolder("Username or email")
password := widget.NewPasswordEntry()
password.SetPlaceHolder("Password")
form := widget.NewForm(
widget.NewFormItem("Username", username),
widget.NewFormItem("Password", password),
)
dialog.ShowCustomConfirm(
"Login — MrixsCraft",
"Login",
"Cancel",
form,
func(confirmed bool) {
if !confirmed {
return
}
sess, err := client.Authenticate(username.Text, password.Text)
if err != nil {
dialog.ShowError(err, w)
return
}
if err := sess.Save(); err != nil {
dialog.ShowError(err, w)
return
}
onLogin(sess)
},
w,
)
}
// SettingsScreen builds the settings modal.
func SettingsScreen(w fyne.Window, onSave func(memoryMB int, extraArgs string)) {
memorySlider := widget.NewSlider(1024, 16384)
memorySlider.Step = 512
memorySlider.Value = 4096
memLabel := widget.NewLabel("4096 MB")
memorySlider.OnChanged = func(v float64) {
memLabel.SetText(fmt.Sprintf("%.0f MB", v))
}
extraArgs := widget.NewMultiLineEntry()
extraArgs.SetPlaceHolder("e.g. -XX:+UseG1GC -XX:MaxGCPauseMillis=50")
content := container.NewVBox(
widget.NewLabelWithStyle("Memory (RAM)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
memLabel,
memorySlider,
widget.NewSeparator(),
widget.NewLabelWithStyle("Extra JVM arguments", fyne.TextAlignLeading, fyne.TextStyle{}),
extraArgs,
)
dialog.ShowCustomConfirm(
"Settings",
"Save",
"Cancel",
content,
func(confirmed bool) {
if confirmed {
onSave(int(memorySlider.Value), extraArgs.Text)
}
},
w,
)
}