mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 19:56:18 -04:00
Updated UI default next session detail
This commit is contained in:
@@ -55,12 +55,22 @@ type AppModel struct {
|
||||
trackMap TrackMapModel
|
||||
|
||||
meetings []models.Meeting
|
||||
nextInfo *statusBarTarget
|
||||
|
||||
// Splash screen state
|
||||
showSplash bool
|
||||
splashSpinner spinner.Model
|
||||
}
|
||||
|
||||
type statusBarTarget struct {
|
||||
meeting *models.Meeting
|
||||
current *models.Session
|
||||
next *models.Session
|
||||
loading bool
|
||||
stale bool
|
||||
err error
|
||||
}
|
||||
|
||||
func NewAppModel(client *api.OpenF1Client) AppModel {
|
||||
year := time.Now().Year()
|
||||
|
||||
@@ -105,6 +115,43 @@ func prefetchTrackOutlines(client *api.OpenF1Client, year int) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
type statusBarScheduleLoadedMsg struct {
|
||||
target *statusBarTarget
|
||||
}
|
||||
|
||||
func fetchStatusBarSchedule(client *api.OpenF1Client, meetings []models.Meeting) tea.Cmd {
|
||||
meetingsCopy := append([]models.Meeting(nil), meetings...)
|
||||
return func() tea.Msg {
|
||||
now := time.Now()
|
||||
target := &statusBarTarget{}
|
||||
|
||||
meeting := currentMeeting(meetingsCopy, now)
|
||||
if meeting == nil {
|
||||
meeting = nextUpcomingMeeting(meetingsCopy, now)
|
||||
}
|
||||
if meeting == nil {
|
||||
return statusBarScheduleLoadedMsg{target: target}
|
||||
}
|
||||
|
||||
target.meeting = meeting
|
||||
target.loading = true
|
||||
|
||||
sessions, err := client.GetSessionsForMeeting(int(meeting.MeetingKey))
|
||||
if err != nil {
|
||||
target.loading = false
|
||||
target.err = err
|
||||
target.stale = client.LastResponseWasStale()
|
||||
return statusBarScheduleLoadedMsg{target: target}
|
||||
}
|
||||
|
||||
sortSessionsByStart(sessions)
|
||||
target.current, target.next = currentAndNextSession(sessions, now)
|
||||
target.loading = false
|
||||
target.stale = client.LastResponseWasStale()
|
||||
return statusBarScheduleLoadedMsg{target: target}
|
||||
}
|
||||
}
|
||||
|
||||
func (m AppModel) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
m.dashboard.Init(),
|
||||
@@ -268,12 +315,18 @@ func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case meetingsLoadedMsg:
|
||||
if msg.err == nil {
|
||||
m.meetings = msg.meetings
|
||||
m.nextInfo = &statusBarTarget{loading: true}
|
||||
cmds = append(cmds, fetchStatusBarSchedule(m.client, msg.meetings))
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.calendar, cmd = m.calendar.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
return m, tea.Batch(cmds...)
|
||||
|
||||
case statusBarScheduleLoadedMsg:
|
||||
m.nextInfo = msg.target
|
||||
return m, nil
|
||||
|
||||
case sessionResultsLoadedMsg:
|
||||
var cmd tea.Cmd
|
||||
m.raceDetail, cmd = m.raceDetail.Update(msg)
|
||||
@@ -521,37 +574,51 @@ func (m AppModel) renderStatusBar(width int) string {
|
||||
Render("BOX-BOX"),
|
||||
}
|
||||
|
||||
// Next race countdown
|
||||
var nextMeeting *models.Meeting
|
||||
for i := range m.meetings {
|
||||
start, err := time.Parse(time.RFC3339, m.meetings[i].DateStart)
|
||||
if err != nil {
|
||||
start, _ = time.Parse("2006-01-02", m.meetings[i].DateStart[:min(len(m.meetings[i].DateStart), 10)])
|
||||
}
|
||||
if start.After(now) {
|
||||
nextMeeting = &m.meetings[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if m.nextInfo != nil && m.nextInfo.meeting != nil {
|
||||
flag := countryFlag(m.nextInfo.meeting.CountryCode)
|
||||
raceName := styleStatusValue.Render(m.nextInfo.meeting.MeetingName)
|
||||
|
||||
if nextMeeting != nil {
|
||||
start, err := time.Parse(time.RFC3339, nextMeeting.DateStart)
|
||||
if err == nil {
|
||||
diff := start.Sub(now)
|
||||
days := int(diff.Hours() / 24)
|
||||
hours := int(diff.Hours()) % 24
|
||||
|
||||
flag := countryFlag(nextMeeting.CountryCode)
|
||||
raceName := styleStatusValue.Render(nextMeeting.MeetingName)
|
||||
countdown := styleCountdown.Render(fmt.Sprintf("%dd %dh", days, hours))
|
||||
leftParts = append(leftParts, styleStatusLabel.Render("│"))
|
||||
|
||||
switch {
|
||||
case m.nextInfo.current != nil:
|
||||
leftParts = append(leftParts,
|
||||
styleStatusLabel.Render("│"),
|
||||
styleStatusLabel.Render("NEXT"),
|
||||
styleStatusLabel.Render("LIVE"),
|
||||
flag+" "+raceName,
|
||||
styleStatusLabel.Render("in"),
|
||||
countdown,
|
||||
styleStatusLabel.Render("·"),
|
||||
styleStatusValue.Render(m.nextInfo.current.SessionName),
|
||||
)
|
||||
case m.nextInfo.next != nil:
|
||||
if start, err := sessionStartTime(*m.nextInfo.next); err == nil {
|
||||
diff := start.Sub(now)
|
||||
days := int(diff.Hours() / 24)
|
||||
hours := int(diff.Hours()) % 24
|
||||
leftParts = append(leftParts,
|
||||
styleStatusLabel.Render("NEXT"),
|
||||
flag+" "+raceName,
|
||||
styleStatusLabel.Render("·"),
|
||||
styleStatusValue.Render(m.nextInfo.next.SessionName),
|
||||
styleStatusLabel.Render("in"),
|
||||
styleCountdown.Render(fmt.Sprintf("%dd %dh", days, hours)),
|
||||
)
|
||||
}
|
||||
case meetingHasStarted(*m.nextInfo.meeting, now):
|
||||
leftParts = append(leftParts,
|
||||
styleStatusLabel.Render("DONE"),
|
||||
flag+" "+raceName,
|
||||
)
|
||||
default:
|
||||
if start, err := meetingStartTime(*m.nextInfo.meeting); err == nil {
|
||||
diff := start.Sub(now)
|
||||
days := int(diff.Hours() / 24)
|
||||
hours := int(diff.Hours()) % 24
|
||||
leftParts = append(leftParts,
|
||||
styleStatusLabel.Render("NEXT"),
|
||||
flag+" "+raceName,
|
||||
styleStatusLabel.Render("in"),
|
||||
styleCountdown.Render(fmt.Sprintf("%dd %dh", days, hours)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -64,10 +63,7 @@ func fetchDashboardSessions(client *api.OpenF1Client, meetingKey int) tea.Cmd {
|
||||
if err != nil {
|
||||
return dashboardSessionsLoadedMsg{err: err}
|
||||
}
|
||||
// Sort by DateStart
|
||||
sort.Slice(sessions, func(i, j int) bool {
|
||||
return sessions[i].DateStart < sessions[j].DateStart
|
||||
})
|
||||
sortSessionsByStart(sessions)
|
||||
return dashboardSessionsLoadedMsg{sessions: sessions}
|
||||
}
|
||||
}
|
||||
@@ -113,29 +109,9 @@ func (m DashboardModel) Update(msg tea.Msg) (DashboardModel, tea.Cmd) {
|
||||
m.stale = true
|
||||
}
|
||||
now := time.Now()
|
||||
|
||||
// First priority: find a meeting that has already started but not yet ended
|
||||
// (i.e. we're currently in a GP weekend).
|
||||
for i := range m.meetings {
|
||||
mtg := m.meetings[i]
|
||||
start, _ := time.Parse(time.RFC3339, mtg.DateStart)
|
||||
end, _ := time.Parse(time.RFC3339, mtg.DateEnd)
|
||||
if now.After(start.Local()) && now.Before(end.Local().Add(24*time.Hour)) {
|
||||
m.next = &mtg
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Second priority: if no ongoing weekend, find the next upcoming meeting.
|
||||
m.next = currentMeeting(m.meetings, now)
|
||||
if m.next == nil {
|
||||
for i := range m.meetings {
|
||||
mtg := m.meetings[i]
|
||||
start, _ := time.Parse(time.RFC3339, mtg.DateStart)
|
||||
if now.Before(start.Local()) {
|
||||
m.next = &mtg
|
||||
break
|
||||
}
|
||||
}
|
||||
m.next = nextUpcomingMeeting(m.meetings, now)
|
||||
}
|
||||
|
||||
if m.next != nil {
|
||||
@@ -193,11 +169,8 @@ func (m DashboardModel) View() string {
|
||||
Foreground(lipgloss.Color(colorF1Red)).
|
||||
Bold(true)
|
||||
|
||||
// Determine if the weekend has started
|
||||
now := time.Now()
|
||||
meetingStart, _ := time.Parse(time.RFC3339, m.next.DateStart)
|
||||
meetingStart = meetingStart.Local()
|
||||
weekendUnderway := now.After(meetingStart)
|
||||
weekendUnderway := meetingHasStarted(*m.next, now)
|
||||
|
||||
sb.WriteString("\n")
|
||||
if weekendUnderway {
|
||||
@@ -207,42 +180,23 @@ func (m DashboardModel) View() string {
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" %s • %s\n", countryFlag(m.next.CountryCode), m.next.Location))
|
||||
|
||||
end, _ := time.Parse(time.RFC3339, m.next.DateEnd)
|
||||
endLocal := end.Local()
|
||||
|
||||
// Show countdown to next session if available, else use start date
|
||||
var nextStart time.Time
|
||||
var nextSession *models.Session
|
||||
startFound := false
|
||||
for i := range m.sessions {
|
||||
st, _ := time.Parse(time.RFC3339, m.sessions[i].DateStart)
|
||||
if now.Before(st.Local()) {
|
||||
nextStart = st.Local()
|
||||
nextSession = &m.sessions[i]
|
||||
startFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !startFound {
|
||||
nextStart, _ = time.Parse(time.RFC3339, m.next.DateStart)
|
||||
nextStart = nextStart.Local()
|
||||
}
|
||||
currentSession, nextSession := currentAndNextSession(m.sessions, now)
|
||||
|
||||
sb.WriteString("\n")
|
||||
if now.After(nextStart) && now.Before(endLocal) {
|
||||
if currentSession != nil {
|
||||
liveBadge := lipgloss.NewStyle().Background(lipgloss.Color(colorSoft)).Foreground(lipgloss.Color(colorSurface0)).Bold(true).Padding(0, 1).Render("LIVE")
|
||||
sb.WriteString(" " + liveBadge + "\n")
|
||||
} else if now.Before(nextStart) {
|
||||
diff := nextStart.Sub(now)
|
||||
days := int(diff.Hours() / 24)
|
||||
hours := int(diff.Hours()) % 24
|
||||
mins := int(diff.Minutes()) % 60
|
||||
secs := int(diff.Seconds()) % 60
|
||||
if nextSession != nil {
|
||||
sb.WriteString(fmt.Sprintf(" Next: %s in %dd %02dh %02dm %02ds\n", nextSession.SessionName, days, hours, mins, secs))
|
||||
sb.WriteString(fmt.Sprintf(" %s %s\n", liveBadge, currentSession.SessionName))
|
||||
} else if nextSession != nil {
|
||||
nextStart, err := sessionStartTime(*nextSession)
|
||||
if err != nil {
|
||||
sb.WriteString(" " + lipgloss.NewStyle().Foreground(lipgloss.Color(colorMuted)).Render("Schedule unavailable") + "\n")
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf(" Starts in: %dd %02dh %02dm %02ds\n", days, hours, mins, secs))
|
||||
diff := nextStart.Sub(now)
|
||||
days := int(diff.Hours() / 24)
|
||||
hours := int(diff.Hours()) % 24
|
||||
mins := int(diff.Minutes()) % 60
|
||||
secs := int(diff.Seconds()) % 60
|
||||
sb.WriteString(fmt.Sprintf(" Next: %s in %dd %02dh %02dm %02ds\n", nextSession.SessionName, days, hours, mins, secs))
|
||||
}
|
||||
} else {
|
||||
sb.WriteString(" " + lipgloss.NewStyle().Foreground(lipgloss.Color(colorMuted)).Render("Weekend Finished") + "\n")
|
||||
@@ -254,14 +208,18 @@ func (m DashboardModel) View() string {
|
||||
if len(m.sessions) > 0 {
|
||||
sb.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color(colorWhite)).Bold(true).Render(" WEEKEND SCHEDULE (Local Time)") + "\n")
|
||||
for _, s := range m.sessions {
|
||||
st, _ := time.Parse(time.RFC3339, s.DateStart)
|
||||
stLocal := st.Local()
|
||||
stLocal, err := sessionStartTime(s)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
day := stLocal.Format("Mon 02 Jan")
|
||||
tStr := stLocal.Format("15:04")
|
||||
|
||||
rowStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(colorWhite))
|
||||
if now.After(stLocal) {
|
||||
if end, err := sessionEndTime(s); err == nil && !now.Before(end) {
|
||||
rowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(colorMuted))
|
||||
} else if currentSession != nil && currentSession.SessionKey == s.SessionKey {
|
||||
rowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(colorSoft)).Bold(true)
|
||||
}
|
||||
|
||||
sb.WriteString(rowStyle.Render(fmt.Sprintf(" %-15s %-12s %s", s.SessionName, day, tStr)) + "\n")
|
||||
|
||||
140
internal/ui/schedule.go
Normal file
140
internal/ui/schedule.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/AmanTahiliani/box-box/internal/models"
|
||||
)
|
||||
|
||||
const defaultSessionDuration = 3 * time.Hour
|
||||
|
||||
func parseScheduleTime(value string) (time.Time, error) {
|
||||
if value == "" {
|
||||
return time.Time{}, fmt.Errorf("empty time value")
|
||||
}
|
||||
|
||||
t, err := time.Parse(time.RFC3339, value)
|
||||
if err == nil {
|
||||
return t.Local(), nil
|
||||
}
|
||||
|
||||
t, err = time.Parse("2006-01-02", value[:min(len(value), 10)])
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return t.Local(), nil
|
||||
}
|
||||
|
||||
func meetingStartTime(meeting models.Meeting) (time.Time, error) {
|
||||
return parseScheduleTime(meeting.DateStart)
|
||||
}
|
||||
|
||||
func meetingEndTime(meeting models.Meeting) (time.Time, error) {
|
||||
if meeting.DateEnd != "" {
|
||||
if end, err := parseScheduleTime(meeting.DateEnd); err == nil {
|
||||
return end, nil
|
||||
}
|
||||
}
|
||||
|
||||
start, err := meetingStartTime(meeting)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return start.Add(72 * time.Hour), nil
|
||||
}
|
||||
|
||||
func sessionStartTime(session models.Session) (time.Time, error) {
|
||||
return parseScheduleTime(session.DateStart)
|
||||
}
|
||||
|
||||
func sessionEndTime(session models.Session) (time.Time, error) {
|
||||
if session.DateEnd != "" {
|
||||
if end, err := parseScheduleTime(session.DateEnd); err == nil {
|
||||
return end, nil
|
||||
}
|
||||
}
|
||||
|
||||
start, err := sessionStartTime(session)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return start.Add(defaultSessionDuration), nil
|
||||
}
|
||||
|
||||
func sortSessionsByStart(sessions []models.Session) {
|
||||
sort.Slice(sessions, func(i, j int) bool {
|
||||
left, errLeft := sessionStartTime(sessions[i])
|
||||
right, errRight := sessionStartTime(sessions[j])
|
||||
if errLeft != nil || errRight != nil {
|
||||
return sessions[i].DateStart < sessions[j].DateStart
|
||||
}
|
||||
return left.Before(right)
|
||||
})
|
||||
}
|
||||
|
||||
func currentMeeting(meetings []models.Meeting, now time.Time) *models.Meeting {
|
||||
var selected *models.Meeting
|
||||
var latest time.Time
|
||||
|
||||
for i := range meetings {
|
||||
start, err := meetingStartTime(meetings[i])
|
||||
if err != nil || start.After(now) {
|
||||
continue
|
||||
}
|
||||
end, err := meetingEndTime(meetings[i])
|
||||
if err != nil || now.After(end.Add(24*time.Hour)) {
|
||||
continue
|
||||
}
|
||||
if selected == nil || start.After(latest) {
|
||||
selected = &meetings[i]
|
||||
latest = start
|
||||
}
|
||||
}
|
||||
|
||||
return selected
|
||||
}
|
||||
|
||||
func nextUpcomingMeeting(meetings []models.Meeting, now time.Time) *models.Meeting {
|
||||
for i := range meetings {
|
||||
start, err := meetingStartTime(meetings[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if start.After(now) {
|
||||
return &meetings[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func currentAndNextSession(sessions []models.Session, now time.Time) (*models.Session, *models.Session) {
|
||||
for i := range sessions {
|
||||
start, err := sessionStartTime(sessions[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
end, err := sessionEndTime(sessions[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if (now.Equal(start) || now.After(start)) && now.Before(end) {
|
||||
return &sessions[i], nil
|
||||
}
|
||||
if now.Before(start) {
|
||||
return nil, &sessions[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func meetingHasStarted(meeting models.Meeting, now time.Time) bool {
|
||||
start, err := meetingStartTime(meeting)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !now.Before(start)
|
||||
}
|
||||
Reference in New Issue
Block a user