Files
Nanami-UI/Whisper.lua
2026-03-18 02:01:36 +08:00

909 lines
36 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)