diff --git a/ui/view/event/event.go b/ui/view/event/event.go new file mode 100644 index 0000000..b6bf13f --- /dev/null +++ b/ui/view/event/event.go @@ -0,0 +1,121 @@ +// Package event provides the functions to draw all the upcoming zeus events on a TUI +package event + +import ( + "context" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/zeusWPI/scc/internal/pkg/db" + "github.com/zeusWPI/scc/internal/pkg/db/dto" + "github.com/zeusWPI/scc/pkg/config" + "github.com/zeusWPI/scc/pkg/util" + "github.com/zeusWPI/scc/ui/view" +) + +var ( + passedAmount = 3 + upcomingAmount = 7 +) + +// Model represents the model for the event view +type Model struct { + db *db.DB + passed []dto.Event + upcoming []dto.Event + today *dto.Event +} + +// Msg represents the message to update the event view +type Msg struct { + upcoming []dto.Event + passed []dto.Event + today *dto.Event +} + +// NewModel creates a new event view +func NewModel(db *db.DB) view.View { + return &Model{db: db} +} + +// Init initializes the event model view +func (m *Model) Init() tea.Cmd { + return nil +} + +// Name returns the name of the view +func (m *Model) Name() string { + return "Events" +} + +// Update updates the event model view +func (m *Model) Update(msg tea.Msg) (view.View, tea.Cmd) { + switch msg := msg.(type) { + case Msg: + m.passed = msg.passed + m.upcoming = msg.upcoming + m.today = msg.today + } + + return m, nil +} + +// View returns the view for the event model +func (m *Model) View() string { + if m.today != nil { + return m.viewToday() + } + + return m.viewNormal() +} + +// GetUpdateDatas returns all the update function for the event model +func (m *Model) GetUpdateDatas() []view.UpdateData { + return []view.UpdateData{ + { + Name: "event update", + View: m, + Update: updateEvents, + Interval: config.GetDefaultInt("tui.event.interval_s", 3600), + }, + } +} + +func updateEvents(view view.View) (tea.Msg, error) { + m := view.(*Model) + + eventsDB, err := m.db.Queries.GetEventsCurrentAcademicYear(context.Background()) + if err != nil { + return nil, err + } + + events := util.SliceMap(eventsDB, dto.EventDTO) + + passed := make([]dto.Event, 0) + upcoming := make([]dto.Event, 0) + var today *dto.Event + + now := time.Now() + for _, event := range events { + if event.Date.Before(now) { + passed = append(passed, *event) + } else { + upcoming = append(upcoming, *event) + } + + if event.Date.Year() == now.Year() && event.Date.YearDay() == now.YearDay() { + today = event + } + } + + // Truncate passed and upcoming slices + if len(passed) > passedAmount { + passed = passed[len(passed)-passedAmount:] + } + + if len(upcoming) > upcomingAmount { + upcoming = upcoming[:upcomingAmount] + } + + return Msg{passed: passed, upcoming: upcoming, today: today}, nil +} diff --git a/ui/view/event/style.go b/ui/view/event/style.go new file mode 100644 index 0000000..6c2f928 --- /dev/null +++ b/ui/view/event/style.go @@ -0,0 +1,56 @@ +package event + +import "github.com/charmbracelet/lipgloss" + +// Widths +var ( + widthToday = 45 + widthImage = 32 + + widthOverview = 45 + widthOverviewName = 35 + widthOverviewImage = 32 +) + +// Base +var ( + base = lipgloss.NewStyle() + baseToday = base.Width(widthToday).Align(lipgloss.Center) +) + +// Margins +var ( + mTodayWarning = 3 + mOverview = 5 +) + +// Color +var ( + cZeus = lipgloss.Color("#FF7F00") + cWarning = lipgloss.Color("#EE4B2B") + cBorder = lipgloss.Color("#383838") + cUpcoming = lipgloss.Color("#FFBF00") +) + +// Styles today +var ( + sTodayWarning = baseToday.Bold(true).Blink(true).Foreground(cWarning).Border(lipgloss.DoubleBorder(), true, false) + sTodayName = baseToday.Bold(true).Foreground(cZeus).BorderStyle(lipgloss.NormalBorder()).BorderBottom(true).BorderForeground(cBorder) + sTodayTime = baseToday + sTodayPlace = baseToday.Italic(true).Faint(true) + sToday = baseToday.MarginLeft(8).AlignVertical(lipgloss.Center) +) + +// Styles overview +var ( + sOverviewTitle = base.Bold(true).Foreground(cWarning).Width(widthOverview).Align(lipgloss.Center) + sOverview = base.Border(lipgloss.NormalBorder(), true, false, false, false).BorderForeground(cBorder).Width(widthOverview).MarginRight(mOverview) + sPassedName = base.Foreground(cZeus).Faint(true).Width(widthOverviewName) + sPassedTime = base.Faint(true) + sNextName = base.Bold(true).Foreground(cZeus).Width(widthOverviewName) + sNextTime = base.Bold(true) + sNextPlace = base.Italic(true).Width(widthOverviewName) + sUpcomingName = base.Width(widthOverviewName).Foreground(cUpcoming) + sUpcomingTime = base.Faint(true) + sUpcomingPlace = base.Italic(true).Faint(true).Width(widthOverviewName) +) diff --git a/ui/view/event/view.go b/ui/view/event/view.go new file mode 100644 index 0000000..16deb16 --- /dev/null +++ b/ui/view/event/view.go @@ -0,0 +1,111 @@ +package event + +import ( + "bytes" + "image" + + "github.com/charmbracelet/lipgloss" + "github.com/zeusWPI/scc/ui/view" +) + +func (m *Model) viewToday() string { + // Render image + im := "" + if m.today.Poster != nil { + i, _, err := image.Decode(bytes.NewReader(m.today.Poster)) + if err == nil { + im = view.ImagetoString(widthImage, i) + } + } + + // Render text + warningTop := sTodayWarning.MarginBottom(mTodayWarning).Render("🥳 Event Today 🥳") + warningBottom := sTodayWarning.MarginTop(mTodayWarning).Render("🥳 Event Today 🥳") + + name := sTodayName.Render(m.today.Name) + time := sTodayTime.Render("🕙 " + m.today.Date.Format("15:04")) + location := sTodayPlace.Render("📍 " + m.today.Location) + + text := lipgloss.JoinVertical(lipgloss.Left, warningTop, name, time, location, warningBottom) + + // Resize so it's centered + if lipgloss.Height(im) > lipgloss.Height(text) { + sToday = sToday.Height(lipgloss.Height(im)) + } + text = sToday.Render(text) + + return lipgloss.JoinHorizontal(lipgloss.Top, im, text) +} + +func (m *Model) viewNormal() string { + // Poster if present + im := "" + if len(m.upcoming) > 0 && m.upcoming[0].Poster != nil { + i, _, err := image.Decode(bytes.NewReader(m.upcoming[0].Poster)) + if err == nil { + im = view.ImagetoString(widthOverviewImage, i) + } + } + + // Overview + events := m.viewGetEvents() + + // Filthy hack to avoid the last event being centered by the cammie screen + events = append(events, "\n") + + // Render events overview + overview := lipgloss.JoinVertical(lipgloss.Left, events...) + overview = sOverview.Render(overview) + + title := sOverviewTitle.Render("Events") + overview = lipgloss.JoinVertical(lipgloss.Left, title, overview) + + // Combine image and overview + view := lipgloss.JoinHorizontal(lipgloss.Top, overview, im) + + return view +} + +func (m *Model) viewGetEvents() []string { + events := make([]string, 0, len(m.passed)+len(m.upcoming)) + + // Passed + for _, event := range m.passed { + time := sPassedTime.Render(event.Date.Format("02/01") + "\t") + name := sPassedName.Render(event.Name) + text := lipgloss.JoinHorizontal(lipgloss.Top, time, name) + + events = append(events, text) + } + + if len(m.upcoming) == 0 { + return events + } + + // Next + name := sNextName.Render(m.upcoming[0].Name) + time := sNextTime.Render(m.upcoming[0].Date.Format("02/01") + "\t") + location := sNextPlace.Render("📍 " + m.upcoming[0].Location) + + text := lipgloss.JoinVertical(lipgloss.Left, name, location) + text = lipgloss.JoinHorizontal(lipgloss.Top, time, text) + + events = append(events, text) + + // Upcoming + for i := 1; i < len(m.upcoming); i++ { + time := sUpcomingTime.Render(m.upcoming[i].Date.Format("02/01") + "\t") + name := sUpcomingName.Render(m.upcoming[i].Name) + text := name + if i < 3 { + location := sUpcomingPlace.Render("📍 " + m.upcoming[i].Location) + text = lipgloss.JoinVertical(lipgloss.Left, name, location) + } + + text = lipgloss.JoinHorizontal(lipgloss.Top, time, text) + + events = append(events, text) + } + + return events +}