Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f9af617
add skeleton impl
ElectricalBoy Feb 19, 2026
9a7e5e5
add recent matches
ElectricalBoy Feb 19, 2026
e1d8eb3
cleanup
ElectricalBoy Feb 19, 2026
8b714f1
move
ElectricalBoy Feb 19, 2026
9a75277
css
ElectricalBoy Feb 19, 2026
d96273b
type annotation
ElectricalBoy Feb 19, 2026
8b75999
add team version
ElectricalBoy Feb 19, 2026
55cd42c
remove unused imports
ElectricalBoy Feb 19, 2026
aa7f61e
annotations
ElectricalBoy Feb 19, 2026
7cd1c26
basic starcraft impl
ElectricalBoy Feb 19, 2026
c657f40
copy from live
ElectricalBoy Feb 19, 2026
5353742
type annotations
ElectricalBoy Feb 19, 2026
14011b0
use widget
ElectricalBoy Feb 19, 2026
f8ce06f
backport css change to team version
ElectricalBoy Feb 19, 2026
52f6256
add custom to sc(2)
ElectricalBoy Feb 19, 2026
6544e96
add custom to LoL
ElectricalBoy Feb 19, 2026
924c4fe
lint
ElectricalBoy Feb 19, 2026
a31fab0
feedback from review
ElectricalBoy Feb 19, 2026
e33e985
remove unused import
ElectricalBoy Feb 19, 2026
df87f4c
add gap
ElectricalBoy Feb 19, 2026
5ae5057
use table2
ElectricalBoy Feb 19, 2026
334dd77
remove unused import
ElectricalBoy Feb 19, 2026
33c05cd
feedback from review
ElectricalBoy Feb 20, 2026
5946589
simplify faction check
ElectricalBoy Feb 20, 2026
0da922b
use formatPercentage
ElectricalBoy Feb 20, 2026
93c83da
use title param
ElectricalBoy Feb 20, 2026
3a57090
remove font-size css
ElectricalBoy Feb 20, 2026
701573a
suppress name display for solo opponent
ElectricalBoy Feb 20, 2026
72d1809
add option for suppressing map table
ElectricalBoy Feb 20, 2026
b757cf9
rename as suggested
ElectricalBoy Feb 20, 2026
fba0b30
tabs
ElectricalBoy Feb 20, 2026
5f95ab8
adjust display
ElectricalBoy Feb 24, 2026
8de7534
use compact date
ElectricalBoy Feb 24, 2026
58c2176
code conciseness
ElectricalBoy Feb 24, 2026
32e6313
remove unused import
ElectricalBoy Feb 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lua/wikis/commons/MatchGroup/Util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ MatchGroupUtil.types.Game = TypeUtil.struct({

---@class MatchGroupUtilMatch
---@field bracketData MatchGroupUtilBracketData
---@field bracketId string
---@field comment string?
---@field date string
---@field dateIsExact boolean
Expand Down Expand Up @@ -565,6 +566,7 @@ function MatchGroupUtil.matchFromRecord(record)
local match = {
bestof = tonumber(record.bestof) or 0,
bracketData = bracketData,
bracketId = record.match2bracketid,
comment = nilIfEmpty(Table.extract(extradata, 'comment')),
extradata = extradata,
date = record.date,
Expand Down
306 changes: 306 additions & 0 deletions lua/wikis/commons/StreamPage/Base.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
---
-- @Liquipedia
-- page=Module:StreamPage/Base
--
-- Please see https://github.com/Liquipedia/Lua-Modules to contribute
--

local Lua = require('Module:Lua')

local Arguments = Lua.import('Module:Arguments')
local Array = Lua.import('Module:Array')
local Class = Lua.import('Module:Class')
local Countdown = Lua.import('Module:Countdown')
local DateExt = Lua.import('Module:Date/Ext')
local HighlightConditions = Lua.import('Module:HighlightConditions')
local Logic = Lua.import('Module:Logic')
local Lpdb = Lua.import('Module:Lpdb')
local MatchGroupUtil = Lua.import('Module:MatchGroup/Util/Custom')
local MatchPage = Lua.requireIfExists('Module:MatchPage')
local MatchTable = Lua.import('Module:MatchTable')
local MatchTicker = Lua.import('Module:MatchTicker')
local Opponent = Lua.import('Module:Opponent/Custom')
local OpponentDisplay = Lua.import('Module:OpponentDisplay/Custom')
local Table = Lua.import('Module:Table')
local TeamTemplate = Lua.import('Module:TeamTemplate')
local Tournament = Lua.import('Module:Tournament')

local Condition = Lua.import('Module:Condition')
local ConditionTree = Condition.Tree
local ConditionNode = Condition.Node
local Comparator = Condition.Comparator
local BooleanOperator = Condition.BooleanOperator
local ColumnName = Condition.ColumnName

local GridWidgets = Lua.import('Module:Widget/Grid')
local HtmlWidgets = Lua.import('Module:Widget/Html/All')
local IconImage = Lua.import('Module:Widget/Image/Icon/Image')
local MatchPageAdditionalSection = Lua.import('Module:Widget/Match/Page/AdditionalSection')
local MatchPageHeader = Lua.import('Module:Widget/Match/Page/Header')
local WidgetUtil = Lua.import('Module:Widget/Util')

---@class BaseStreamPage: BaseClass
---@operator call(table): BaseStreamPage
---@field channel string
---@field provider string
---@field suppressBottomContent boolean
---@field matches MatchGroupUtilMatch[]
local StreamPage = Class.new(function (self, args)
self.channel = assert(Logic.nilIfEmpty(args.channel))
self.provider = assert(Logic.nilIfEmpty(args.provider))
self.suppressBottomContent = Logic.readBool(args.suppressBottomContent)
self.matches = {}

self:_fetchMatches()
end)

---@param frame Frame
---@return Widget?
function StreamPage.run(frame)
local args = Arguments.getArgs(frame)
return StreamPage(args):create()
end

---@private
---@return ConditionTree
function StreamPage:_createMatchQueryCondition()
local conditions = ConditionTree(BooleanOperator.all):add{
ConditionTree(BooleanOperator.any):add{
ConditionNode(ColumnName(self.provider, 'stream'), Comparator.eq, self.channel),
ConditionNode(ColumnName(self.provider .. '_en_1', 'stream'), Comparator.eq, self.channel)
},
ConditionNode(ColumnName('finished'), Comparator.eq, 0),
ConditionNode(ColumnName('date'), Comparator.neq, DateExt.defaultDateTime),
ConditionNode(ColumnName('dateexact'), Comparator.eq, 1)
}

conditions:add(StreamPage:addMatchConditions())
return conditions
end

---@private
function StreamPage:_fetchMatches()
local conditions = self:_createMatchQueryCondition()

Lpdb.executeMassQuery('match2', {
conditions = tostring(conditions),
order = 'date asc',
limit = 25,
}, function (record)
local match = MatchGroupUtil.matchFromRecord(record)
if Array.any(match.opponents, Opponent.isBye) then
return
elseif #self.matches == 5 then
return false
end
Array.appendWith(self.matches, match)
end)
end

---@protected
---@return AbstractConditionNode|AbstractConditionNode[]?
function StreamPage:addMatchConditions()
end

---@private
---@return Widget
function StreamPage:_header()
local match = self.matches[1]
local countdownBlock = HtmlWidgets.Div{
css = {
display = 'block',
['text-align'] = 'center'
},
children = Countdown.create{
date = DateExt.toCountdownArg(match.timestamp, match.timezoneId, match.dateIsExact),
finished = match.finished,
rawdatetime = Logic.readBool(match.finished),
}
} or nil
local tournament = Tournament.partialTournamentFromMatch(match)
Array.forEach(match.opponents, function (opponent, opponentIndex)
if opponent.type ~= Opponent.team then
return
end
---@cast opponent MatchPageOpponent
opponent.iconDisplay = OpponentDisplay.InlineTeamContainer{
style = 'icon',
template = opponent.template,
}
opponent.teamTemplateData = TeamTemplate.getRaw(opponent.template)
opponent.seriesDots = {}
end)

return MatchPageHeader{
countdownBlock = countdownBlock,
isBestOfOne = match.bestof == 1,
mvp = match.extradata.mvp,
opponent1 = match.opponents[1],
opponent2 = match.opponents[2],
parent = tournament.pageName,
phase = MatchGroupUtil.computeMatchPhase(match),
stream = {},
tournamentName = tournament.fullName,
highlighted = HighlightConditions.tournament(tournament)
}
end

---@return Widget?
function StreamPage:create()
if Logic.isEmpty(self.matches) then
return
elseif self.matches[1].bracketData.matchPage then
return HtmlWidgets.Fragment{children = {
'__NOTOC__',
MatchPage(self.matches[1]):render(),
HtmlWidgets.H3{children = 'Channel Schedule'},
self:_createMatchTicker():query():create()
}}
end

return HtmlWidgets.Fragment{children = WidgetUtil.collect(
'__NOTOC__',
self:_header(),
GridWidgets.Container{gridCells = {
GridWidgets.Cell{
cellContent = self:render(),
xs = 'ignore',
sm = 'ignore',
lg = 8,
xl = 8,
xxl = 9,
xxxl = 9,
},
GridWidgets.Cell{
cellContent = MatchPageAdditionalSection{
header = 'Channel Schedule',
children = self:_createMatchTicker():query():create():css('width', '100%')
},
xs = 'ignore',
sm = 'ignore',
lg = 4,
xl = 4,
xxl = 3,
xxxl = 3,
}
}},
not self.suppressBottomContent and self:createBottomContent() or nil
)}
end

---@private
---@return MatchTicker
function StreamPage:_createMatchTicker()
return MatchTicker{
additionalConditions = 'AND (' .. tostring(self:_createMatchQueryCondition()) .. ')',
limit = 5,
newStyle = true,
ongoing = true,
upcoming = true,
}
end

---@protected
---@return string|Widget|Html|(string|Widget|Html)[]?
function StreamPage:render()
end

---@protected
---@return Widget[]?
function StreamPage:createBottomContent()
local match = self.matches[1]

if Array.all(match.opponents, Opponent.isTbd) then
return
end

local headToHead = self:_buildHeadToHeadMatchTable()

return WidgetUtil.collect(
not self.suppressBottomContent and HtmlWidgets.H3{children = 'Match History'} or nil,
HtmlWidgets.Div{
classes = {'match-bm-match-additional'},
children = WidgetUtil.collect(
headToHead and MatchPageAdditionalSection{
css = {flex = '2 0 100%'},
header = 'Head to Head',
bodyClasses = {'match-table-wrapper'},
children = headToHead,
} or nil,
Array.map(match.opponents, function (opponent)
local matchTable = self:_buildMatchTable(opponent)
return MatchPageAdditionalSection{
header = OpponentDisplay.InlineOpponent{opponent = opponent, teamStyle = 'hybrid'},
bodyClasses = matchTable and {'match-table-wrapper'} or nil,
children = matchTable or IconImage{
imageLight = match.icon,
imageDark = match.iconDark,
size = '50x32px',
}
}
end)
)
}
)
end

---@private
---@param props table
---@return Html
function StreamPage:_createMatchTable(props)
local match = self.matches[1]
return MatchTable(Table.mergeInto({
addCategory = false,
dateFormat = 'compact',
edate = match.timestamp - DateExt.daysToSeconds(1) --[[ MatchTable adds 1-day offset to make edate
inclusive, and we don't want that here ]],
limit = 5,
stats = false,
vod = false,
matchPageButtonText = 'short',
}, props)):readConfig():query():buildDisplay()
end

---@private
---@param opponent standardOpponent
---@return Html?
function StreamPage:_buildMatchTable(opponent)
if opponent.type ~= Opponent.solo and opponent.type ~= Opponent.team then
return
end
local base = opponent.type == Opponent.team and Opponent.team or 'player'
return self:_createMatchTable{
['hide_tier'] = true,
limit = 5,
stats = false,
tableMode = opponent.type,
[base] = Opponent.toName(opponent),
useTickerName = true,
}
end

---@private
---@return Html?
function StreamPage:_buildHeadToHeadMatchTable()
local match = self.matches[1]
if Array.any(match.opponents, Opponent.isTbd) then
return
elseif match.opponents[1].type ~= match.opponents[2].type then
return
elseif match.opponents[1].type ~= Opponent.solo and match.opponents[1].type ~= Opponent.team then
return
end

local base = match.opponents[1].type == Opponent.team and Opponent.team or 'player'

return self:_createMatchTable{
[base] = Opponent.toName(match.opponents[1]),
['vs' .. base] = Opponent.toName(match.opponents[2]),
tableMode = match.opponents[1].type,
showOpponent = true,
teamStyle = 'hybrid',
useTickerName = true,
}
end

return StreamPage
10 changes: 10 additions & 0 deletions lua/wikis/commons/StreamPage/Custom.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
-- @Liquipedia
-- page=Module:StreamPage/Custom
--
-- Please see https://github.com/Liquipedia/Lua-Modules to contribute
--

local Lua = require('Module:Lua')

return Lua.import('Module:StreamPage')
Loading
Loading