This commit is contained in:
Tomas Dvorak
2025-10-24 14:52:46 +02:00
parent 70ea0c3c91
commit 8a7c292e54
41 changed files with 912 additions and 404 deletions
+9
View File
@@ -129,6 +129,7 @@ func (pc *PollController) CreatePoll(c *gin.Context) {
Title string `json:"title" binding:"required"`
Description string `json:"description"`
Type string `json:"type"`
Style string `json:"style"`
Status string `json:"status"`
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
@@ -166,6 +167,7 @@ func (pc *PollController) CreatePoll(c *gin.Context) {
Title: input.Title,
Description: input.Description,
Type: input.Type,
Style: input.Style,
Status: input.Status,
StartDate: input.StartDate,
EndDate: input.EndDate,
@@ -188,6 +190,9 @@ func (pc *PollController) CreatePoll(c *gin.Context) {
if poll.Type == "" {
poll.Type = "single"
}
if poll.Style == "" {
poll.Style = "auto"
}
if poll.Status == "" {
poll.Status = "draft"
}
@@ -250,6 +255,7 @@ func (pc *PollController) UpdatePoll(c *gin.Context) {
Title *string `json:"title"`
Description *string `json:"description"`
Type *string `json:"type"`
Style *string `json:"style"`
Status *string `json:"status"`
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
@@ -282,6 +288,9 @@ func (pc *PollController) UpdatePoll(c *gin.Context) {
if input.Type != nil {
poll.Type = *input.Type
}
if input.Style != nil {
poll.Style = *input.Style
}
if input.Status != nil {
poll.Status = *input.Status
}
+40 -18
View File
@@ -144,36 +144,37 @@ func averageHex(img image.Image) string {
return fmt.Sprintf("#%02x%02x%02x", r8, g8, b8)
}
// SwapSides swaps home and away team info including names, logos, shorts, scores and colors.
// SwapSides toggles visual sides flipping only. It does NOT swap team data.
func (c *ScoreboardController) SwapSides(ctx *gin.Context) {
s, err := c.getOrCreateSingleton()
if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot load scoreboard"}); return }
s.HomeName, s.AwayName = s.AwayName, s.HomeName
s.HomeLogoURL, s.AwayLogoURL = s.AwayLogoURL, s.HomeLogoURL
s.HomeScore, s.AwayScore = s.AwayScore, s.HomeScore
s.HomeShort, s.AwayShort = s.AwayShort, s.HomeShort
s.PrimaryColor, s.SecondaryColor = s.SecondaryColor, s.PrimaryColor
s.SidesFlipped = !s.SidesFlipped
if err := c.DB.Save(s).Error; err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save"}); return
}
ctx.JSON(http.StatusOK, gin.H{"ok": true})
}
// StartSecondHalf swaps sides, resets the timer to 00:00 and immediately starts it.
// StartSecondHalf starts the second half without flipping visual sides and continues timer from end of 1st half.
func (c *ScoreboardController) StartSecondHalf(ctx *gin.Context) {
s, err := c.getOrCreateSingleton()
if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot load scoreboard"}); return }
// swap first
s.HomeName, s.AwayName = s.AwayName, s.HomeName
s.HomeLogoURL, s.AwayLogoURL = s.AwayLogoURL, s.HomeLogoURL
s.HomeScore, s.AwayScore = s.AwayScore, s.HomeScore
s.HomeShort, s.AwayShort = s.AwayShort, s.HomeShort
s.PrimaryColor, s.SecondaryColor = s.SecondaryColor, s.PrimaryColor
// reset and start timer for next half
// Move to second half and continue from end of first half
s.Half = 2
// Ensure base elapsed reflects end of first half
capFirst := s.HalfLength * 60
if capFirst <= 0 { capFirst = 45 * 60 }
base := s.ElapsedSeconds
if s.Running && s.TimerStartUnix > 0 {
now := time.Now().Unix()
diff := int(now - s.TimerStartUnix)
if diff > base { base = diff }
}
if base < capFirst { base = capFirst }
s.ElapsedSeconds = base
s.Timer = formatSeconds(base)
s.Running = true
s.ElapsedSeconds = 0
s.TimerStartUnix = time.Now().Unix()
s.Timer = "00:00"
s.TimerStartUnix = time.Now().Unix() - int64(base)
if err := c.DB.Save(s).Error; err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save"}); return
}
@@ -260,6 +261,10 @@ func applyImportedState(imported models.ScoreboardState, c *ScoreboardController
if imported.SecondaryColor != "" { s.SecondaryColor = imported.SecondaryColor }
s.HomeScore = imported.HomeScore
s.AwayScore = imported.AwayScore
// fouls with clamping
clamp := func(v int) int { if v < 0 { return 0 }; if v > 5 { return 5 }; return v }
s.HomeFouls = clamp(imported.HomeFouls)
s.AwayFouls = clamp(imported.AwayFouls)
if imported.HalfLength > 0 { s.HalfLength = imported.HalfLength }
if imported.Theme != "" { s.Theme = imported.Theme }
// timer handling
@@ -321,9 +326,10 @@ func computeTimer(s models.ScoreboardState) (timer string, running bool) {
if diff > 0 { base = diff } else { base = 0 }
}
}
// Cap by half length
// Cap by half length; allow up to 2*half when second half is active
cap := s.HalfLength * 60
if cap <= 0 { cap = 45 * 60 }
if s.Half >= 2 { cap = s.HalfLength * 120 }
if base >= cap {
base = cap
running = false
@@ -411,6 +417,8 @@ func (c *ScoreboardController) getOrCreateSingleton() (*models.ScoreboardState,
Half: 1,
QRShowEveryMinutes: 5,
QRShowDurationSeconds: 60,
HomeFouls: 0,
AwayFouls: 0,
}
if err := c.DB.Create(&s).Error; err != nil {
return nil, err
@@ -424,6 +432,12 @@ func (c *ScoreboardController) getOrCreateSingleton() (*models.ScoreboardState,
if s.Half == 0 { s.Half = 1; changed = true }
if s.QRShowEveryMinutes == 0 { s.QRShowEveryMinutes = 5; changed = true }
if s.QRShowDurationSeconds == 0 { s.QRShowDurationSeconds = 60; changed = true }
// Clamp fouls 0..5 and ensure non-negative
clamp := func(v int) int { if v < 0 { return 0 }; if v > 5 { return 5 }; return v }
nf := clamp(s.HomeFouls)
af := clamp(s.AwayFouls)
if s.HomeFouls != nf { s.HomeFouls = nf; changed = true }
if s.AwayFouls != af { s.AwayFouls = af; changed = true }
if changed { _ = c.DB.Save(&s).Error }
return &s, nil
}
@@ -447,6 +461,8 @@ func (c *ScoreboardController) GetPublic(ctx *gin.Context) {
"secondaryColor": s.SecondaryColor,
"homeScore": s.HomeScore,
"awayScore": s.AwayScore,
"homeFouls": s.HomeFouls,
"awayFouls": s.AwayFouls,
"halfLength": s.HalfLength,
"theme": s.Theme,
"external_match_id": s.ExternalMatchID,
@@ -491,6 +507,8 @@ func (c *ScoreboardController) PutAdmin(ctx *gin.Context) {
SecondaryColor *string `json:"secondaryColor"`
HomeScore *int `json:"homeScore"`
AwayScore *int `json:"awayScore"`
HomeFouls *int `json:"homeFouls"`
AwayFouls *int `json:"awayFouls"`
HalfLength *int `json:"halfLength"`
Theme *string `json:"theme"`
ExternalMatchID *string `json:"externalMatchId"`
@@ -523,6 +541,10 @@ func (c *ScoreboardController) PutAdmin(ctx *gin.Context) {
if payload.SecondaryColor != nil { s.SecondaryColor = *payload.SecondaryColor }
if payload.HomeScore != nil { s.HomeScore = *payload.HomeScore }
if payload.AwayScore != nil { s.AwayScore = *payload.AwayScore }
// Clamp fouls 0..5
clamp := func(v int) int { if v < 0 { return 0 }; if v > 5 { return 5 }; return v }
if payload.HomeFouls != nil { s.HomeFouls = clamp(*payload.HomeFouls) }
if payload.AwayFouls != nil { s.AwayFouls = clamp(*payload.AwayFouls) }
if payload.HalfLength != nil { s.HalfLength = *payload.HalfLength }
if payload.Theme != nil { s.Theme = *payload.Theme }
if payload.ExternalMatchID != nil { s.ExternalMatchID = *payload.ExternalMatchID }
+2 -1
View File
@@ -9,7 +9,8 @@ type Poll struct {
ID uint `gorm:"primarykey" json:"id"`
Title string `gorm:"size:255;not null" json:"title"`
Description string `gorm:"type:text" json:"description"`
Type string `gorm:"size:50;not null;default:'single'" json:"type"` // single, multiple, rating
Type string `gorm:"size:50;not null;default:'single'" json:"type"`
Style string `gorm:"size:50;not null;default:'auto'" json:"style"`
Status string `gorm:"size:20;not null;default:'draft'" json:"status"` // draft, active, closed, archived
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
+3
View File
@@ -33,6 +33,9 @@ type ScoreboardState struct {
// QR overlay schedule settings
QRShowEveryMinutes int `json:"qr_show_every_minutes"`
QRShowDurationSeconds int `json:"qr_show_duration_seconds"`
// Team fouls (0..5 display dots)
HomeFouls int `json:"home_fouls"`
AwayFouls int `json:"away_fouls"`
}
func (ScoreboardState) TableName() string { return "scoreboard_states" }