package ui import ( "github.com/humdrum-tiv/sportsball/internal/config" "github.com/humdrum-tiv/sportsball/internal/model" ) // League visibility (TASK-002 / TASK-019) is a season-driven default with // per-league overrides: // // visible(l) = (inSeason(l) OR forceShow(l)) AND NOT forceHide(l) // // `order` is the user's display arrangement of the catalog; `hide`/`show` are // the explicit overrides. `App.leagues` is the resolved, ordered visible set // that the rest of the app reads. Toggling a league back to what the season // would do clears its override, so a league only stays pinned when the user // genuinely deviates from the seasonal default. // idSet builds a membership set from a slice of league-ID strings. func idSet(ids []string) map[model.LeagueID]bool { m := make(map[model.LeagueID]bool, len(ids)) for _, id := range ids { m[model.LeagueID(id)] = true } return m } // leagueOrderFromCfg parses the persisted order into known league IDs, dropping // unknown/duplicate entries. Empty → nil (meaning catalog order). func leagueOrderFromCfg(ids []string) []model.LeagueID { if len(ids) == 0 { return nil } seen := map[model.LeagueID]bool{} var out []model.LeagueID for _, s := range ids { id := model.LeagueID(s) if _, ok := model.LeagueByID(id); ok && !seen[id] { out = append(out, id) seen[id] = true } } return out } // orderedCatalog returns every catalog league in the user's display order: // `order` first (known IDs), then any leagues it omits in catalog order. func (a App) orderedCatalog() []model.League { if len(a.order) == 0 { return append([]model.League(nil), model.Leagues...) } seen := map[model.LeagueID]bool{} var out []model.League for _, id := range a.order { if l, ok := model.LeagueByID(id); ok && !seen[id] { out = append(out, l) seen[id] = true } } for _, l := range model.Leagues { if !seen[l.ID] { out = append(out, l) } } return out } // inSeasonOrUnprobed reports a league as in-season until the scan proves // otherwise, so startup shows everything and only narrows once confirmed empty. func (a App) inSeasonOrUnprobed(id model.LeagueID) bool { in, probed := a.inSeason[id] return in || !probed } // leagueVisible applies the override-over-season rule for one league. func (a App) leagueVisible(id model.LeagueID) bool { switch { case a.hide[id]: return false case a.show[id]: return true default: return a.inSeasonOrUnprobed(id) } } // applyAutoLeagues recomputes the effective visible league set (ordered), // returning true if it changed. Falls back so the app is never blank: if the // overrides hide everything, ignore force-hide; if still empty (all off-season), // show the whole catalog. If the active filter falls out, it resets to all. func (a *App) applyAutoLeagues() bool { catalog := a.orderedCatalog() var next []model.League for _, l := range catalog { if a.leagueVisible(l.ID) { next = append(next, l) } } if len(next) == 0 { for _, l := range catalog { if a.inSeasonOrUnprobed(l.ID) { next = append(next, l) } } } if len(next) == 0 { next = catalog } if sameLeagues(a.leagues, next) { return false } a.leagues = next if a.filter != "" { if _, ok := a.leagueIndex()[a.filter]; !ok { a.filter = "" a.cursor = 0 } } return true } // sameLeagues reports whether two league slices have the same IDs in order. func sameLeagues(a, b []model.League) bool { if len(a) != len(b) { return false } for i := range a { if a[i].ID != b[i].ID { return false } } return true } // toggleLeagueVisibility flips a league's effective visibility via overrides. // If the desired state matches what the season alone would do, the override is // cleared (back to auto); otherwise the matching force-hide/force-show is set. func (a *App) toggleLeagueVisibility(id model.LeagueID) { auto := a.inSeasonOrUnprobed(id) desired := !a.leagueVisible(id) delete(a.hide, id) delete(a.show, id) if desired == auto { return // back to seasonal default — no override needed } if desired { a.show[id] = true } else { a.hide[id] = true } } // moveLeague swaps the visible league at index i with its visible neighbor in // dir (-1 up / +1 down), reflecting the change in the persisted order. No-op at // the ends. func (a *App) moveLeague(i, dir int) { j := i + dir if i < 0 || i >= len(a.leagues) || j < 0 || j >= len(a.leagues) { return } // Establish a concrete order to mutate (catalog order until customized). if len(a.order) == 0 { for _, l := range a.orderedCatalog() { a.order = append(a.order, l.ID) } } p1 := indexOfID(a.order, a.leagues[i].ID) p2 := indexOfID(a.order, a.leagues[j].ID) if p1 >= 0 && p2 >= 0 { a.order[p1], a.order[p2] = a.order[p2], a.order[p1] } } func indexOfID(ids []model.LeagueID, id model.LeagueID) int { for i, x := range ids { if x == id { return i } } return -1 } // leagueIDs returns the visible leagues' IDs in display order. func (a App) leagueIDs() []model.LeagueID { ids := make([]model.LeagueID, len(a.leagues)) for i, l := range a.leagues { ids[i] = l.ID } return ids } // leagueIndex maps each visible league to its display position, a stable // tiebreak when ordering games across leagues. func (a App) leagueIndex() map[model.LeagueID]int { m := make(map[model.LeagueID]int, len(a.leagues)) for i, l := range a.leagues { m[l.ID] = i } return m } // settingsRows is the settings list: visible leagues first (in display order, // reorderable), then the hidden ones. enabled[i] reports row i's visibility. func (a App) settingsRows() (rows []model.League, enabled []bool) { visible := map[model.LeagueID]bool{} for _, l := range a.leagues { rows = append(rows, l) enabled = append(enabled, true) visible[l.ID] = true } for _, l := range a.orderedCatalog() { if !visible[l.ID] { rows = append(rows, l) enabled = append(enabled, false) } } return rows, enabled } // persistLeagues writes the current order + overrides to config (best-effort). func (a *App) persistLeagues() { a.cfg.LeagueOrder = idsToStrings(a.order) a.cfg.HideLeagues = setToStrings(a.hide) a.cfg.ShowLeagues = setToStrings(a.show) _ = config.Save(a.cfg) } func idsToStrings(ids []model.LeagueID) []string { if len(ids) == 0 { return nil } out := make([]string, len(ids)) for i, id := range ids { out[i] = string(id) } return out } // setToStrings serializes an override set in catalog order for stable output. func setToStrings(set map[model.LeagueID]bool) []string { var out []string for _, l := range model.Leagues { if set[l.ID] { out = append(out, string(l.ID)) } } return out }