diff --git a/components/match_ticker/commons/match_ticker.lua b/components/match_ticker/commons/match_ticker.lua index 89040bc6af2..04e3617728a 100644 --- a/components/match_ticker/commons/match_ticker.lua +++ b/components/match_ticker/commons/match_ticker.lua @@ -58,6 +58,17 @@ local DEFAULT_RECENT_ORDER = 'date desc, liquipediatier asc, tournament asc' local DEFAULT_LIVE_HOURS = 8 local NOW = os.date('%Y-%m-%d %H:%M', os.time(os.date('!*t') --[[@as osdateparam]])) +--- Extract externally if it grows +---@param matchTickerConfig MatchTickerConfig +---@return unknown # Todo: Add interface for MatchTickerDisplay +local MatchTickerDisplayFactory = function (matchTickerConfig) + if matchTickerConfig.newStyle then + return Lua.import('Module:MatchTicker/DisplayComponents/New') + else + return Lua.import('Module:MatchTicker/DisplayComponents') + end +end + ---@class MatchTickerConfig ---@field tournaments string[] ---@field limit integer @@ -80,6 +91,7 @@ local NOW = os.date('%Y-%m-%d %H:%M', os.time(os.date('!*t') --[[@as osdateparam ---@field onlyHighlightOnValue string? ---@field tiers string[]? ---@field tierTypes string[]? +---@field newStyle boolean? ---@class MatchTicker ---@operator call(table): MatchTicker @@ -88,8 +100,6 @@ local NOW = os.date('%Y-%m-%d %H:%M', os.time(os.date('!*t') --[[@as osdateparam ---@field matches table[]? local MatchTicker = Class.new(function(self, args) self:init(args) end) -MatchTicker.DisplayComponents = Lua.import('Module:MatchTicker/DisplayComponents') - ---@param args table? ---@return table function MatchTicker:init(args) @@ -124,6 +134,7 @@ function MatchTicker:init(args) tierTypes = args.tiertypes and Array.filter( Array.parseCommaSeparatedString(args.tiertypes), FnUtil.curry(Tier.isValid, 1) ) or nil, + newStyle = Logic.readBool(args.newStyle), } --min 1 of them has to be set; recent can not be set while any of the others is set @@ -164,6 +175,8 @@ function MatchTicker:init(args) end config.wrapperClasses = wrapperClasses + MatchTicker.DisplayComponents = MatchTickerDisplayFactory(config) + self.config = config return self diff --git a/components/match_ticker/commons/match_ticker_custom.lua b/components/match_ticker/commons/match_ticker_custom.lua index 609882795e7..7ce33296b20 100644 --- a/components/match_ticker/commons/match_ticker_custom.lua +++ b/components/match_ticker/commons/match_ticker_custom.lua @@ -45,6 +45,15 @@ function CustomMatchTicker.mainPage(frame) return MatchTicker(args):query():create() end +---Entry point for display on the main page with the new style +---@param frame Frame? +---@return Html +function CustomMatchTicker.newMainPage(frame) + local args = Arguments.getArgs(frame) + args.newStyle = true + return MatchTicker(args):query():create():addClass('new-match-style') +end + ---Entry point for display on player pages ---@param frame Frame? ---@return Html diff --git a/components/match_ticker/commons/match_ticker_display_components.lua b/components/match_ticker/commons/match_ticker_display_components.lua index 5c867587eb6..6e54c949db8 100644 --- a/components/match_ticker/commons/match_ticker_display_components.lua +++ b/components/match_ticker/commons/match_ticker_display_components.lua @@ -98,7 +98,7 @@ function Versus:create() return self.root :node(mw.html.create('div') - :css('line-height', '1.1'):node(upperText or VS) + :addClass('versus-upper'):node(upperText or VS) ):node(mw.html.create('div') :addClass('versus-lower'):wikitext('(' .. lowerText .. ')') ) @@ -123,6 +123,7 @@ function Versus:scores() local scores, scores2 = {}, {} local hasSecondScore + local delimiter = ':' local setWinner = function(score, opponentIndex) if winner == opponentIndex then @@ -135,20 +136,20 @@ function Versus:scores() local score = Logic.isNotEmpty(opponent.status) and opponent.status ~= SCORE_STATUS and opponent.status or tonumber(opponent.score) or -1 - table.insert(scores, setWinner(score ~= -1 and score or 0, opponentIndex)) + table.insert(scores, '' .. setWinner(score ~= -1 and score or 0, opponentIndex) .. '' ) local score2 = tonumber((opponent.extradata or {}).score2) or 0 - table.insert(scores2, setWinner(score2, opponentIndex)) + table.insert(scores2, '' .. setWinner(score2, opponentIndex) .. '' ) if score2 > 0 then hasSecondScore = true end end) if hasSecondScore then - return table.concat(scores, ':'), table.concat(scores2, ':') + return table.concat(scores, delimiter), table.concat(scores2, delimiter) end - return table.concat(scores, ':') + return table.concat(scores, delimiter) end ---Display class for matches shown within a match ticker diff --git a/components/match_ticker/commons/match_ticker_display_components_new.lua b/components/match_ticker/commons/match_ticker_display_components_new.lua new file mode 100644 index 00000000000..9473a9ff0db --- /dev/null +++ b/components/match_ticker/commons/match_ticker_display_components_new.lua @@ -0,0 +1,240 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:MatchTicker/DisplayComponents/New +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +-- Holds DisplayComponents for the MatchTicker module +-- It contains the new html structure intented to be use for the new Dota2 Main Page (for now) +-- Will most likely be expanded to other games in the future and other pages + +local Class = require('Module:Class') +local Countdown = require('Module:Countdown') +local DateExt = require('Module:Date/Ext') +local LeagueIcon = require('Module:LeagueIcon') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') +local Timezone = require('Module:Timezone') +local StreamLinks = require('Module:Links/Stream') +local Page = require('Module:Page') +local DefaultMatchTickerDisplayComponents = require('Module:MatchTicker/DisplayComponents') + +local HighlightConditions = Lua.import('Module:HighlightConditions') + +local OpponentLibraries = require('Module:OpponentLibraries') +local Opponent = OpponentLibraries.Opponent +local OpponentDisplay = OpponentLibraries.OpponentDisplay + +local CURRENT_PAGE = mw.title.getCurrentTitle().text +local HIGHLIGHT_CLASS = 'tournament-highlighted-bg' +local TOURNAMENT_DEFAULT_ICON = 'Generic_Tournament_icon.png' + +---Display class for matches shown within a match ticker +---@class NewMatchTickerScoreBoard +---@operator call(table): NewMatchTickerScoreBoard +---@field root Html +---@field match table +local ScoreBoard = Class.new( + function(self, match) + self.root = mw.html.create('div'):addClass('match-scoreboard') + self.match = match + end +) + +---@return Html +function ScoreBoard:create() + local match = self.match + local winner = tonumber(match.winner) + + return self.root + :node(self:opponent(match.match2opponents[1], winner == 1, true):addClass('team-left')) + :node(self:versus()) + :node(self:opponent(match.match2opponents[2], winner == 2):addClass('team-right')) +end + +---@param opponentData table +---@param isWinner boolean +---@param flip boolean? +---@return Html +function ScoreBoard:opponent(opponentData, isWinner, flip) + local opponent = Opponent.fromMatch2Record(opponentData) + ---@cast opponent -nil + if Opponent.isEmpty(opponent) or Opponent.isTbd(opponent) and opponent.type ~= Opponent.literal then + opponent = Opponent.tbd(Opponent.literal) + end + + local opponentName = Opponent.toName(opponent) + if not opponentName then + mw.logObject(opponent, 'Invalid Opponent, Opponent.toName returns nil') + opponentName = '' + end + + local opponentDisplay = mw.html.create('div') + :node(OpponentDisplay.InlineOpponent{ + opponent = opponent, + teamStyle = 'short', + flip = flip, + showLink = opponentName:gsub('_', ' ') ~= CURRENT_PAGE + }) + + if isWinner then + opponentDisplay:addClass('match-winner') + end + + return opponentDisplay +end + +---@return Html +function ScoreBoard:versus() + return mw.html.create('div') + :addClass('versus') + :node(DefaultMatchTickerDisplayComponents.Versus(self.match):create()) +end + +---Display class for the details of a match displayed at the bottom of a match ticker +---@class NewMatchTickerDetails +---@operator call(table): NewMatchTickerMatch +---@field root Html +---@field hideTournament boolean +---@field onlyHighlightOnValue string? +---@field match table +local Details = Class.new( + function(self, args) + assert(args.match, 'No Match passed to MatchTickerDetails class') + self.root = mw.html.create('div'):addClass('match-details') + self.hideTournament = args.hideTournament + self.onlyHighlightOnValue = args.onlyHighlightOnValue + self.match = args.match + end +) + +---@return Html +function Details:create() + local highlightCondition = HighlightConditions.match or HighlightConditions.tournament + if highlightCondition(self.match, {onlyHighlightOnValue = self.onlyHighlightOnValue}) then + self.root:addClass(HIGHLIGHT_CLASS) + end + + return self.root + :node(self:streams()) + :node(self:tournament()) + :node(self:countdown()) +end + +---It will display both countdown and date of the match so the user can select which one to show +---@return Html +function Details:countdown() + local match = self.match + + local dateString + if Logic.readBool(match.dateexact) then + local timestamp = DateExt.readTimestamp(match.date) + (Timezone.getOffset(match.extradata.timezoneid) or 0) + dateString = DateExt.formatTimestamp('F j, Y - H:i', timestamp) .. ' ' + .. (Timezone.getTimezoneString(match.extradata.timezoneid) or (Timezone.getTimezoneString('UTC'))) + else + dateString = mw.getContentLanguage():formatDate('F j, Y', match.date) .. (Timezone.getTimezoneString('UTC')) + end + + local countdownArgs = { + date = dateString, + finished = match.finished + } + + local countdownDisplay = mw.html.create('span') + :addClass('match-countdown') + :node(Countdown._create(countdownArgs)) + + return countdownDisplay +end + +---@return Html? +function Details:streams() + local match = self.match + local links = mw.html.create('div') + :addClass('match-streams') + + links:wikitext(table.concat(StreamLinks.buildDisplays(StreamLinks.filterStreams(match.stream)) or {})) + + return links +end + +---@return Html? +function Details:tournament() + if self.hideTournament then + return + end + + local match = self.match + + local icon = LeagueIcon.display{ + icon = Logic.emptyOr(match.icon, TOURNAMENT_DEFAULT_ICON), + iconDark = match.icondark, + link = match.pagename, + name = match.tournament, + options = {noTemplate = true}, + } + + local displayName = Logic.emptyOr( + match.tickername, + match.tournament, + match.parent:gsub('_', ' ') + ) + + return mw.html.create('div') + :addClass('match-tournament') + :node(mw.html.create('div') + :addClass('tournament-icon') + :node(mw.html.create('div') + :wikitext(icon) + ) + ) + :node(mw.html.create('div') + :addClass('tournament-text') + :wikitext(Page.makeInternalLink({}, displayName, match.pagename)) + ) + +end + +---Display class for matches shown within a match ticker +---@class NewMatchTickerMatch +---@operator call({config: MatchTickerConfig, match: table}): NewMatchTickerMatch +---@field root Html +---@field config MatchTickerConfig +---@field match table +local Match = Class.new( + function(self, args) + self.root = mw.html.create('div'):addClass('match') + self.config = args.config + self.match = args.match + end +) + +---@return Html +function Match:create() + self.root:node(self:standardMatchRow()) + self.root:node(self:detailsRow()) + + return self.root +end + +---@return Html +function Match:standardMatchRow() + return ScoreBoard(self.match):create() +end + +---@return Html +function Match:detailsRow() + return Details{ + match = self.match, + hideTournament = self.config.hideTournament, + onlyHighlightOnValue = self.config.onlyHighlightOnValue + }:create() +end + +return { + Match = Match, + Details = Details, + ScoreBoard = ScoreBoard, +} diff --git a/stylesheets/commons/MatchTicker.less b/stylesheets/commons/MatchTicker.less new file mode 100644 index 00000000000..78a4381c707 --- /dev/null +++ b/stylesheets/commons/MatchTicker.less @@ -0,0 +1,144 @@ +/******************************************************************************* +Template(s): Match Ticker +Author(s): Nadox +*******************************************************************************/ + +// Backwards compatibility for previous display component +.versus-upper { + line-height: 1.1; +} + +.new-match-style { + .match-section-header { + display: flex; + flex-direction: column; + padding: 1rem; + gap: 0.5em; + } + + .match { + border-bottom: 1px solid var( --table-border-color, var( --clr-border, #bbbbbb ) ); + padding: 0.75rem 1rem; + font-size: 0.875rem; + + .match-scoreboard { + display: flex; + justify-content: center; + align-items: center; + gap: 2rem; + margin-bottom: 0.5rem; + + > div { + flex: 1; + } + + .team-left { + display: flex; + justify-content: right; + } + + .versus { + flex: 0 1 auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .versus-upper { + display: flex; + gap: 0.25rem; + align-items: center; + justify-content: center; + } + + .versus-lower { + font-size: 0.75rem; + color: var( --clr-secondary ); + text-align: center; + } + } + + .match-winner { + font-weight: bold; + } + } + + .match-details { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + font-size: 0.75rem; + + .theme--light & { + background-color: #0000000a; + } + + .theme--dark & { + background-color: #ffffff0a; + } + + .match-streams { + display: flex; + align-items: center; + justify-content: center; + gap: 0.25rem; + } + + .match-tournament { + display: flex; + gap: 0.5rem; + justify-content: center; + align-items: center; + } + + .timer-object-countdown, + .timer-object-date { + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-weight: bold; + + &.timer-object-countdown-live { + .theme--light & { + color: #ffffff; + background-color: #b81414; + } + + .theme--dark & { + color: #2e0505; + background-color: #f5a3a3; + } + } + + &.timer-object-countdown-completed { + .theme--light & { + color: #ffffff; + background-color: #1d7c1d; + } + + .theme--dark & { + color: #0a290a; + background-color: #adebad; + } + } + + &:not( .timer-object-countdown-live, .timer-object-countdown-completed ) { + color: var( --clr-on-background ); + + .theme--light & { + background-color: #00000014; + } + + .theme--dark & { + background-color: #ffffff14; + } + } + } + } + + .timer-object-separator { + display: none; + } + } +}