909 lines
36 KiB
Lua
909 lines
36 KiB
Lua
local CFG_THEME = SFrames.ActiveTheme
|
||
|
||
SFrames.Whisper = SFrames.Whisper or {}
|
||
SFrames.Whisper.history = SFrames.Whisper.history or {}
|
||
SFrames.Whisper.contacts = SFrames.Whisper.contacts or {}
|
||
SFrames.Whisper.unreadCount = SFrames.Whisper.unreadCount or {}
|
||
SFrames.Whisper.activeContact = nil
|
||
|
||
local MAX_WHISPER_CONTACTS = 200
|
||
local MAX_MESSAGES_PER_CONTACT = 100
|
||
|
||
function SFrames.Whisper:SaveCache()
|
||
if not SFramesDB then SFramesDB = {} end
|
||
SFramesDB.whisperContacts = {}
|
||
SFramesDB.whisperHistory = {}
|
||
for _, contact in ipairs(self.contacts) do
|
||
table.insert(SFramesDB.whisperContacts, contact)
|
||
if self.history[contact] then
|
||
local msgs = self.history[contact]
|
||
-- Only persist the last MAX_MESSAGES_PER_CONTACT messages per contact
|
||
local start = math.max(1, table.getn(msgs) - MAX_MESSAGES_PER_CONTACT + 1)
|
||
local trimmed = {}
|
||
for i = start, table.getn(msgs) do
|
||
table.insert(trimmed, { time = msgs[i].time, text = msgs[i].text, isMe = msgs[i].isMe, translated = msgs[i].translated })
|
||
end
|
||
SFramesDB.whisperHistory[contact] = trimmed
|
||
end
|
||
end
|
||
end
|
||
|
||
function SFrames.Whisper:LoadCache()
|
||
if not SFramesDB then return end
|
||
if type(SFramesDB.whisperContacts) ~= "table" or type(SFramesDB.whisperHistory) ~= "table" then return end
|
||
|
||
self.contacts = {}
|
||
self.history = {}
|
||
for _, contact in ipairs(SFramesDB.whisperContacts) do
|
||
if type(contact) == "string" and SFramesDB.whisperHistory[contact] and table.getn(SFramesDB.whisperHistory[contact]) > 0 then
|
||
table.insert(self.contacts, contact)
|
||
self.history[contact] = SFramesDB.whisperHistory[contact]
|
||
end
|
||
end
|
||
-- Trim to max contacts (remove oldest = last in list)
|
||
while table.getn(self.contacts) > MAX_WHISPER_CONTACTS do
|
||
local oldest = table.remove(self.contacts)
|
||
if oldest then
|
||
self.history[oldest] = nil
|
||
end
|
||
end
|
||
end
|
||
|
||
function SFrames.Whisper:RemoveContact(contact)
|
||
for i, v in ipairs(self.contacts) do
|
||
if v == contact then
|
||
table.remove(self.contacts, i)
|
||
break
|
||
end
|
||
end
|
||
self.history[contact] = nil
|
||
self.unreadCount[contact] = nil
|
||
if self.activeContact == contact then
|
||
self.activeContact = self.contacts[1]
|
||
end
|
||
self:SaveCache()
|
||
if self.frame and self.frame:IsShown() then
|
||
self:UpdateContacts()
|
||
self:UpdateMessages()
|
||
end
|
||
end
|
||
|
||
function SFrames.Whisper:ClearAllHistory()
|
||
self.history = {}
|
||
self.contacts = {}
|
||
self.unreadCount = {}
|
||
self.activeContact = nil
|
||
self:SaveCache()
|
||
if self.frame and self.frame:IsShown() then
|
||
self:UpdateContacts()
|
||
self:UpdateMessages()
|
||
end
|
||
end
|
||
|
||
local function FormatTime()
|
||
local h, m = GetGameTime()
|
||
return string.format("%02d:%02d", h, m)
|
||
end
|
||
|
||
local function GetFont()
|
||
if SFrames and SFrames.GetFont then return SFrames:GetFont() end
|
||
return "Fonts\\ARIALN.TTF"
|
||
end
|
||
|
||
local function SetRoundBackdrop(frame, bgColor, borderColor)
|
||
frame:SetBackdrop({
|
||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||
tile = true, tileSize = 16, edgeSize = 14,
|
||
insets = { left = 3, right = 3, top = 3, bottom = 3 },
|
||
})
|
||
local bg = bgColor or CFG_THEME.panelBg
|
||
local bd = borderColor or CFG_THEME.panelBorder
|
||
frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1)
|
||
frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1)
|
||
end
|
||
|
||
local function CreateShadow(parent, size)
|
||
local s = CreateFrame("Frame", nil, parent)
|
||
local sz = size or 4
|
||
s:SetPoint("TOPLEFT", parent, "TOPLEFT", -sz, sz)
|
||
s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", sz, -sz)
|
||
s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0))
|
||
s:SetBackdrop({
|
||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||
tile = true, tileSize = 16, edgeSize = 16,
|
||
insets = { left = 4, right = 4, top = 4, bottom = 4 },
|
||
})
|
||
s:SetBackdropColor(0, 0, 0, 0.55)
|
||
s:SetBackdropBorderColor(0, 0, 0, 0.4)
|
||
return s
|
||
end
|
||
|
||
local function EnsureBackdrop(frame)
|
||
if not frame then return end
|
||
if frame.sfCfgBackdrop then return end
|
||
SetRoundBackdrop(frame)
|
||
frame.sfCfgBackdrop = true
|
||
end
|
||
|
||
local function StyleActionButton(btn, label)
|
||
SetRoundBackdrop(btn, CFG_THEME.buttonBg, CFG_THEME.buttonBorder)
|
||
local fs = btn:CreateFontString(nil, "OVERLAY")
|
||
fs:SetFont(GetFont(), 12, "OUTLINE")
|
||
fs:SetTextColor(CFG_THEME.btnText[1], CFG_THEME.btnText[2], CFG_THEME.btnText[3])
|
||
fs:SetPoint("CENTER", btn, "CENTER", 0, 0)
|
||
if label then fs:SetText(label) end
|
||
btn.nLabel = fs
|
||
|
||
btn:SetScript("OnEnter", function()
|
||
this:SetBackdropColor(CFG_THEME.buttonHoverBg[1], CFG_THEME.buttonHoverBg[2], CFG_THEME.buttonHoverBg[3], CFG_THEME.buttonHoverBg[4])
|
||
this:SetBackdropBorderColor(CFG_THEME.btnHoverBorder[1], CFG_THEME.btnHoverBorder[2], CFG_THEME.btnHoverBorder[3], CFG_THEME.btnHoverBorder[4])
|
||
if this.nLabel then this.nLabel:SetTextColor(CFG_THEME.btnActiveText[1], CFG_THEME.btnActiveText[2], CFG_THEME.btnActiveText[3]) end
|
||
end)
|
||
btn:SetScript("OnLeave", function()
|
||
this:SetBackdropColor(CFG_THEME.buttonBg[1], CFG_THEME.buttonBg[2], CFG_THEME.buttonBg[3], CFG_THEME.buttonBg[4])
|
||
this:SetBackdropBorderColor(CFG_THEME.buttonBorder[1], CFG_THEME.buttonBorder[2], CFG_THEME.buttonBorder[3], CFG_THEME.buttonBorder[4])
|
||
if this.nLabel then this.nLabel:SetTextColor(CFG_THEME.btnText[1], CFG_THEME.btnText[2], CFG_THEME.btnText[3]) end
|
||
end)
|
||
btn:SetScript("OnMouseDown", function()
|
||
this:SetBackdropColor(CFG_THEME.buttonDownBg[1], CFG_THEME.buttonDownBg[2], CFG_THEME.buttonDownBg[3], CFG_THEME.buttonDownBg[4])
|
||
end)
|
||
btn:SetScript("OnMouseUp", function()
|
||
this:SetBackdropColor(CFG_THEME.buttonHoverBg[1], CFG_THEME.buttonHoverBg[2], CFG_THEME.buttonHoverBg[3], CFG_THEME.buttonHoverBg[4])
|
||
end)
|
||
return fs
|
||
end
|
||
|
||
local function IsNotFullyChinese(text)
|
||
-- If there's any english letter, we consider it requires translation
|
||
return string.find(text, "[a-zA-Z]") ~= nil
|
||
end
|
||
|
||
local function TranslateMessage(text, callback)
|
||
if SFramesDB and SFramesDB.Chat and SFramesDB.Chat.translateEnabled == false then
|
||
callback(nil)
|
||
return
|
||
end
|
||
if not IsNotFullyChinese(text) then
|
||
callback(nil)
|
||
return
|
||
end
|
||
|
||
local cleanText = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
|
||
cleanText = string.gsub(cleanText, "|r", "")
|
||
cleanText = string.gsub(cleanText, "|H.-|h(.-)|h", "%1")
|
||
|
||
if _G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady() then
|
||
_G.STranslateAPI.Translate(cleanText, "auto", "zh", function(result, err, meta)
|
||
if result then
|
||
callback(result)
|
||
end
|
||
end, "Nanami-UI")
|
||
else
|
||
callback(nil)
|
||
end
|
||
end
|
||
|
||
function SFrames.Whisper:AddMessage(sender, text, isMe)
|
||
if not self.history[sender] then
|
||
self.history[sender] = {}
|
||
table.insert(self.contacts, 1, sender)
|
||
else
|
||
-- Move to front
|
||
for i, v in ipairs(self.contacts) do
|
||
if v == sender then
|
||
table.remove(self.contacts, i)
|
||
break
|
||
end
|
||
end
|
||
table.insert(self.contacts, 1, sender)
|
||
end
|
||
|
||
local msgData = {
|
||
time = FormatTime(),
|
||
text = text,
|
||
isMe = isMe
|
||
}
|
||
table.insert(self.history[sender], msgData)
|
||
|
||
if not isMe then
|
||
PlaySound("TellIncoming")
|
||
if self.activeContact ~= sender or not (self.frame and self.frame:IsShown()) then
|
||
self.unreadCount[sender] = (self.unreadCount[sender] or 0) + 1
|
||
if SFrames.Chat and SFrames.Chat.frame and SFrames.Chat.frame.whisperButton then
|
||
SFrames.Chat.frame.whisperButton.hasUnread = true
|
||
if SFrames.Chat.frame.whisperButton.flashFrame then
|
||
SFrames.Chat.frame.whisperButton.flashFrame:Show()
|
||
end
|
||
end
|
||
end
|
||
|
||
TranslateMessage(text, function(translated)
|
||
if translated and translated ~= "" then
|
||
msgData.translated = "(翻译) " .. translated
|
||
if self.frame and self.frame:IsShown() and self.activeContact == sender then
|
||
self:UpdateMessages()
|
||
end
|
||
end
|
||
end)
|
||
end
|
||
|
||
-- Trim contacts if exceeding max
|
||
while table.getn(self.contacts) > MAX_WHISPER_CONTACTS do
|
||
local oldest = table.remove(self.contacts)
|
||
if oldest then
|
||
self.history[oldest] = nil
|
||
self.unreadCount[oldest] = nil
|
||
end
|
||
end
|
||
|
||
if self.frame and self.frame:IsShown() then
|
||
self:UpdateContacts()
|
||
if self.activeContact == sender then
|
||
self:UpdateMessages()
|
||
end
|
||
end
|
||
|
||
self:SaveCache()
|
||
end
|
||
|
||
function SFrames.Whisper:SelectContact(contact)
|
||
self.activeContact = contact
|
||
self.unreadCount[contact] = 0
|
||
self:UpdateContacts()
|
||
self:UpdateMessages()
|
||
|
||
-- Clear global unread state if no more unread
|
||
local totalUnread = 0
|
||
for k, v in pairs(self.unreadCount) do
|
||
totalUnread = totalUnread + v
|
||
end
|
||
if totalUnread == 0 and SFrames.Chat and SFrames.Chat.frame and SFrames.Chat.frame.whisperButton then
|
||
SFrames.Chat.frame.whisperButton.hasUnread = false
|
||
if SFrames.Chat.frame.whisperButton.flashFrame then
|
||
SFrames.Chat.frame.whisperButton.flashFrame:Hide()
|
||
end
|
||
end
|
||
|
||
if self.frame and self.frame.editBox then
|
||
self.frame.editBox:SetText("")
|
||
self.frame.editBox:SetFocus()
|
||
end
|
||
end
|
||
|
||
function SFrames.Whisper:UpdateContacts()
|
||
if not self.frame or not self.frame.contactList then return end
|
||
|
||
local scrollChild = self.frame.contactList.scrollChild
|
||
for _, child in ipairs({scrollChild:GetChildren()}) do
|
||
child:Hide()
|
||
end
|
||
|
||
local yOffset = -5
|
||
local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
|
||
|
||
for i, contact in ipairs(self.contacts) do
|
||
local btn = self.contactButtons and self.contactButtons[i]
|
||
if not btn then
|
||
btn = CreateFrame("Button", nil, scrollChild)
|
||
btn:SetWidth(120)
|
||
btn:SetHeight(24)
|
||
|
||
EnsureBackdrop(btn)
|
||
|
||
local txt = btn:CreateFontString(nil, "OVERLAY")
|
||
txt:SetFont(fontPath, 12, "OUTLINE")
|
||
txt:SetPoint("LEFT", btn, "LEFT", 8, 0)
|
||
btn.txt = txt
|
||
|
||
local closeBtn = CreateFrame("Button", nil, btn)
|
||
closeBtn:SetWidth(16)
|
||
closeBtn:SetHeight(16)
|
||
closeBtn:SetPoint("RIGHT", btn, "RIGHT", -2, 0)
|
||
|
||
local closeTxt = closeBtn:CreateFontString(nil, "OVERLAY")
|
||
closeTxt:SetFont(fontPath, 10, "OUTLINE")
|
||
closeTxt:SetPoint("CENTER", closeBtn, "CENTER", 0, 1)
|
||
closeTxt:SetText("x")
|
||
closeTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3])
|
||
closeBtn:SetFontString(closeTxt)
|
||
closeBtn:SetScript("OnEnter", function() closeTxt:SetTextColor(1, 0.4, 0.5) end)
|
||
closeBtn:SetScript("OnLeave", function() closeTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3]) end)
|
||
closeBtn:SetScript("OnClick", function()
|
||
SFrames.Whisper:RemoveContact(this:GetParent().contact)
|
||
end)
|
||
btn.closeBtn = closeBtn
|
||
|
||
local unreadTxt = btn:CreateFontString(nil, "OVERLAY")
|
||
unreadTxt:SetFont(fontPath, 10, "OUTLINE")
|
||
unreadTxt:SetPoint("RIGHT", closeBtn, "LEFT", -2, 0)
|
||
unreadTxt:SetTextColor(1, 0.4, 0.4)
|
||
btn.unreadTxt = unreadTxt
|
||
|
||
btn:SetScript("OnClick", function()
|
||
SFrames.Whisper:SelectContact(this.contact)
|
||
end)
|
||
|
||
btn:SetScript("OnEnter", function()
|
||
if this.contact ~= SFrames.Whisper.activeContact then
|
||
this:SetBackdropColor(CFG_THEME.buttonHoverBg[1], CFG_THEME.buttonHoverBg[2], CFG_THEME.buttonHoverBg[3], CFG_THEME.buttonHoverBg[4])
|
||
end
|
||
end)
|
||
btn:SetScript("OnLeave", function()
|
||
if this.contact ~= SFrames.Whisper.activeContact then
|
||
this:SetBackdropColor(0,0,0,0)
|
||
end
|
||
end)
|
||
|
||
if not self.contactButtons then self.contactButtons = {} end
|
||
self.contactButtons[i] = btn
|
||
end
|
||
|
||
btn.contact = contact
|
||
btn:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 5, yOffset)
|
||
btn.txt:SetText(contact)
|
||
|
||
local unreads = self.unreadCount[contact] or 0
|
||
if unreads > 0 then
|
||
btn.unreadTxt:SetText("("..unreads..")")
|
||
btn.txt:SetTextColor(1, 0.8, 0.2)
|
||
else
|
||
btn.unreadTxt:SetText("")
|
||
btn.txt:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
|
||
end
|
||
|
||
if contact == self.activeContact then
|
||
btn:SetBackdropColor(CFG_THEME.buttonBg[1], CFG_THEME.buttonBg[2], CFG_THEME.buttonBg[3], CFG_THEME.buttonBg[4])
|
||
btn:SetBackdropBorderColor(CFG_THEME.buttonBorder[1], CFG_THEME.buttonBorder[2], CFG_THEME.buttonBorder[3], CFG_THEME.buttonBorder[4])
|
||
else
|
||
btn:SetBackdropColor(0,0,0,0)
|
||
btn:SetBackdropBorderColor(0,0,0,0)
|
||
end
|
||
|
||
btn:Show()
|
||
yOffset = yOffset - 26
|
||
end
|
||
|
||
scrollChild:SetHeight(math.abs(yOffset))
|
||
self.frame.contactList:UpdateScrollChildRect()
|
||
local maxScroll = math.max(0, math.abs(yOffset) - 330)
|
||
local slider = _G["SFramesWhisperContactScrollScrollBar"]
|
||
if slider then
|
||
slider:SetMinMaxValues(0, maxScroll)
|
||
end
|
||
end
|
||
|
||
function SFrames.Whisper:UpdateMessages()
|
||
if not self.frame or not self.frame.messageScroll then return end
|
||
|
||
local scrollChild = self.frame.messageScroll.scrollChild
|
||
if self.msgLabels then
|
||
for i, fs in ipairs(self.msgLabels) do
|
||
fs:Hide()
|
||
end
|
||
end
|
||
if self.msgCopyBtns then
|
||
for i, btn in ipairs(self.msgCopyBtns) do
|
||
btn:Hide()
|
||
end
|
||
end
|
||
|
||
if not self.activeContact then return end
|
||
local messages = self.history[self.activeContact] or {}
|
||
|
||
local yOffset = -5
|
||
local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
|
||
|
||
if not self.msgLabels then self.msgLabels = {} end
|
||
if not self.msgCopyBtns then self.msgCopyBtns = {} end
|
||
|
||
for i, msg in ipairs(messages) do
|
||
local fs = self.msgLabels[i]
|
||
if not fs then
|
||
fs = scrollChild:CreateFontString(nil, "OVERLAY")
|
||
fs:SetFont(fontPath, 13, "OUTLINE")
|
||
fs:SetJustifyH("LEFT")
|
||
fs:SetWidth(380)
|
||
if fs.EnableMouse then fs:EnableMouse(true) end
|
||
self.msgLabels[i] = fs
|
||
end
|
||
|
||
local btn = self.msgCopyBtns[i]
|
||
if not btn then
|
||
btn = CreateFrame("Button", nil, scrollChild)
|
||
btn:SetWidth(20)
|
||
btn:SetHeight(16)
|
||
local txt = btn:CreateFontString(nil, "OVERLAY")
|
||
txt:SetFont(fontPath, 13, "OUTLINE")
|
||
txt:SetPoint("CENTER", btn, "CENTER", 0, 0)
|
||
txt:SetText("[+]")
|
||
txt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3])
|
||
btn:SetFontString(txt)
|
||
|
||
btn:SetScript("OnEnter", function() txt:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) end)
|
||
btn:SetScript("OnLeave", function() txt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3]) end)
|
||
btn:SetScript("OnClick", function()
|
||
if SFrames and SFrames.Chat and SFrames.Chat.OpenMessageContextMenu then
|
||
SFrames.Chat:OpenMessageContextMenu("whisper", this.rawText, this.senderName)
|
||
end
|
||
end)
|
||
self.msgCopyBtns[i] = btn
|
||
end
|
||
|
||
local color = msg.isMe and "|cff66ccff" or "|cffffbbee"
|
||
local nameStr = msg.isMe and "我" or self.activeContact
|
||
local textStr = string.format("%s[%s] %s:|r %s", color, msg.time, nameStr, msg.text)
|
||
|
||
if msg.translated then
|
||
textStr = textStr .. "\n|cffaaaaaa" .. msg.translated .. "|r"
|
||
end
|
||
|
||
btn.rawText = msg.text
|
||
btn.senderName = nameStr
|
||
btn:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 5, yOffset)
|
||
btn:Show()
|
||
|
||
fs:SetText(textStr)
|
||
fs:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 28, yOffset)
|
||
fs:Show()
|
||
|
||
yOffset = yOffset - fs:GetHeight() - 8
|
||
end
|
||
|
||
scrollChild:SetHeight(math.abs(yOffset))
|
||
self.frame.messageScroll:UpdateScrollChildRect()
|
||
local maxScroll = math.max(0, math.abs(yOffset) - 270)
|
||
local slider = _G["SFramesWhisperMessageScrollScrollBar"]
|
||
if slider then
|
||
slider:SetMinMaxValues(0, maxScroll)
|
||
slider:SetValue(maxScroll)
|
||
end
|
||
self.frame.messageScroll:SetVerticalScroll(maxScroll)
|
||
end
|
||
|
||
function SFrames.Whisper:EnsureFrame()
|
||
if self.frame then return end
|
||
|
||
local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
|
||
|
||
local f = CreateFrame("Frame", "SFramesWhisperContainer", UIParent)
|
||
f:SetWidth(580)
|
||
f:SetHeight(380)
|
||
f:SetPoint("CENTER", UIParent, "CENTER", 0, 100)
|
||
f:SetFrameStrata("HIGH")
|
||
f:SetMovable(true)
|
||
f:EnableMouse(true)
|
||
f:RegisterForDrag("LeftButton")
|
||
f:SetScript("OnDragStart", function() this:StartMoving() end)
|
||
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
|
||
|
||
if not self.enterHooked then
|
||
self.enterHooked = true
|
||
local orig_ChatFrame_OpenChat = ChatFrame_OpenChat
|
||
if orig_ChatFrame_OpenChat then
|
||
ChatFrame_OpenChat = function(text, chatFrame)
|
||
if SFrames and SFrames.Whisper and SFrames.Whisper.frame and SFrames.Whisper.frame:IsShown() and (not text or text == "") then
|
||
SFrames.Whisper.frame.editBox:SetFocus()
|
||
return
|
||
end
|
||
orig_ChatFrame_OpenChat(text, chatFrame)
|
||
end
|
||
end
|
||
end
|
||
|
||
table.insert(UISpecialFrames, "SFramesWhisperContainer")
|
||
|
||
SetRoundBackdrop(f, CFG_THEME.panelBg, CFG_THEME.panelBorder)
|
||
CreateShadow(f, 5)
|
||
|
||
local titleIcon = SFrames:CreateIcon(f, "chat", 14)
|
||
titleIcon:SetDrawLayer("OVERLAY")
|
||
titleIcon:SetPoint("TOPLEFT", f, "TOPLEFT", 15, -12)
|
||
titleIcon:SetVertexColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])
|
||
|
||
local title = f:CreateFontString(nil, "OVERLAY")
|
||
title:SetFont(fontPath, 14, "OUTLINE")
|
||
title:SetPoint("LEFT", titleIcon, "RIGHT", 4, 0)
|
||
title:SetText("私聊对话管理")
|
||
title:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])
|
||
|
||
local close = CreateFrame("Button", nil, f)
|
||
close:SetWidth(18)
|
||
close:SetHeight(18)
|
||
close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -8)
|
||
close:SetFrameLevel(f:GetFrameLevel() + 3)
|
||
SetRoundBackdrop(close, CFG_THEME.buttonDownBg or { 0.35, 0.06, 0.06, 0.85 }, CFG_THEME.buttonBorder or { 0.45, 0.1, 0.1, 0.6 })
|
||
local closeIco = SFrames:CreateIcon(close, "close", 12)
|
||
closeIco:SetDrawLayer("OVERLAY")
|
||
closeIco:SetPoint("CENTER", close, "CENTER", 0, 0)
|
||
closeIco:SetVertexColor(1, 0.7, 0.7)
|
||
close:SetScript("OnClick", function() f:Hide() end)
|
||
close:SetScript("OnEnter", function()
|
||
local h = CFG_THEME.buttonHoverBg or { 0.55, 0.1, 0.1, 0.95 }
|
||
local hb = CFG_THEME.btnHoverBd or { 0.65, 0.15, 0.15, 0.9 }
|
||
this:SetBackdropColor(h[1], h[2], h[3], h[4] or 0.95)
|
||
this:SetBackdropBorderColor(hb[1], hb[2], hb[3], hb[4] or 0.9)
|
||
end)
|
||
close:SetScript("OnLeave", function()
|
||
local d = CFG_THEME.buttonDownBg or { 0.35, 0.06, 0.06, 0.85 }
|
||
local db = CFG_THEME.buttonBorder or { 0.45, 0.1, 0.1, 0.6 }
|
||
this:SetBackdropColor(d[1], d[2], d[3], d[4] or 0.85)
|
||
this:SetBackdropBorderColor(db[1], db[2], db[3], db[4] or 0.6)
|
||
end)
|
||
|
||
-- Contact List
|
||
local contactList = CreateFrame("ScrollFrame", "SFramesWhisperContactScroll", f, "UIPanelScrollFrameTemplate")
|
||
contactList:SetWidth(130)
|
||
contactList:SetHeight(330)
|
||
contactList:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -40)
|
||
SetRoundBackdrop(contactList, CFG_THEME.listBg, CFG_THEME.listBorder)
|
||
|
||
local contactChild = CreateFrame("Frame", nil, contactList)
|
||
contactChild:SetWidth(130)
|
||
contactChild:SetHeight(10)
|
||
contactList:SetScrollChild(contactChild)
|
||
contactList.scrollChild = contactChild
|
||
f.contactList = contactList
|
||
|
||
-- Apply Nanami-UI scrollbar styling
|
||
local contactSlider = _G["SFramesWhisperContactScrollScrollBar"]
|
||
if contactSlider then
|
||
contactSlider:SetWidth(12)
|
||
local regions = { contactSlider:GetRegions() }
|
||
for i = 1, table.getn(regions) do
|
||
local region = regions[i]
|
||
if region and region.GetObjectType and region:GetObjectType() == "Texture" then
|
||
region:SetTexture(nil)
|
||
end
|
||
end
|
||
local track = contactSlider:CreateTexture(nil, "BACKGROUND")
|
||
track:SetTexture("Interface\\Buttons\\WHITE8X8")
|
||
track:SetPoint("TOPLEFT", contactSlider, "TOPLEFT", 3, 0)
|
||
track:SetPoint("BOTTOMRIGHT", contactSlider, "BOTTOMRIGHT", -3, 0)
|
||
track:SetVertexColor(0.22, 0.12, 0.18, 0.9)
|
||
|
||
if contactSlider.SetThumbTexture then
|
||
contactSlider:SetThumbTexture("Interface\\Buttons\\WHITE8X8")
|
||
end
|
||
local thumb = contactSlider.GetThumbTexture and contactSlider:GetThumbTexture()
|
||
if thumb then
|
||
thumb:SetWidth(8)
|
||
thumb:SetHeight(20)
|
||
thumb:SetVertexColor(0.85, 0.55, 0.70, 0.95)
|
||
end
|
||
|
||
local upBtn = _G["SFramesWhisperContactScrollScrollBarScrollUpButton"]
|
||
local downBtn = _G["SFramesWhisperContactScrollScrollBarScrollDownButton"]
|
||
if upBtn then upBtn:Hide() end
|
||
if downBtn then downBtn:Hide() end
|
||
|
||
contactSlider:ClearAllPoints()
|
||
contactSlider:SetPoint("TOPRIGHT", contactList, "TOPRIGHT", -2, -6)
|
||
contactSlider:SetPoint("BOTTOMRIGHT", contactList, "BOTTOMRIGHT", -2, 6)
|
||
end
|
||
|
||
-- Message List
|
||
local messageScroll = CreateFrame("ScrollFrame", "SFramesWhisperMessageScroll", f, "UIPanelScrollFrameTemplate")
|
||
messageScroll:SetWidth(405)
|
||
messageScroll:SetHeight(270)
|
||
messageScroll:SetPoint("TOPLEFT", contactList, "TOPRIGHT", 5, 0)
|
||
SetRoundBackdrop(messageScroll, CFG_THEME.listBg, CFG_THEME.listBorder)
|
||
|
||
local messageChild = CreateFrame("Frame", nil, messageScroll)
|
||
messageChild:SetWidth(405)
|
||
messageChild:SetHeight(10)
|
||
messageChild:EnableMouse(true)
|
||
-- Hyperlink 脚本仅部分帧类型支持,pcall 防止不支持的客户端报错
|
||
pcall(function()
|
||
messageChild:SetScript("OnHyperlinkClick", function()
|
||
local link = arg1
|
||
if not link then return end
|
||
if IsShiftKeyDown() then
|
||
if ChatFrameEditBox and ChatFrameEditBox:IsShown() then
|
||
ChatFrameEditBox:Insert(link)
|
||
elseif SFrames.Whisper.frame and SFrames.Whisper.frame.editBox then
|
||
SFrames.Whisper.frame.editBox:Insert(link)
|
||
end
|
||
else
|
||
pcall(function() SetItemRef(link, arg2, arg3) end)
|
||
end
|
||
end)
|
||
end)
|
||
pcall(function()
|
||
messageChild:SetScript("OnHyperlinkEnter", function()
|
||
local link = arg1
|
||
if not link then return end
|
||
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
|
||
local ok = pcall(function() GameTooltip:SetHyperlink(link) end)
|
||
if ok then
|
||
GameTooltip:Show()
|
||
else
|
||
GameTooltip:Hide()
|
||
end
|
||
end)
|
||
end)
|
||
pcall(function()
|
||
messageChild:SetScript("OnHyperlinkLeave", function()
|
||
GameTooltip:Hide()
|
||
end)
|
||
end)
|
||
messageScroll:SetScrollChild(messageChild)
|
||
messageScroll.scrollChild = messageChild
|
||
f.messageScroll = messageScroll
|
||
|
||
local messageSlider = _G["SFramesWhisperMessageScrollScrollBar"]
|
||
if messageSlider then
|
||
messageSlider:SetWidth(12)
|
||
local regions = { messageSlider:GetRegions() }
|
||
for i = 1, table.getn(regions) do
|
||
local region = regions[i]
|
||
if region and region.GetObjectType and region:GetObjectType() == "Texture" then
|
||
region:SetTexture(nil)
|
||
end
|
||
end
|
||
local track = messageSlider:CreateTexture(nil, "BACKGROUND")
|
||
track:SetTexture("Interface\\Buttons\\WHITE8X8")
|
||
track:SetPoint("TOPLEFT", messageSlider, "TOPLEFT", 3, 0)
|
||
track:SetPoint("BOTTOMRIGHT", messageSlider, "BOTTOMRIGHT", -3, 0)
|
||
track:SetVertexColor(0.22, 0.12, 0.18, 0.9)
|
||
|
||
if messageSlider.SetThumbTexture then
|
||
messageSlider:SetThumbTexture("Interface\\Buttons\\WHITE8X8")
|
||
end
|
||
local thumb = messageSlider.GetThumbTexture and messageSlider:GetThumbTexture()
|
||
if thumb then
|
||
thumb:SetWidth(8)
|
||
thumb:SetHeight(20)
|
||
thumb:SetVertexColor(0.85, 0.55, 0.70, 0.95)
|
||
end
|
||
|
||
local upBtn = _G["SFramesWhisperMessageScrollScrollBarScrollUpButton"]
|
||
local downBtn = _G["SFramesWhisperMessageScrollScrollBarScrollDownButton"]
|
||
if upBtn then upBtn:Hide() end
|
||
if downBtn then downBtn:Hide() end
|
||
|
||
messageSlider:ClearAllPoints()
|
||
messageSlider:SetPoint("TOPRIGHT", messageScroll, "TOPRIGHT", -2, -22)
|
||
messageSlider:SetPoint("BOTTOMRIGHT", messageScroll, "BOTTOMRIGHT", -2, 22)
|
||
|
||
local topBtn = CreateFrame("Button", nil, messageScroll)
|
||
topBtn:SetWidth(12)
|
||
topBtn:SetHeight(12)
|
||
topBtn:SetPoint("BOTTOM", messageSlider, "TOP", 0, 4)
|
||
local topTxt = topBtn:CreateFontString(nil, "OVERLAY")
|
||
topTxt:SetFont(fontPath, 11, "OUTLINE")
|
||
topTxt:SetPoint("CENTER", topBtn, "CENTER", 0, 1)
|
||
topTxt:SetText("▲")
|
||
topTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3])
|
||
topBtn:SetFontString(topTxt)
|
||
topBtn:SetScript("OnEnter", function() topTxt:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) end)
|
||
topBtn:SetScript("OnLeave", function() topTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3]) end)
|
||
topBtn:SetScript("OnClick", function()
|
||
messageSlider:SetValue(0)
|
||
end)
|
||
|
||
local bottomBtn = CreateFrame("Button", nil, messageScroll)
|
||
bottomBtn:SetWidth(12)
|
||
bottomBtn:SetHeight(12)
|
||
bottomBtn:SetPoint("TOP", messageSlider, "BOTTOM", 0, -4)
|
||
local botTxt = bottomBtn:CreateFontString(nil, "OVERLAY")
|
||
botTxt:SetFont(fontPath, 11, "OUTLINE")
|
||
botTxt:SetPoint("CENTER", bottomBtn, "CENTER", 0, -1)
|
||
botTxt:SetText("▼")
|
||
botTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3])
|
||
bottomBtn:SetFontString(botTxt)
|
||
bottomBtn:SetScript("OnEnter", function() botTxt:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3]) end)
|
||
bottomBtn:SetScript("OnLeave", function() botTxt:SetTextColor(CFG_THEME.dimText[1], CFG_THEME.dimText[2], CFG_THEME.dimText[3]) end)
|
||
bottomBtn:SetScript("OnClick", function()
|
||
local _, maxVal = messageSlider:GetMinMaxValues()
|
||
messageSlider:SetValue(maxVal)
|
||
end)
|
||
|
||
if messageScroll.EnableMouseWheel then
|
||
messageScroll:EnableMouseWheel(true)
|
||
messageScroll:SetScript("OnMouseWheel", function()
|
||
local delta = arg1
|
||
local _, maxVal = messageSlider:GetMinMaxValues()
|
||
local val = messageSlider:GetValue() - delta * 40
|
||
if val < 0 then val = 0 end
|
||
if val > maxVal then val = maxVal end
|
||
messageSlider:SetValue(val)
|
||
end)
|
||
end
|
||
end
|
||
|
||
local contactSlider = _G["SFramesWhisperContactScrollScrollBar"]
|
||
if contactList and contactSlider and contactList.EnableMouseWheel then
|
||
contactList:EnableMouseWheel(true)
|
||
contactList:SetScript("OnMouseWheel", function()
|
||
local delta = arg1
|
||
local _, maxVal = contactSlider:GetMinMaxValues()
|
||
local val = contactSlider:GetValue() - delta * 30
|
||
if val < 0 then val = 0 end
|
||
if val > maxVal then val = maxVal end
|
||
contactSlider:SetValue(val)
|
||
end)
|
||
end
|
||
|
||
-- EditBox for replying
|
||
local editBox = CreateFrame("EditBox", "SFramesWhisperEditBox", f, "InputBoxTemplate")
|
||
editBox:SetWidth(405)
|
||
editBox:SetHeight(20)
|
||
editBox:SetPoint("TOPLEFT", messageScroll, "BOTTOMLEFT", 0, -10)
|
||
editBox:SetAutoFocus(false)
|
||
editBox:SetFont(fontPath, 13, "OUTLINE")
|
||
|
||
local translateCheck = CreateFrame("CheckButton", "SFramesWhisperTranslateCheck", f, "UICheckButtonTemplate")
|
||
translateCheck:SetWidth(24)
|
||
translateCheck:SetHeight(24)
|
||
translateCheck:SetPoint("TOPLEFT", editBox, "BOTTOMLEFT", -5, -6)
|
||
|
||
local txt = translateCheck:CreateFontString(nil, "OVERLAY")
|
||
txt:SetFont(fontPath, 11, "OUTLINE")
|
||
txt:SetPoint("LEFT", translateCheck, "RIGHT", 2, 0)
|
||
txt:SetText("发前自动翻译 (译后可修改再回车)")
|
||
txt:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
|
||
|
||
-- Hide the default text and reskin the button if you have defined StyleCfgCheck inside your environment
|
||
if _G["SFramesWhisperTranslateCheckText"] then
|
||
_G["SFramesWhisperTranslateCheckText"]:Hide()
|
||
end
|
||
-- Reskin checkbox
|
||
local function StyleCheck(cb)
|
||
local box = CreateFrame("Frame", nil, cb)
|
||
box:SetPoint("TOPLEFT", cb, "TOPLEFT", 2, -2)
|
||
box:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", -2, 2)
|
||
local boxLevel = (cb:GetFrameLevel() or 1) - 1
|
||
if boxLevel < 0 then boxLevel = 0 end
|
||
box:SetFrameLevel(boxLevel)
|
||
EnsureBackdrop(box)
|
||
if box.SetBackdropColor then
|
||
box:SetBackdropColor(CFG_THEME.buttonBg[1], CFG_THEME.buttonBg[2], CFG_THEME.buttonBg[3], CFG_THEME.buttonBg[4])
|
||
end
|
||
if box.SetBackdropBorderColor then
|
||
box:SetBackdropBorderColor(CFG_THEME.buttonBorder[1], CFG_THEME.buttonBorder[2], CFG_THEME.buttonBorder[3], CFG_THEME.buttonBorder[4])
|
||
end
|
||
cb.sfBox = box
|
||
|
||
if cb.GetNormalTexture and cb:GetNormalTexture() then cb:GetNormalTexture():Hide() end
|
||
if cb.GetPushedTexture and cb:GetPushedTexture() then cb:GetPushedTexture():Hide() end
|
||
if cb.GetHighlightTexture and cb:GetHighlightTexture() then cb:GetHighlightTexture():Hide() end
|
||
|
||
if cb.SetCheckedTexture then cb:SetCheckedTexture("Interface\\Buttons\\WHITE8X8") end
|
||
local checked = cb.GetCheckedTexture and cb:GetCheckedTexture()
|
||
if checked then
|
||
checked:ClearAllPoints()
|
||
checked:SetPoint("TOPLEFT", cb, "TOPLEFT", 5, -5)
|
||
checked:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", -5, 5)
|
||
checked:SetVertexColor(CFG_THEME.checkFill[1], CFG_THEME.checkFill[2], CFG_THEME.checkFill[3], CFG_THEME.checkFill[4])
|
||
end
|
||
end
|
||
StyleCheck(translateCheck)
|
||
|
||
f.translateCheck = translateCheck
|
||
|
||
local function SendReply()
|
||
if SFrames.Whisper.isTranslating then return end
|
||
|
||
local text = editBox:GetText()
|
||
if not text or text == "" or not SFrames.Whisper.activeContact then return end
|
||
|
||
if f.translateCheck and f.translateCheck:GetChecked() then
|
||
if text == SFrames.Whisper.lastTranslation then
|
||
SendChatMessage(text, "WHISPER", nil, SFrames.Whisper.activeContact)
|
||
editBox:SetText("")
|
||
editBox:ClearFocus()
|
||
SFrames.Whisper.lastTranslation = nil
|
||
else
|
||
local targetLang = "en"
|
||
if not string.find(text, "[\128-\255]") then
|
||
targetLang = "zh"
|
||
end
|
||
|
||
if _G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady() then
|
||
SFrames.Whisper.isTranslating = true
|
||
editBox:SetText("翻译中...")
|
||
_G.STranslateAPI.Translate(text, "auto", targetLang, function(result, err, meta)
|
||
SFrames.Whisper.isTranslating = false
|
||
if result then
|
||
editBox:SetText(result)
|
||
SFrames.Whisper.lastTranslation = result
|
||
editBox:SetFocus()
|
||
else
|
||
editBox:SetText(text)
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff3333[私聊翻译失败]|r " .. tostring(err))
|
||
end
|
||
end, "Nanami-UI")
|
||
else
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff3333[Nanami-UI] STranslate插件未加载|r")
|
||
SFrames.Whisper.lastTranslation = text
|
||
end
|
||
end
|
||
else
|
||
SendChatMessage(text, "WHISPER", nil, SFrames.Whisper.activeContact)
|
||
editBox:SetText("")
|
||
editBox:ClearFocus()
|
||
end
|
||
end
|
||
|
||
editBox:SetScript("OnEnterPressed", SendReply)
|
||
editBox:SetScript("OnEscapePressed", function() this:ClearFocus() end)
|
||
f.editBox = editBox
|
||
|
||
local sendBtn = CreateFrame("Button", nil, f)
|
||
sendBtn:SetWidth(60)
|
||
sendBtn:SetHeight(24)
|
||
sendBtn:SetPoint("TOPRIGHT", editBox, "BOTTOMRIGHT", 0, -5)
|
||
StyleActionButton(sendBtn, "发送")
|
||
sendBtn:SetScript("OnClick", SendReply)
|
||
|
||
f:Hide()
|
||
self.frame = f
|
||
end
|
||
|
||
function SFrames.Whisper:Toggle()
|
||
self:EnsureFrame()
|
||
if self.frame:IsShown() then
|
||
self.frame:Hide()
|
||
else
|
||
self.frame:Show()
|
||
self:UpdateContacts()
|
||
if self.contacts[1] and not self.activeContact then
|
||
self:SelectContact(self.contacts[1])
|
||
elseif self.activeContact then
|
||
self:SelectContact(self.activeContact)
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Filter out addon sync/data messages that abuse the whisper channel.
|
||
-- These are not real conversations and should not appear in the whisper UI.
|
||
local ADDON_WHISPER_PATTERNS = {
|
||
"^%[%u+:%u+%]", -- [ST:HB], [GS:REQ], etc.
|
||
"^%u+:%u+:%d", -- ADDON:CMD:data
|
||
"^<.->.+", -- <AddonName>data
|
||
"^%$%u+%$", -- $ADDON$
|
||
"^##%u+##", -- ##ADDON##
|
||
"^{.-}", -- {json-like data}
|
||
"^LVGS:", -- LevelGearSync
|
||
"^EEQ:", -- EquipExchange
|
||
"^GS:%d", -- GearScore sync
|
||
"^ST:%u", -- SpellTimer
|
||
}
|
||
|
||
local function IsAddonWhisper(text)
|
||
if not text or text == "" then return false end
|
||
for _, pat in ipairs(ADDON_WHISPER_PATTERNS) do
|
||
if string.find(text, pat) then return true end
|
||
end
|
||
-- Pure numeric data (e.g. "30343.996") with no real words
|
||
if string.find(text, "^[%d%.%s%-]+$") then return true end
|
||
return false
|
||
end
|
||
|
||
-- Hook Events
|
||
local eventFrame = CreateFrame("Frame")
|
||
eventFrame:RegisterEvent("CHAT_MSG_WHISPER")
|
||
eventFrame:RegisterEvent("CHAT_MSG_WHISPER_INFORM")
|
||
eventFrame:RegisterEvent("PLAYER_LOGIN")
|
||
eventFrame:SetScript("OnEvent", function()
|
||
if event == "PLAYER_LOGIN" then
|
||
SFrames.Whisper:LoadCache()
|
||
return
|
||
end
|
||
|
||
local text = arg1
|
||
local sender = arg2
|
||
if not sender or sender == "" then return end
|
||
if IsAddonWhisper(text) then return end
|
||
|
||
sender = string.gsub(sender, "-.*", "") -- remove realm name if attached
|
||
|
||
if event == "CHAT_MSG_WHISPER" then
|
||
SFrames.Whisper:AddMessage(sender, text, false)
|
||
elseif event == "CHAT_MSG_WHISPER_INFORM" then
|
||
SFrames.Whisper:AddMessage(sender, text, true)
|
||
end
|
||
end)
|