Compare commits
2 commits
fad1e8eba6
...
fe2785825b
Author | SHA1 | Date | |
---|---|---|---|
fe2785825b | |||
631874aebc |
3 changed files with 266 additions and 40 deletions
|
@ -1,12 +1,32 @@
|
||||||
local DisableClipping = DisableClipping
|
|
||||||
local ScrW = ScrW
|
local ScrW = ScrW
|
||||||
local ScrH = ScrH
|
local ScrH = ScrH
|
||||||
|
local Color = Color
|
||||||
|
|
||||||
|
local STENCIL_EQUAL = STENCIL_EQUAL
|
||||||
|
local STENCIL_KEEP = STENCIL_KEEP
|
||||||
|
local STENCIL_NEVER = STENCIL_NEVER
|
||||||
|
local STENCIL_REPLACE = STENCIL_REPLACE
|
||||||
|
|
||||||
|
local draw = draw
|
||||||
|
local math = math
|
||||||
local render = render
|
local render = render
|
||||||
local string = string
|
|
||||||
local surface = surface
|
local surface = surface
|
||||||
|
|
||||||
|
local draw_NoTexture = draw.NoTexture
|
||||||
|
local math_rad = math.rad
|
||||||
|
local math_sin = math.sin
|
||||||
|
local math_cos = math.cos
|
||||||
|
local render_ClearStencil = render.ClearStencil
|
||||||
|
local render_SetStencilCompareFunction = render.SetStencilCompareFunction
|
||||||
|
local render_SetStencilEnable = render.SetStencilEnable
|
||||||
|
local render_SetStencilFailOperation = render.SetStencilFailOperation
|
||||||
|
local render_SetStencilPassOperation = render.SetStencilPassOperation
|
||||||
|
local render_SetStencilReferenceValue = render.SetStencilReferenceValue
|
||||||
|
local render_SetStencilTestMask = render.SetStencilTestMask
|
||||||
|
local render_SetStencilWriteMask = render.SetStencilWriteMask
|
||||||
|
local render_SetStencilZFailOperation = render.SetStencilZFailOperation
|
||||||
local render_UpdateScreenEffectTexture = render.UpdateScreenEffectTexture
|
local render_UpdateScreenEffectTexture = render.UpdateScreenEffectTexture
|
||||||
|
local surface_DrawPoly = surface.DrawPoly
|
||||||
local surface_DrawRect = surface.DrawRect
|
local surface_DrawRect = surface.DrawRect
|
||||||
local surface_DrawTexturedRect = surface.DrawTexturedRect
|
local surface_DrawTexturedRect = surface.DrawTexturedRect
|
||||||
local surface_SetDrawColor = surface.SetDrawColor
|
local surface_SetDrawColor = surface.SetDrawColor
|
||||||
|
@ -16,10 +36,10 @@ cbox.chatbox = cbox.chatbox or {}
|
||||||
cbox.chatbox.tabs = cbox.chatbox.tabs or {}
|
cbox.chatbox.tabs = cbox.chatbox.tabs or {}
|
||||||
cbox.chatbox.panels = cbox.chatbox.panels or {}
|
cbox.chatbox.panels = cbox.chatbox.panels or {}
|
||||||
|
|
||||||
|
|
||||||
local CHATBOX_COLOR = CreateClientConVar("cbox_chatbox_color", "160 160 160", true, false, "Chatbox background color")
|
local CHATBOX_COLOR = CreateClientConVar("cbox_chatbox_color", "160 160 160", true, false, "Chatbox background color")
|
||||||
local CHATBOX_ALPHA = CreateClientConVar("cbox_chatbox_alpha", "128", true, false, "Chatbox background alpha")
|
local CHATBOX_ALPHA = CreateClientConVar("cbox_chatbox_alpha", "128", true, false, "Chatbox background alpha")
|
||||||
local CHATBOX_BLUR = CreateClientConVar("cbox_chatbox_blur", "0", true, false, "Chatbox background is blurred")
|
local CHATBOX_BLUR = CreateClientConVar("cbox_chatbox_blur", "0", true, false, "Chatbox background is blurred")
|
||||||
|
local CHATBOX_FADE = CreateClientConVar("cbox_chatbox_fade", "1", true, false, "Chatbox fades in and out")
|
||||||
|
|
||||||
---@param height number
|
---@param height number
|
||||||
---@return number
|
---@return number
|
||||||
|
@ -51,6 +71,55 @@ end
|
||||||
|
|
||||||
local MATERIAL_BLUR = Material("pp/blurscreen")
|
local MATERIAL_BLUR = Material("pp/blurscreen")
|
||||||
|
|
||||||
|
local function add_rounded_poly(poly, x, y, rad, seg, offset)
|
||||||
|
offset = offset or 0
|
||||||
|
|
||||||
|
for i = 0, seg do
|
||||||
|
local r = math_rad(((i + offset) / seg) * -90)
|
||||||
|
poly[#poly + 1] = {
|
||||||
|
x = x + math_sin(r) * rad,
|
||||||
|
y = y + math_cos(r) * rad,
|
||||||
|
u = math_sin(r) / 2 + 0.5,
|
||||||
|
v = math_cos(r) / 2 + 0.5,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function RoundedBoxPoly(x, y, w, h, rad, seg)
|
||||||
|
local poly = {}
|
||||||
|
|
||||||
|
add_rounded_poly(poly, x + rad, y + rad, rad, seg, seg)
|
||||||
|
|
||||||
|
poly[#poly + 1] = {
|
||||||
|
x = x + (w - rad),
|
||||||
|
y = y,
|
||||||
|
u = 0.5,
|
||||||
|
v = 0.5,
|
||||||
|
}
|
||||||
|
|
||||||
|
add_rounded_poly(poly, x + (w - rad), y + rad, rad, seg, seg * 2)
|
||||||
|
|
||||||
|
poly[#poly + 1] = {
|
||||||
|
x = x + w,
|
||||||
|
y = y + (h - rad),
|
||||||
|
u = 0.5,
|
||||||
|
v = 0.5,
|
||||||
|
}
|
||||||
|
|
||||||
|
add_rounded_poly(poly, x + (w - rad), y + (h - rad), rad, seg, seg * 3)
|
||||||
|
|
||||||
|
poly[#poly + 1] = {
|
||||||
|
x = x + rad,
|
||||||
|
y = y + h,
|
||||||
|
u = 0.5,
|
||||||
|
v = 0.5,
|
||||||
|
}
|
||||||
|
|
||||||
|
add_rounded_poly(poly, x + rad, y + (h - rad), rad, seg)
|
||||||
|
|
||||||
|
surface_DrawPoly(poly)
|
||||||
|
end
|
||||||
|
|
||||||
local function CreateChatbox()
|
local function CreateChatbox()
|
||||||
if IsValid(cbox.chatbox.panels.frame) then
|
if IsValid(cbox.chatbox.panels.frame) then
|
||||||
cbox.chatbox.panels.frame:Remove()
|
cbox.chatbox.panels.frame:Remove()
|
||||||
|
@ -61,15 +130,85 @@ local function CreateChatbox()
|
||||||
frame:SetDeleteOnClose(false)
|
frame:SetDeleteOnClose(false)
|
||||||
frame:SetSizable(true)
|
frame:SetSizable(true)
|
||||||
frame:SetScreenLock(true)
|
frame:SetScreenLock(true)
|
||||||
frame:DockPadding(4, 4, 4, 4)
|
frame:DockPadding(8, 8, 8, 8)
|
||||||
|
|
||||||
frame.btnMinim:SetVisible(false)
|
local dx, dy, dw, dh = GetDefaultBounds()
|
||||||
frame.btnMaxim:SetVisible(false)
|
|
||||||
frame.lblTitle:SetVisible(false)
|
-- TODO: make this configurable
|
||||||
|
frame:SetMinWidth(dw)
|
||||||
|
frame:SetMinWidth(dh)
|
||||||
|
|
||||||
|
-- TODO: save resizing/moving
|
||||||
|
frame:SetPos(dx, dy)
|
||||||
|
frame:SetSize(dw, dh)
|
||||||
|
|
||||||
|
frame:SetVisible(false)
|
||||||
|
|
||||||
|
function frame:PerformLayout() end
|
||||||
|
|
||||||
|
frame.btnClose:Remove()
|
||||||
|
frame.btnMinim:Remove()
|
||||||
|
frame.btnMaxim:Remove()
|
||||||
|
frame.lblTitle:Remove()
|
||||||
|
|
||||||
|
function frame:CrossFade(anim, delta, out)
|
||||||
|
if anim.Finished then
|
||||||
|
if out then
|
||||||
|
self:Close()
|
||||||
|
else
|
||||||
|
self:MakePopup()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if anim.Started then
|
||||||
|
if out then
|
||||||
|
self:SetAlpha(255)
|
||||||
|
else
|
||||||
|
self:SetAlpha(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self:SetAlpha(out and 255 * (1 - delta) or 255 * delta)
|
||||||
|
end
|
||||||
|
|
||||||
|
frame.animFade = Derma_Anim("Fade", frame, frame.CrossFade)
|
||||||
|
|
||||||
|
frame.oldThink = frame.Think
|
||||||
|
|
||||||
|
function frame:Think()
|
||||||
|
self:oldThink()
|
||||||
|
|
||||||
|
self.animFade:Run()
|
||||||
|
|
||||||
|
-- TODO: save position and size
|
||||||
|
end
|
||||||
|
|
||||||
function frame:Paint(w, h)
|
function frame:Paint(w, h)
|
||||||
local alpha = CHATBOX_ALPHA:GetInt()
|
local alpha = CHATBOX_ALPHA:GetInt()
|
||||||
|
|
||||||
|
-- Reset everything to known good
|
||||||
|
render_SetStencilWriteMask( 0xFF )
|
||||||
|
render_SetStencilTestMask( 0xFF )
|
||||||
|
render_SetStencilReferenceValue( 0 )
|
||||||
|
render_SetStencilPassOperation( STENCIL_KEEP )
|
||||||
|
render_SetStencilZFailOperation( STENCIL_KEEP )
|
||||||
|
render_ClearStencil()
|
||||||
|
|
||||||
|
-- Enable stencils
|
||||||
|
render_SetStencilEnable( true )
|
||||||
|
-- Set everything up everything draws to the stencil buffer instead of the screen
|
||||||
|
render_SetStencilReferenceValue( 1 )
|
||||||
|
render_SetStencilCompareFunction( STENCIL_NEVER )
|
||||||
|
render_SetStencilFailOperation( STENCIL_REPLACE )
|
||||||
|
|
||||||
|
draw_NoTexture()
|
||||||
|
surface_SetDrawColor(255, 255, 255)
|
||||||
|
RoundedBoxPoly(0, 0, w, h, 8, 24)
|
||||||
|
|
||||||
|
-- Only draw things that are in the stencil buffer
|
||||||
|
render_SetStencilCompareFunction( STENCIL_EQUAL )
|
||||||
|
render_SetStencilFailOperation( STENCIL_KEEP )
|
||||||
|
|
||||||
if CHATBOX_BLUR:GetBool() and alpha ~= 255 then
|
if CHATBOX_BLUR:GetBool() and alpha ~= 255 then
|
||||||
local x, y = self:LocalToScreen(0, 0)
|
local x, y = self:LocalToScreen(0, 0)
|
||||||
|
|
||||||
|
@ -84,32 +223,26 @@ local function CreateChatbox()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local color = string.Explode(" ", CHATBOX_COLOR:GetString())
|
local r, g, b = CHATBOX_COLOR:GetString():match("(%d+) (%d+) (%d+)")
|
||||||
surface_SetDrawColor(tonumber(color[1]), tonumber(color[2]), tonumber(color[3]), alpha)
|
surface_SetDrawColor(r, g, b, alpha)
|
||||||
surface_DrawRect(0, 0, w, h)
|
surface_DrawRect(0, 0, w, h)
|
||||||
|
|
||||||
|
-- Let everything render normally again
|
||||||
|
render_SetStencilEnable(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
local dx, dy, dw, dh = GetDefaultBounds()
|
|
||||||
|
|
||||||
-- TODO: make this configurable
|
|
||||||
frame:SetMinWidth(dw)
|
|
||||||
frame:SetMinWidth(dh)
|
|
||||||
|
|
||||||
-- TODO: save resizing/moving
|
|
||||||
frame:SetPos(dx, dy)
|
|
||||||
frame:SetSize(dw, dh)
|
|
||||||
|
|
||||||
frame:SetVisible(false)
|
|
||||||
|
|
||||||
cbox.chatbox.panels.frame = frame
|
cbox.chatbox.panels.frame = frame
|
||||||
|
|
||||||
local tabs = vgui.Create("DPropertySheet", frame, "cbox.chatbox.tabs")
|
local tabs = vgui.Create("DPropertySheet", frame, "cbox.chatbox.tabs")
|
||||||
tabs:Dock(FILL)
|
tabs:Dock(FILL)
|
||||||
tabs:SetPadding(0)
|
tabs:SetPadding(0)
|
||||||
|
|
||||||
function tabs:Paint(w, h)
|
function tabs:Paint(w, h) end
|
||||||
surface_SetDrawColor(0, 0, 0, 72)
|
|
||||||
surface_DrawRect(0, 20, w, h - 20)
|
function tabs:OnActiveTabChanged( old, new )
|
||||||
|
if new:GetPanel().cbox_id == "\1chat" then
|
||||||
|
cbox.chatbox.panels.input:RequestFocus()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
for id, tab in next, cbox.chatbox.tabs do
|
for id, tab in next, cbox.chatbox.tabs do
|
||||||
|
@ -124,27 +257,68 @@ local function CreateChatbox()
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
|
|
||||||
tabs:AddSheet(tab.name, ret, tab.icon)
|
local sheet = tabs:AddSheet(tab.name, ret, tab.icon)
|
||||||
|
sheet.Panel.cbox_id = id
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
frame.btnClose:SetZPos(99)
|
local function Init()
|
||||||
|
local tab_files = file.Find("cbox/tabs/*", "LUA")
|
||||||
|
for _, name in ipairs(tab_files) do
|
||||||
|
cbox.utils.RealmPrint("Loading chatbox tab:", name)
|
||||||
|
include("cbox/tabs/" .. name)
|
||||||
end
|
end
|
||||||
|
|
||||||
if not IsValid(cbox.chatbox.panels.frame) then
|
if not IsValid(cbox.chatbox.panels.frame) then
|
||||||
CreateChatbox()
|
CreateChatbox()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
hook.Add("PlayerBindPress", "cbox.chatbox", function(ply, bind, pressed)
|
||||||
|
if bind ~= "messagemode" and bind ~= "messagemode2" then return end
|
||||||
|
if not pressed then return end
|
||||||
|
|
||||||
|
cbox.chatbox.Open(bind == "messagemode2")
|
||||||
|
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
---Opens the chatbox
|
---Opens the chatbox
|
||||||
---@param alt? boolean Was this request to open made by messagemode2 (team chat)
|
---@param alt? boolean Was this request to open made by messagemode2 (team chat)
|
||||||
function cbox.chatbox.Open(alt)
|
function cbox.chatbox.Open(alt)
|
||||||
alt = alt ~= nil and alt or false
|
alt = alt ~= nil and alt or false
|
||||||
|
|
||||||
if not IsValid(cbox.chatbox.panels.frame) then
|
local frame = cbox.chatbox.panels.frame
|
||||||
|
|
||||||
|
if not IsValid(frame) then
|
||||||
CreateChatbox()
|
CreateChatbox()
|
||||||
end
|
end
|
||||||
|
|
||||||
cbox.chatbox.panels.frame:SetVisible(true)
|
frame:SetVisible(true)
|
||||||
cbox.chatbox.panels.frame:MakePopup()
|
|
||||||
|
if frame.animFade and CHATBOX_FADE:GetBool() then
|
||||||
|
frame.animFade:Start(0.1, false)
|
||||||
|
else
|
||||||
|
frame:MakePopup()
|
||||||
|
end
|
||||||
|
|
||||||
|
if not IsValid(cbox.chatbox.panels.input) then
|
||||||
|
-- attempt to reinit
|
||||||
|
if IsValid(frame) then
|
||||||
|
frame:Remove()
|
||||||
|
end
|
||||||
|
Init()
|
||||||
|
end
|
||||||
|
|
||||||
|
if not IsValid(cbox.chatbox.panels.input) then
|
||||||
|
cbox.utils.RealmError("Input isn't valid, chat tab failed to load, bailing!")
|
||||||
|
if IsValid(frame) then
|
||||||
|
frame:Remove()
|
||||||
|
end
|
||||||
|
hook.Remove("PlayerBindPress", "cbox.chatbox")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
cbox.chatbox.panels.input:RequestFocus()
|
cbox.chatbox.panels.input:RequestFocus()
|
||||||
|
|
||||||
hook.Run("StartChat")
|
hook.Run("StartChat")
|
||||||
|
@ -152,7 +326,13 @@ end
|
||||||
|
|
||||||
---Closes the chatbox
|
---Closes the chatbox
|
||||||
function cbox.chatbox.Close()
|
function cbox.chatbox.Close()
|
||||||
cbox.chatbox.panels.frame:Close()
|
local frame = cbox.chatbox.panels.frame
|
||||||
|
|
||||||
|
if frame.animFade and CHATBOX_FADE:GetBool() then
|
||||||
|
frame.animFade:Start(0.1, true)
|
||||||
|
else
|
||||||
|
frame:Close()
|
||||||
|
end
|
||||||
|
|
||||||
hook.Run("FinishChat")
|
hook.Run("FinishChat")
|
||||||
|
|
||||||
|
@ -160,14 +340,15 @@ function cbox.chatbox.Close()
|
||||||
hook.Run("ChatTextChanged", "")
|
hook.Run("ChatTextChanged", "")
|
||||||
end
|
end
|
||||||
|
|
||||||
hook.Add("PlayerBindPress", "cbox.chatbox", function(ply, bind, pressed)
|
hook.Add("Initialize", "cbox.chatbox", Init)
|
||||||
if bind ~= "messagemode" and bind ~= "messagemode2" then return end
|
|
||||||
|
|
||||||
cbox.chatbox.Open(bind == "messagemode2")
|
|
||||||
|
|
||||||
return true
|
|
||||||
end)
|
|
||||||
|
|
||||||
concommand.Add("cbox_chatbox_reload", function()
|
concommand.Add("cbox_chatbox_reload", function()
|
||||||
CreateChatbox()
|
CreateChatbox()
|
||||||
end, nil, "Reloads the chatbox")
|
end, nil, "Reloads the chatbox")
|
||||||
|
|
||||||
|
concommand.Add("_cbox_chatbox_fullreload", function()
|
||||||
|
if IsValid(cbox.chatbox.panels.frame) then
|
||||||
|
cbox.chatbox.panels.frame:Remove()
|
||||||
|
end
|
||||||
|
Init()
|
||||||
|
end, nil, "Fully reinitializes the chatbox, use only in extreme breakage.")
|
||||||
|
|
|
@ -1,9 +1,23 @@
|
||||||
|
local Color = Color
|
||||||
|
|
||||||
|
local surface = surface
|
||||||
|
|
||||||
|
local surface_DrawRect = surface.DrawRect
|
||||||
|
local surface_SetDrawColor = surface.SetDrawColor
|
||||||
|
|
||||||
|
local INPUT_TEXT_COLOR = Color(221, 221, 221)
|
||||||
|
local INPUT_HIGHLIGHT_COLOR = Color(192, 28, 0, 140)
|
||||||
|
|
||||||
cbox.chatbox.AddTab("\1chat", "Chat", "icon16/comments.png", function()
|
cbox.chatbox.AddTab("\1chat", "Chat", "icon16/comments.png", function()
|
||||||
local wrapper = vgui.Create("EditablePanel")
|
local wrapper = vgui.Create("EditablePanel")
|
||||||
|
|
||||||
-- TODO: custom richtext panel
|
-- TODO: custom richtext panel
|
||||||
local history = vgui.Create("RichText", wrapper)
|
local history = vgui.Create("RichText", wrapper)
|
||||||
history:Dock(FILL)
|
history:Dock(FILL)
|
||||||
|
function history:Paint(w, h)
|
||||||
|
surface_SetDrawColor(0, 0, 0, 128)
|
||||||
|
surface_DrawRect(0, 0, w, h)
|
||||||
|
end
|
||||||
cbox.chatbox.panels.history = history
|
cbox.chatbox.panels.history = history
|
||||||
|
|
||||||
function history:PerformLayout()
|
function history:PerformLayout()
|
||||||
|
@ -13,18 +27,35 @@ cbox.chatbox.AddTab("\1chat", "Chat", "icon16/comments.png", function()
|
||||||
|
|
||||||
local input_wrapper = vgui.Create("EditablePanel", wrapper)
|
local input_wrapper = vgui.Create("EditablePanel", wrapper)
|
||||||
input_wrapper:SetHeight(20)
|
input_wrapper:SetHeight(20)
|
||||||
|
input_wrapper:DockMargin(0, 8, 0, 0)
|
||||||
input_wrapper:Dock(BOTTOM)
|
input_wrapper:Dock(BOTTOM)
|
||||||
|
|
||||||
local input = vgui.Create("DTextEntry", input_wrapper)
|
local input = vgui.Create("DTextEntry", input_wrapper)
|
||||||
|
input:DockMargin(4, 0, 0, 0)
|
||||||
input:Dock(FILL)
|
input:Dock(FILL)
|
||||||
|
input:SetFont("ChatFont")
|
||||||
cbox.chatbox.panels.input = input
|
cbox.chatbox.panels.input = input
|
||||||
|
|
||||||
|
function input:Paint(w, h)
|
||||||
|
surface_SetDrawColor(0, 0, 0, 128)
|
||||||
|
surface_DrawRect(0, 0, w, h)
|
||||||
|
|
||||||
|
self:DrawTextEntryText(INPUT_TEXT_COLOR, INPUT_HIGHLIGHT_COLOR, INPUT_TEXT_COLOR)
|
||||||
|
end
|
||||||
|
|
||||||
local mode_switch = vgui.Create("DButton", input_wrapper)
|
local mode_switch = vgui.Create("DButton", input_wrapper)
|
||||||
|
mode_switch:SetFont("ChatFont")
|
||||||
|
mode_switch:SetTextColor(INPUT_TEXT_COLOR)
|
||||||
mode_switch:SetText("Say")
|
mode_switch:SetText("Say")
|
||||||
mode_switch:SizeToContents()
|
mode_switch:SizeToContents()
|
||||||
mode_switch:Dock(LEFT)
|
mode_switch:Dock(LEFT)
|
||||||
cbox.chatbox.panels.mode_switch = mode_switch
|
cbox.chatbox.panels.mode_switch = mode_switch
|
||||||
|
|
||||||
|
function mode_switch:Paint(w, h)
|
||||||
|
surface_SetDrawColor(0, 0, 0, 128)
|
||||||
|
surface_DrawRect(0, 0, w, h)
|
||||||
|
end
|
||||||
|
|
||||||
function mode_switch:DoClick()
|
function mode_switch:DoClick()
|
||||||
-- TODO
|
-- TODO
|
||||||
end
|
end
|
||||||
|
|
14
lua/cbox/tabs/settings.lua
Normal file
14
lua/cbox/tabs/settings.lua
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
local surface = surface
|
||||||
|
|
||||||
|
local surface_DrawRect = surface.DrawRect
|
||||||
|
local surface_SetDrawColor = surface.SetDrawColor
|
||||||
|
|
||||||
|
cbox.chatbox.AddTab("zzzzzzzsettings", "Settings", "icon16/cog.png", function()
|
||||||
|
local wrapper = vgui.Create("EditablePanel")
|
||||||
|
function wrapper:Paint(w, h)
|
||||||
|
surface_SetDrawColor(0, 0, 0, 128)
|
||||||
|
surface_DrawRect(0, 0, w, h)
|
||||||
|
end
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
end)
|
Loading…
Reference in a new issue