adding menus and title handling
This commit is contained in:
+592
@@ -0,0 +1,592 @@
|
||||
local gui = require("gui")
|
||||
local color = require("gui.core.color")
|
||||
local loader = require("loader")
|
||||
local theme = require("gui.core.theme")
|
||||
local fmt = require("fmt")
|
||||
local timer = require("utils")
|
||||
local multi, thread = require("multi"):init()
|
||||
local menu = require("menus.menus")
|
||||
|
||||
local timesup = love.audio.newSource("assets/sounds/timesup.mp3", "static")
|
||||
local double = love.audio.newSource("assets/sounds/double.mp3", "static")
|
||||
|
||||
local boardUpdater = gui:getProcessor():newProcessor("board-updater")
|
||||
boardUpdater.Start()
|
||||
|
||||
local activePlayer
|
||||
local playerList = {}
|
||||
local playerStaticList = {}
|
||||
local scoreboard = {}
|
||||
|
||||
-- Create a table to manage GUI elements
|
||||
local manage = {}
|
||||
|
||||
-- Function to resize fonts for all managed elements
|
||||
local function resizeFonts()
|
||||
-- Use multi-threading to improve performance
|
||||
multi:newThread(function()
|
||||
-- Introduce a small delay to avoid too many iterations at once
|
||||
thread.skip(2)
|
||||
-- Iterate over each element in the 'manage' table
|
||||
for i = 1, #manage do
|
||||
local elem = manage[i]
|
||||
|
||||
-- Check if the current element has a centerFont property
|
||||
if elem.centerFont then
|
||||
-- Adjust font size and re-center the text
|
||||
elem:fitFont(nil, nil, {scale = 2/3})
|
||||
elem:centerFont()
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
gui.Events.OnResized(resizeFonts)
|
||||
|
||||
local completed_questions = 0
|
||||
local min_questions = 0
|
||||
local dd_count = 0
|
||||
local dd_enabled = false
|
||||
local applied_dd = false
|
||||
local board, question
|
||||
|
||||
function gui:cleanup()
|
||||
for i = #self.children, 1, -1 do
|
||||
self.children[i]:destroy()
|
||||
end
|
||||
self.children = {}
|
||||
completed_questions = completed_questions + 1
|
||||
end
|
||||
|
||||
local function pickUniqueIndices(t, count)
|
||||
assert(#t >= count, "Not enough elements to pick " .. count .. " unique items")
|
||||
|
||||
local indices = {}
|
||||
for i = 1, #t do indices[i] = i end
|
||||
|
||||
-- partial Fisher-Yates shuffle
|
||||
for i = 1, count do
|
||||
local j = math.random(i, #indices)
|
||||
indices[i], indices[j] = indices[j], indices[i]
|
||||
end
|
||||
|
||||
return unpack(indices, 1, count)
|
||||
end
|
||||
|
||||
local function pickFiltered(items, count)
|
||||
-- Filter to only elements whose text contains "$"
|
||||
local filtered = {}
|
||||
for _, item in ipairs(items) do
|
||||
if item.text:find("%$") then
|
||||
filtered[#filtered + 1] = item
|
||||
end
|
||||
end
|
||||
|
||||
-- Use pickUniqueIndices to select from filtered results
|
||||
local picks = { pickUniqueIndices(filtered, count) }
|
||||
local results = {}
|
||||
for _, idx in ipairs(picks) do
|
||||
results[#results + 1] = filtered[idx]
|
||||
end
|
||||
|
||||
return unpack(results)
|
||||
end
|
||||
|
||||
function applyDD()
|
||||
if not applied_dd and completed_questions > min_questions then
|
||||
local dd = 0
|
||||
local dd_list = {}
|
||||
local dds = {pickFiltered(board.children, dd_count)}
|
||||
for i,v in pairs(dds) do
|
||||
print(i,v)
|
||||
v.isDouble = true
|
||||
end
|
||||
applied_dd = true
|
||||
end
|
||||
end
|
||||
|
||||
local qUpdater = gui:getProcessor():newProcessor("question-updater")
|
||||
qUpdater.Start()
|
||||
|
||||
function LoadTemplate(name, path)
|
||||
|
||||
if love.filesystem.getInfo("templates/" .. name .. ".lua") then
|
||||
data = love.filesystem.read("templates/" .. name .. ".lua")
|
||||
elseif love.filesystem.getInfo(path .."/templates/" .. name .. ".lua") then
|
||||
data = love.filesystem.read(path .."/templates/" .. name .. ".lua")
|
||||
end
|
||||
|
||||
local template, err = loadstring(data)
|
||||
|
||||
if err ~= nil then
|
||||
error(err)
|
||||
end
|
||||
|
||||
local timer = function(wait, callback)
|
||||
multi:newAlarm(wait):OnRing(callback)
|
||||
end
|
||||
|
||||
local env = { -- lua built-ins except io/os code execution
|
||||
pairs=pairs,
|
||||
print=print,
|
||||
tostring=tostring,
|
||||
tonumber=tonumber,
|
||||
type=type,
|
||||
assert=assert,
|
||||
ipairs=ipairs,
|
||||
table=table,
|
||||
math=math,
|
||||
string=string,
|
||||
next=next,
|
||||
pcall=pcall,
|
||||
select=select,
|
||||
xpcall=xpcall,
|
||||
utf8=utf8,
|
||||
clock = require("socket").gettime,
|
||||
ALIGN_CENTER = gui.ALIGN_CENTER,
|
||||
ALIGN_LEFT = gui.ALIGN_LEFT,
|
||||
ALIGN_RIGHT = gui.ALIGN_RIGHT,
|
||||
ALIGN_JUSTIFY = gui.ALIGN_JUSTIFY,
|
||||
-- Global vars
|
||||
color = color,
|
||||
error = multi.error,
|
||||
theme = theme,
|
||||
gui = gui,
|
||||
timer = timer,
|
||||
-- love stuff
|
||||
newSource = love.audio.newSource
|
||||
}
|
||||
|
||||
env._G = env
|
||||
local isoTemplate = multi.isolateFunction(template, env)
|
||||
|
||||
return isoTemplate()
|
||||
end
|
||||
|
||||
function Menu(frame, path)
|
||||
local qframe = frame:newFrame(0, 0, 0, 0, .2, .05, .75, .9)
|
||||
qframe.color = color.new("#060ee9")
|
||||
local scoreboard = ScoreBoard(frame, 0, 0, 0, 0, .015, .05, .170, .9)
|
||||
|
||||
local data = loader:new(path)
|
||||
board, question, dailydouble = qframe:newFrame(), qframe:newFrame(), qframe:newImageLabel("assets/images/double.jpg")
|
||||
index = data.index
|
||||
|
||||
board:fullFrame()
|
||||
question:fullFrame()
|
||||
dailydouble:fullFrame()
|
||||
|
||||
dailydouble.visible = false
|
||||
question.visible = false
|
||||
question.color = color.new("#060ce9")
|
||||
|
||||
local tiers = index.settings.tiers or 5
|
||||
local start = index.settings.start or 100
|
||||
local inc = index.settings.increment or 100
|
||||
|
||||
if index.settings.dailyDouble and index.settings.dailyDouble.enabled then
|
||||
local dd = index.settings.dailyDouble
|
||||
dd_enabled = dd.enabled
|
||||
min_questions = dd.minQuestions
|
||||
dd_count = dd.count
|
||||
end
|
||||
for cat,v in pairs(index.categories) do
|
||||
local c
|
||||
if v.image then
|
||||
c = board:newImageLabel(v.image, 0, 0, 0, 0,(1/#index.categories)*(cat-1),0,1/#index.categories,1/(tiers+1))
|
||||
else
|
||||
c = board:newTextLabel(v.displayName or v.name,0,0,0,0,(1/#index.categories)*(cat-1),0,1/#index.categories,1/(tiers+1))
|
||||
c.align = gui.ALIGN_CENTER
|
||||
c.textColor = color.new("#ffffff")
|
||||
c.color = color.new("#060ce9")
|
||||
end
|
||||
|
||||
c.UUID = multi.generate_uuid7() -- Each otpion gets a unique UUID
|
||||
|
||||
img = c:newImageButton("assets/images/placeholder.jpg")
|
||||
img.visibility = 0
|
||||
|
||||
img:OnReleased(function(self)
|
||||
self.visible = false
|
||||
end)
|
||||
|
||||
img:fullFrame()
|
||||
table.insert(manage,c)
|
||||
for tier = 1,tiers do
|
||||
local t = board:newTextButton("$" .. start + inc*(tier-1),0,0,0,0,(1/#index.categories)*(cat-1),1/(tiers+1)*tier,(1/#index.categories),1/(tiers+1))
|
||||
t.textColor = color.new("#9b9024")
|
||||
t.align = gui.ALIGN_CENTER
|
||||
t.color = color.new("#060ce9")
|
||||
t.category = v.name
|
||||
t.index = tier
|
||||
t.price = start + inc*(tier-1)
|
||||
t:OnReleased(boardUpdater:newFunction(function(self)
|
||||
if self.text == "" or GetActivePlayer() == nil then return end
|
||||
if dd_enabled then
|
||||
applyDD() -- check and run DD if conditions meet
|
||||
end
|
||||
if index.categories[cat].questions == nil then fmt.Printf("Question not defined: File: %v Category: %v - %v\n",path,index.categories[cat].name,start + inc*(tier-1)) return end
|
||||
local q = index.categories[cat].questions[self.index]
|
||||
if q == nil then fmt.Printf("Question contains no data: File: %v Category: %v Tier: %v\n",path,index.categories[cat].name,start + inc*(tier-1)) return end
|
||||
self.textVisibility = 0
|
||||
local template = LoadTemplate(q.template, path)
|
||||
question.visible = true
|
||||
local player = GetActivePlayer()
|
||||
local tm
|
||||
local stop
|
||||
fmt.Printf("--------------------\nQuestion: %v \nAnswer: %v\n--------------------\n",q["title"],q["answer"])
|
||||
local mul = 1
|
||||
boardUpdater:newThread(function()
|
||||
if self.isDouble then
|
||||
mul = 2
|
||||
double:play()
|
||||
dailydouble.visible = true
|
||||
thread.hold(function()
|
||||
return not double:isPlaying()
|
||||
end)
|
||||
dailydouble.visible = false
|
||||
end
|
||||
if q["time-limit"] then
|
||||
tm = timer.startTimer({duration = q["time-limit"]})
|
||||
tm.OnStop(function()
|
||||
-- Make sound? Subtract if daily double
|
||||
if stop then return end
|
||||
timesup:play()
|
||||
end)
|
||||
end
|
||||
local finished = false
|
||||
template.index(question, q, function(ans)
|
||||
if finished then return end
|
||||
player = GetActivePlayer()
|
||||
if tm then
|
||||
tm:Cleanup()
|
||||
end
|
||||
stop = true
|
||||
if ans then
|
||||
player:Add(self.price*mul)
|
||||
finished = true
|
||||
question.visible = false
|
||||
question:cleanup()
|
||||
elseif ans == false then
|
||||
player:Add(-self.price*mul)
|
||||
player = GetNextPlayer()
|
||||
else
|
||||
finished = true
|
||||
question.visible = false
|
||||
question:cleanup()
|
||||
end -- nil is a valid option where you weren't right or wrong, you just skipped
|
||||
self.text = ""
|
||||
end)
|
||||
boardUpdater:newThread("QuestionUpdater",function()
|
||||
while true do
|
||||
template.update(dt)
|
||||
thread.yield()
|
||||
if self.text == "" then return end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end))
|
||||
table.insert(manage,t)
|
||||
end
|
||||
end
|
||||
|
||||
resizeFonts()
|
||||
resizeFonts()
|
||||
|
||||
return background
|
||||
end
|
||||
|
||||
scoreUpdater = gui:getProcessor():newProcessor("score-updater")
|
||||
scoreUpdater.Start()
|
||||
|
||||
function GetActivePlayer()
|
||||
if not activePlayer then return end
|
||||
return activePlayer.link
|
||||
end
|
||||
|
||||
local function GetPlayerPos()
|
||||
for i,v in pairs(playerStaticList) do
|
||||
if v == GetActivePlayer() then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function GetNextPlayer()
|
||||
local pos = GetPlayerPos()
|
||||
|
||||
if pos >= #playerStaticList then
|
||||
activePlayer = playerStaticList[1].Ref.Frame
|
||||
else
|
||||
activePlayer = playerStaticList[pos + 1].Ref.Frame
|
||||
end
|
||||
scoreboard:RenderPlayer(playerList)
|
||||
return GetActivePlayer()
|
||||
end
|
||||
|
||||
function love.filedropped(file)
|
||||
file:open("r")
|
||||
local data = file:read()
|
||||
print("Load file? " .. file:getFilename())
|
||||
end
|
||||
|
||||
function ScoreBoard(frame, x, y, w, h, sx, sy, sw, sh)
|
||||
-- Colors
|
||||
local C_BG_PANEL = color.new("#1a1a2e")
|
||||
local C_BG_HEADER = color.new("#16213e")
|
||||
local C_ACCENT = color.new("#e94560")
|
||||
local C_ROW_TOP = color.new("#1c2641")
|
||||
local C_ROW_NORM = color.new("#121226")
|
||||
local C_BORDER_TOP = color.new("#323c6e")
|
||||
local C_BORDER_NRM = color.new("#1c1c37")
|
||||
local C_BAR_EMPTY = color.new("#232341")
|
||||
local C_TEXT_MUTED = color.new("#786e96")
|
||||
local C_WHITE = color.new("#ffffff")
|
||||
local C_GOLD = color.new("#ffd700")
|
||||
local C_SILVER = color.new("#C0C0C0")
|
||||
local C_BRONZE = color.new("#cd7f32")
|
||||
|
||||
-- Config stuff
|
||||
local LEADER_HEIGHT_SCALE = .06
|
||||
local HEIGHT_SCALE = .03
|
||||
local PLAYER_HEIGHT = .05
|
||||
--
|
||||
|
||||
local rankColors = { C_GOLD, C_SILVER, C_BRONZE }
|
||||
|
||||
local leaderboard = frame:newFrame(x, y, w, h, sx, sy, sw, sh)
|
||||
leaderboard.color = C_BORDER_NRM
|
||||
|
||||
local headernum = leaderboard:newTextLabel("#",0,0,0,0,0,LEADER_HEIGHT_SCALE,1/6,HEIGHT_SCALE)
|
||||
headernum.align = gui.ALIGN_CENTER
|
||||
|
||||
local headerplayer = leaderboard:newTextLabel("PLAYER",0,0,0,0,1/6,LEADER_HEIGHT_SCALE,3/6,HEIGHT_SCALE)
|
||||
headerplayer.align = gui.ALIGN_LEFT
|
||||
|
||||
local headerscore = leaderboard:newTextLabel("SCORE",0,0,0,0,4/6,LEADER_HEIGHT_SCALE,2/6,HEIGHT_SCALE)
|
||||
headerscore.align = gui.ALIGN_RIGHT
|
||||
|
||||
local header = leaderboard:newTextLabel("LEADERBOARD",0,0,0,0,0,0,1,LEADER_HEIGHT_SCALE)
|
||||
header.borderColor = C_ACCENT
|
||||
header.textColor = C_ACCENT
|
||||
header.color = C_BG_HEADER
|
||||
header.align = gui.ALIGN_CENTER
|
||||
|
||||
local BASE_HEIGHT = LEADER_HEIGHT_SCALE + HEIGHT_SCALE
|
||||
|
||||
gui.apply({
|
||||
drawBorder = false,
|
||||
color = color.new("#0f2a60"),
|
||||
textColor = C_TEXT_MUTED,
|
||||
}, headernum, headerplayer, headerscore)
|
||||
|
||||
local updateList = {header, headernum, headerplayer, headerscore}
|
||||
|
||||
local function ScoreResize()
|
||||
scoreUpdater:newThread(function()
|
||||
thread.skip(2)
|
||||
|
||||
for _,object in pairs(updateList) do
|
||||
object:fitFont(nil, nil, {scale = 5/6})
|
||||
object:centerFont()
|
||||
end
|
||||
|
||||
for _,object in pairs(playerList) do
|
||||
if type(object) == "table" and object.Ref then
|
||||
for i, player in pairs(object.Ref) do
|
||||
if player:hasType(gui.TYPE_TEXT) then
|
||||
player:fitFont(nil, nil, {scale = 5/6})
|
||||
player:centerFont()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function scoreboard:AddPlayer(name, score, icon)
|
||||
local player = {
|
||||
Name = name,
|
||||
Score = score,
|
||||
Icon = icon,
|
||||
UUID = multi.generate_uuid7(),
|
||||
Add = function(self, amt)
|
||||
self.Score = tostring(tonumber(self.Score) + amt)
|
||||
table.sort(playerList, function(a, b)
|
||||
if a.Score == b.Score then
|
||||
return a.Name < b.Name
|
||||
end
|
||||
return tonumber(a.Score) > tonumber(b.Score)
|
||||
end)
|
||||
scoreboard:RenderPlayer(playerList)
|
||||
ScoreResize()
|
||||
end,
|
||||
}
|
||||
table.insert(playerList, player)
|
||||
table.insert(playerStaticList, player)
|
||||
scoreboard:RenderPlayer(playerList)
|
||||
ScoreResize()
|
||||
return player
|
||||
end
|
||||
|
||||
local colors = {C_GOLD, C_SILVER, C_BRONZE}
|
||||
local function MapColor(index)
|
||||
if index <=3 and index > 0 then
|
||||
return colors[index]
|
||||
else
|
||||
return C_WHITE
|
||||
end
|
||||
end
|
||||
|
||||
local add_player = leaderboard:newFrame(5,-5,-10,0,0,1-PLAYER_HEIGHT,1,PLAYER_HEIGHT)
|
||||
local edit_player = leaderboard:newFrame(5,-10,-10,0,0,1-2*PLAYER_HEIGHT,1,PLAYER_HEIGHT)
|
||||
local remove_player = leaderboard:newTextButton("Remove Selected",5,-15,-10,0,0,1-3*PLAYER_HEIGHT,1,PLAYER_HEIGHT)
|
||||
remove_player:setFont(20)
|
||||
remove_player.align = gui.ALIGN_CENTER
|
||||
local embededWatch = {remove_player}
|
||||
scoreUpdater:newThread(function()
|
||||
while true do
|
||||
thread.sleep(.01)
|
||||
for i,v in pairs(embededWatch) do
|
||||
v:centerFont()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local function embedTextEdit(reference, default, but_text, callback)
|
||||
reference.color = C_BORDER_NRM
|
||||
local textbox = reference:newTextBox(default,0,0,0,0,.015,.1,.8,.8)
|
||||
textbox.textColor = C_GOLD
|
||||
textbox.blink = false
|
||||
textbox.color = C_BORDER_TOP
|
||||
textbox.textColor = C_WHITE
|
||||
textbox:OnPressed(function()
|
||||
textbox.text = ""
|
||||
end)
|
||||
|
||||
local button = reference:newTextButton(but_text,5,0,-10,0,.815,.1,.185,.8)
|
||||
button.color = color.new("#7eae5b")
|
||||
button:OnReleased(function()
|
||||
callback(textbox)
|
||||
end)
|
||||
gui.apply({
|
||||
setFont = {20},
|
||||
align = gui.ALIGN_CENTER
|
||||
},textbox,button,reference)
|
||||
table.insert(embededWatch,textbox)
|
||||
table.insert(embededWatch,button)
|
||||
end
|
||||
|
||||
remove_player.color = color.new("#a13a3a")
|
||||
remove_player:OnReleased(function()
|
||||
local player = GetActivePlayer()
|
||||
uuid = player.UUID
|
||||
for i = 1, #playerList do
|
||||
if playerList[i].UUID == uuid then
|
||||
table.remove(playerList,i)
|
||||
break
|
||||
end
|
||||
end
|
||||
for i = 1, #playerStaticList do
|
||||
if playerStaticList[i].UUID == uuid then
|
||||
table.remove(playerStaticList,i)
|
||||
break
|
||||
end
|
||||
end
|
||||
scoreboard:RenderPlayer(playerList)
|
||||
player.Ref.Frame:destroy()
|
||||
end)
|
||||
|
||||
embedTextEdit(add_player, "Player Name", "Add", function(self)
|
||||
scoreboard:AddPlayer(self.text, "0")
|
||||
end)
|
||||
|
||||
embedTextEdit(edit_player, "Modify Score", "Edit", function(self)
|
||||
local player = GetActivePlayer()
|
||||
if player then
|
||||
player.Score = self.text
|
||||
scoreboard:RenderPlayer(playerList)
|
||||
end
|
||||
end)
|
||||
|
||||
function scoreboard:RenderPlayer(list)
|
||||
for index, player in ipairs(list) do
|
||||
if player.Ref then
|
||||
player.Ref[1].text = player.Name or ""
|
||||
player.Ref[2].text = player.Score or ""
|
||||
player.Ref[3].text = tostring(index)
|
||||
player.Ref.Frame:setDualDim(nil,5*index,nil,nil,nil,BASE_HEIGHT + (index-1) * PLAYER_HEIGHT)
|
||||
player.Ref.Frame.link = player
|
||||
|
||||
gui.apply({
|
||||
visibility = 0,
|
||||
drawBorder = false,
|
||||
textColor = MapColor(index)
|
||||
}, unpack(player.Ref))
|
||||
|
||||
if activePlayer == nil then
|
||||
activePlayer = player.Ref.Frame
|
||||
end
|
||||
|
||||
if player.Ref.Frame == activePlayer then
|
||||
player.Ref.Frame.borderColor = C_BORDER_NRM
|
||||
player.Ref.Frame.color = C_ROW_NORM
|
||||
else
|
||||
player.Ref.Frame.borderColor = C_BORDER_TOP
|
||||
player.Ref.Frame.color = C_ROW_TOP
|
||||
end
|
||||
else
|
||||
local playernum, playerName, playerIcon, playerScore, playerLine
|
||||
local playerFrame = leaderboard:newFrame(5,5*index,-10,0,0,BASE_HEIGHT + (index-1) * PLAYER_HEIGHT,1,PLAYER_HEIGHT)
|
||||
|
||||
playerFrame:OnReleased(function(self)
|
||||
activePlayer = self
|
||||
scoreboard:RenderPlayer(playerList)
|
||||
end)
|
||||
|
||||
playerFrame:respectHierarchy(false)
|
||||
|
||||
playerFrame.borderColor = C_BORDER_TOP
|
||||
playerFrame.color = C_ROW_TOP
|
||||
|
||||
playernum = playerFrame:newTextLabel(index,0,0,0,0,.015,.1,1/8,.8)
|
||||
playernum.align = gui.ALIGN_CENTER
|
||||
|
||||
if player.Icon ~= nil then
|
||||
playerIcon = playerFrame:newImageLabel(player.Icon,0,0,0,0,.16,.1,.1,.8)
|
||||
playerIcon.square = "h" -- When working with scales squaring is trickier. (h/w) to switch on width or height
|
||||
|
||||
playerName = playerFrame:newTextLabel(player.Name,0,0,0,0,.3,0,2/5,.8)
|
||||
playerName.align = gui.ALIGN_LEFT
|
||||
|
||||
playerLine = playerFrame:newFrame(0,0,0,0,.3,.8,.69,.07)
|
||||
playerLine.color = C_GOLD
|
||||
else
|
||||
playerName = playerFrame:newTextLabel(player.Name,0,0,0,0,.16,0,7/13,.8)
|
||||
playerName.align = gui.ALIGN_LEFT
|
||||
|
||||
playerLine = playerFrame:newFrame(0,0,0,0,.16,.8,7/13 + 2/7,.07)
|
||||
playerLine.color = C_GOLD
|
||||
end
|
||||
|
||||
playerScore = playerFrame:newTextLabel(player.Score,0,0,0,0,.71,0,2/7,.8)
|
||||
playerScore.align = gui.ALIGN_CENTER
|
||||
playerLine.drawBorder = false
|
||||
|
||||
gui.apply({
|
||||
visibility = 0,
|
||||
drawBorder = false,
|
||||
textColor = MapColor(index)
|
||||
},playernum, playerName, playerScore, playerIcon, playerLine)
|
||||
|
||||
player.Ref = {playerName, playerScore, playernum, playerIcon, playerLine, Frame = playerFrame}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
gui.Events.OnResized(ScoreResize)
|
||||
ScoreResize()
|
||||
return scoreboard
|
||||
end
|
||||
|
||||
return menu.registerMenu("board", Menu)
|
||||
Reference in New Issue
Block a user