-------------------------------------------------------------------------------- -- Nanami-UI: Mail UI (Mail.lua) -- Replaces MailFrame with Nanami-UI styled interface -- Tabs: Inbox / Send -- Inbox: select-all / per-item checkboxes, batch collect -- Send: multi-item queue, split into individual sends -------------------------------------------------------------------------------- SFrames = SFrames or {} SFrames.Mail = {} local ML = SFrames.Mail SFramesDB = SFramesDB or {} -- Save original Blizzard functions BEFORE other addons (TurtleMail etc.) hook them. -- File-level locals are captured at load time, which is before later-alphabetical addons. local _ClickSendMailItemButton = ClickSendMailItemButton local _PickupContainerItem = PickupContainerItem local _ClearCursor = ClearCursor local _SendMail = SendMail -------------------------------------------------------------------------------- -- Theme -------------------------------------------------------------------------------- local T = SFrames.Theme:Extend({ moneyGold = { 1, 0.84, 0.0 }, moneySilver = { 0.78, 0.78, 0.78 }, moneyCopper = { 0.71, 0.43, 0.18 }, codColor = { 1.0, 0.35, 0.35 }, unreadMark = { 1.0, 0.85, 0.3 }, successText = { 0.3, 1.0, 0.4 }, errorText = { 1.0, 0.3, 0.3 }, friendColor = { 0.3, 1.0, 0.5 }, guildColor = { 0.4, 0.8, 1.0 }, whoColor = { 0.85, 0.85, 0.85 }, }) -------------------------------------------------------------------------------- -- Shared state table (avoids excessive upvalues) -------------------------------------------------------------------------------- local S = { frame = nil, -- MainFrame inboxRows = {}, currentTab = 1, inboxPage = 1, inboxChecked = {}, collectQueue = {}, collectTimer = nil, isCollecting = false, sendQueue = {}, isSending = false, collectElapsed = 0, multiSend = nil, -- active multi-send state table } local L = { W = 360, H = 480, HEADER = 34, PAD = 12, TAB_H = 28, BOTTOM = 46, ROWS = 8, ROW_H = 38, ICON = 30, MAX_SEND = 12, } -------------------------------------------------------------------------------- -- Helpers -------------------------------------------------------------------------------- local function GetFont() if SFrames and SFrames.GetFont then return SFrames:GetFont() end return "Fonts\\ARIALN.TTF" end local function SetRoundBackdrop(frame) 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 }, }) frame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) frame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) end local function CreateShadow(parent) local s = CreateFrame("Frame", nil, parent) s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) 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.45) s:SetBackdropBorderColor(0, 0, 0, 0.6) s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1)) return s end local function FormatMoneyString(copper) if not copper or copper <= 0 then return "0|cFFB87333c|r" end local g = math.floor(copper / 10000) local sv = math.floor(math.mod(copper, 10000) / 100) local c = math.mod(copper, 100) local parts = "" if g > 0 then parts = parts .. "|cFFFFD700" .. g .. "g|r " end if sv > 0 then parts = parts .. "|cFFC7C7CF" .. sv .. "s|r " end if c > 0 then parts = parts .. "|cFFB87333" .. c .. "c|r" end return parts end local function CreateMoneyIcons(parent, fontSize, iconSize) fontSize = fontSize or 10 iconSize = iconSize or 11 local font = GetFont() local mf = CreateFrame("Frame", nil, parent) mf:SetWidth(100); mf:SetHeight(iconSize + 2) local function MakePair(mfRef, r, g, b, tcL, tcR) local txt = mfRef:CreateFontString(nil, "OVERLAY") txt:SetFont(font, fontSize, "OUTLINE"); txt:SetTextColor(r, g, b) local tex = mfRef:CreateTexture(nil, "ARTWORK") tex:SetTexture("Interface\\MoneyFrame\\UI-MoneyIcons") tex:SetTexCoord(tcL, tcR, 0, 1); tex:SetWidth(iconSize); tex:SetHeight(iconSize) return txt, tex end mf.gTxt, mf.gTex = MakePair(mf, 1, 0.84, 0, 0, 0.25) mf.sTxt, mf.sTex = MakePair(mf, 0.78, 0.78, 0.81, 0.25, 0.5) mf.cTxt, mf.cTex = MakePair(mf, 0.72, 0.45, 0.2, 0.5, 0.75) function mf:SetMoney(copper) copper = copper or 0 local gv = math.floor(copper / 10000) local sv = math.floor(math.mod(copper, 10000) / 100) local cv = math.mod(copper, 100) self.gTxt:Hide(); self.gTex:Hide(); self.gTxt:ClearAllPoints() self.sTxt:Hide(); self.sTex:Hide(); self.sTxt:ClearAllPoints() self.cTxt:Hide(); self.cTex:Hide(); self.cTxt:ClearAllPoints() self.gTex:ClearAllPoints(); self.sTex:ClearAllPoints(); self.cTex:ClearAllPoints() if copper <= 0 then return end local anchor = nil local function Attach(txt, tex, val) txt:SetText(val) txt:ClearAllPoints(); tex:ClearAllPoints() if anchor then txt:SetPoint("LEFT", anchor, "RIGHT", 3, 0) else txt:SetPoint("LEFT", self, "LEFT", 0, 0) end tex:SetPoint("LEFT", txt, "RIGHT", 1, 0) txt:Show(); tex:Show() anchor = tex end if gv > 0 then Attach(self.gTxt, self.gTex, gv) end if sv > 0 then Attach(self.sTxt, self.sTex, sv) end if cv > 0 then Attach(self.cTxt, self.cTex, cv) end end return mf end local function FormatExpiry(daysLeft) if not daysLeft then return "" end local d = math.floor(daysLeft) if d <= 0 then return "|cFFFF3333即将过期|r" end if d == 1 then return "|cFFFF6633剩余 1 天|r" end if d <= 3 then return "|cFFFFAA33剩余 " .. d .. " 天|r" end return "|cFF88FF88" .. d .. " 天|r" end -------------------------------------------------------------------------------- -- Action Button Factory -------------------------------------------------------------------------------- local function CreateActionBtn(parent, text, w) local btn = CreateFrame("Button", nil, parent) btn:SetWidth(w or 100) btn:SetHeight(26) btn: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 }, }) btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) local fs = btn:CreateFontString(nil, "OVERLAY") fs:SetFont(GetFont(), 11, "OUTLINE") fs:SetPoint("CENTER", 0, 0) fs:SetText(text) fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) btn.label = fs btn.disabled = false function btn:SetDisabled(flag) self.disabled = flag if flag then self.label:SetTextColor(T.btnDisabledText[1], T.btnDisabledText[2], T.btnDisabledText[3]) self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], 0.5) else self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) end end btn:SetScript("OnEnter", function() if not this.disabled then this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end end) btn:SetScript("OnLeave", function() if not this.disabled then this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end end) btn:SetScript("OnMouseDown", function() if not this.disabled then this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4]) end end) btn:SetScript("OnMouseUp", function() if not this.disabled then this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) end end) return btn end -------------------------------------------------------------------------------- -- Tab Button Factory -------------------------------------------------------------------------------- local function CreateTabBtn(parent, text, w) local btn = CreateFrame("Button", nil, parent) btn:SetWidth(w or 70) btn:SetHeight(22) btn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) btn:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) btn:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], 0.5) local fs = btn:CreateFontString(nil, "OVERLAY") fs:SetFont(GetFont(), 12, "OUTLINE") fs:SetPoint("CENTER", 0, 0) fs:SetText(text) fs:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) btn.label = fs btn:SetScript("OnEnter", function() if not this.active then this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) end end) btn:SetScript("OnLeave", function() if this.active then this:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], T.tabActiveBg[4]) this:SetBackdropBorderColor(T.tabActiveBorder[1], T.tabActiveBorder[2], T.tabActiveBorder[3], T.tabActiveBorder[4]) else this:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) this:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], 0.5) end end) function btn:SetActive(flag) self.active = flag if flag then self:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], T.tabActiveBg[4]) self:SetBackdropBorderColor(T.tabActiveBorder[1], T.tabActiveBorder[2], T.tabActiveBorder[3], T.tabActiveBorder[4]) self.label:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3]) else self:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4]) self:SetBackdropBorderColor(T.tabBorder[1], T.tabBorder[2], T.tabBorder[3], 0.5) self.label:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3]) end end return btn end -------------------------------------------------------------------------------- -- Checkbox Factory -------------------------------------------------------------------------------- local function CreateSmallCheckbox(parent, size) local sz = size or 16 local cb = CreateFrame("Button", nil, parent) cb:SetWidth(sz); cb:SetHeight(sz) cb:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) cb:SetBackdropColor(T.checkBg[1], T.checkBg[2], T.checkBg[3], T.checkBg[4]) cb:SetBackdropBorderColor(T.checkBorder[1], T.checkBorder[2], T.checkBorder[3], T.checkBorder[4]) local mark = cb:CreateTexture(nil, "OVERLAY") mark:SetTexture("Interface\\Buttons\\UI-CheckBox-Check") mark:SetAllPoints(cb); mark:Hide() cb.mark = mark; cb.checked = false function cb:SetChecked(flag) self.checked = flag if flag then self.mark:Show(); self:SetBackdropColor(T.checkFill[1], T.checkFill[2], T.checkFill[3], 0.3) else self.mark:Hide(); self:SetBackdropColor(T.checkBg[1], T.checkBg[2], T.checkBg[3], T.checkBg[4]) end end function cb:IsChecked() return self.checked end cb:SetScript("OnClick", function() this:SetChecked(not this.checked) if this.onToggle then this.onToggle(this.checked) end end) cb:SetScript("OnEnter", function() this:SetBackdropBorderColor(T.checkHoverBorder[1], T.checkHoverBorder[2], T.checkHoverBorder[3], T.checkHoverBorder[4]) end) cb:SetScript("OnLeave", function() this:SetBackdropBorderColor(T.checkBorder[1], T.checkBorder[2], T.checkBorder[3], T.checkBorder[4]) end) return cb end -------------------------------------------------------------------------------- -- Styled EditBox Factory -------------------------------------------------------------------------------- local function CreateStyledEditBox(parent, width, height, numeric) local eb = CreateFrame("EditBox", nil, parent) eb:SetWidth(width or 120); eb:SetHeight(height or 22) eb:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 }, }) eb:SetBackdropColor(T.inputBg[1], T.inputBg[2], T.inputBg[3], T.inputBg[4] or 0.95) eb:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], 0.8) eb:SetFont(GetFont(), 11, "OUTLINE") eb:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) eb:SetJustifyH("LEFT"); eb:SetAutoFocus(false); eb:SetTextInsets(6, 6, 2, 2) if numeric then eb:SetNumeric(true) end return eb end -------------------------------------------------------------------------------- -- INBOX: Row Factory -------------------------------------------------------------------------------- local function CreateInboxRow(parent, index) local row = CreateFrame("Frame", "SFramesMailInboxRow" .. index, parent) row:SetWidth(L.W - L.PAD * 2); row:SetHeight(L.ROW_H) local bg = row:CreateTexture(nil, "BACKGROUND") bg:SetTexture("Interface\\Buttons\\WHITE8X8"); bg:SetAllPoints(row) local normalR, normalG, normalB, normalA if math.mod(index, 2) == 0 then normalR, normalG, normalB, normalA = T.rowNormal[1], T.rowNormal[2], T.rowNormal[3], T.rowNormal[4] else normalR, normalG, normalB, normalA = T.panelBg[1], T.panelBg[2], T.panelBg[3], 0.3 end bg:SetVertexColor(normalR, normalG, normalB, normalA) row.bg = bg local hl = row:CreateTexture(nil, "ARTWORK") hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") hl:SetBlendMode("ADD"); hl:SetAllPoints(row); hl:SetAlpha(0); hl:Hide() row.highlight = hl row:EnableMouse(false) local cb = CreateSmallCheckbox(row, 16) cb:SetPoint("LEFT", row, "LEFT", 4, 0) cb:SetFrameLevel(row:GetFrameLevel() + 10) row.checkbox = cb local iconFrame = CreateFrame("Frame", nil, row) iconFrame:SetWidth(L.ICON); iconFrame:SetHeight(L.ICON) iconFrame:SetPoint("LEFT", cb, "RIGHT", 6, 0) iconFrame:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 }, }) iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) iconFrame:EnableMouse(true) iconFrame:SetScript("OnEnter", function() if not row.mailIndex then return end row.highlight:SetAlpha(0.1); row.highlight:Show() GameTooltip:SetOwner(iconFrame, "ANCHOR_RIGHT") pcall(GameTooltip.SetInboxItem, GameTooltip, row.mailIndex) GameTooltip:Show() end) iconFrame:SetScript("OnLeave", function() row.highlight:SetAlpha(0); row.highlight:Hide() GameTooltip:Hide() end) iconFrame:SetScript("OnMouseUp", function() if row.mailIndex then ML:ShowMailDetail(row.mailIndex) end end) row.iconFrame = iconFrame local icon = iconFrame:CreateTexture(nil, "ARTWORK") icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) icon:SetPoint("TOPLEFT", 3, -3); icon:SetPoint("BOTTOMRIGHT", -3, 3) row.icon = icon local font = GetFont() local senderFS = row:CreateFontString(nil, "OVERLAY") senderFS:SetFont(font, 11, "OUTLINE") senderFS:SetPoint("TOPLEFT", iconFrame, "TOPRIGHT", 6, -2) senderFS:SetWidth(110); senderFS:SetJustifyH("LEFT") senderFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) row.senderFS = senderFS local subjectFS = row:CreateFontString(nil, "OVERLAY") subjectFS:SetFont(font, 9, "OUTLINE") subjectFS:SetPoint("TOPLEFT", senderFS, "BOTTOMLEFT", 0, -1) subjectFS:SetWidth(110); subjectFS:SetJustifyH("LEFT") subjectFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) row.subjectFS = subjectFS local moneyFrame = CreateMoneyIcons(row, 9, 10) moneyFrame:SetPoint("RIGHT", row, "RIGHT", -52, 6) row.moneyFrame = moneyFrame local codFS = row:CreateFontString(nil, "OVERLAY") codFS:SetFont(font, 9, "OUTLINE"); codFS:SetTextColor(1, 0.33, 0.33) codFS:SetPoint("RIGHT", moneyFrame, "LEFT", -2, 0) codFS:Hide() row.codFS = codFS local expiryFS = row:CreateFontString(nil, "OVERLAY") expiryFS:SetFont(font, 9, "OUTLINE") expiryFS:SetPoint("RIGHT", row, "RIGHT", -52, -6); expiryFS:SetJustifyH("RIGHT") row.expiryFS = expiryFS local takeBtn = CreateActionBtn(row, "收取", 40) takeBtn:SetHeight(17); takeBtn:SetPoint("TOPRIGHT", row, "TOPRIGHT", -6, -3) takeBtn:SetFrameLevel(row:GetFrameLevel() + 10) row.takeBtn = takeBtn local returnBtn = CreateActionBtn(row, "退回", 40) returnBtn:SetHeight(17); returnBtn:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", -6, 3) returnBtn:SetFrameLevel(row:GetFrameLevel() + 10) row.returnBtn = returnBtn row.mailIndex = nil return row end -------------------------------------------------------------------------------- -- INBOX: Update -------------------------------------------------------------------------------- local function UpdateInbox() if not S.frame or not S.frame:IsVisible() or S.currentTab ~= 1 then return end local numItems = GetInboxNumItems() local totalPages = math.max(1, math.ceil(numItems / L.ROWS)) if S.inboxPage > totalPages then S.inboxPage = totalPages end if S.inboxPage < 1 then S.inboxPage = 1 end S.frame.inboxPageText:SetText(string.format("第 %d / %d 页 (%d 封)", S.inboxPage, totalPages, numItems)) S.frame.inboxPrevBtn:SetDisabled(S.inboxPage <= 1) S.frame.inboxNextBtn:SetDisabled(S.inboxPage >= totalPages) for i = 1, L.ROWS do local row = S.inboxRows[i] local mi = (S.inboxPage - 1) * L.ROWS + i if mi <= numItems then local _, _, sender, subject, money, CODAmount, daysLeft, hasItem, wasRead = GetInboxHeaderInfo(mi) row.mailIndex = mi if hasItem then local _, itemTex = GetInboxItem(mi) row.icon:SetTexture(itemTex or "Interface\\Icons\\INV_Misc_Note_01") elseif money and money > 0 then row.icon:SetTexture("Interface\\Icons\\INV_Misc_Coin_01") else row.icon:SetTexture("Interface\\Icons\\INV_Misc_Note_01") end row.senderFS:SetText(sender or "未知") if not wasRead then row.senderFS:SetTextColor(T.unreadMark[1], T.unreadMark[2], T.unreadMark[3]) else row.senderFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) end row.subjectFS:SetText(subject or "") row.codFS:Hide(); row.codFS:SetText("") if money and money > 0 then row.moneyFrame:SetMoney(money) elseif CODAmount and CODAmount > 0 then row.codFS:SetText("COD:"); row.codFS:Show() row.moneyFrame:SetMoney(CODAmount) else row.moneyFrame:SetMoney(0) end row.expiryFS:SetText(FormatExpiry(daysLeft)) local canTake = (hasItem or (money and money > 0)) and (not CODAmount or CODAmount == 0) row.takeBtn:SetDisabled(not canTake) row.takeBtn:SetScript("OnClick", function() if row.mailIndex then if hasItem then TakeInboxItem(row.mailIndex) elseif money and money > 0 then local idx = row.mailIndex TakeInboxMoney(idx) if not S.deleteTimer then S.deleteTimer = CreateFrame("Frame") end S.deleteElapsed = 0 S.deleteTimer:SetScript("OnUpdate", function() S.deleteElapsed = S.deleteElapsed + arg1 if S.deleteElapsed >= 0.5 then this:SetScript("OnUpdate", nil) if idx <= GetInboxNumItems() then DeleteInboxItem(idx) end end end) end end end) row.returnBtn:SetScript("OnClick", function() if row.mailIndex then ReturnInboxItem(row.mailIndex) end end) row.checkbox:SetChecked(S.inboxChecked[mi] == true) row.checkbox.onToggle = function(checked) S.inboxChecked[mi] = checked or nil local any = false for _, v in pairs(S.inboxChecked) do if v then any = true; break end end S.frame.collectSelectedBtn:SetDisabled(not any) end if CODAmount and CODAmount > 0 then row.checkbox:SetChecked(false); row.checkbox.disabled = true; S.inboxChecked[mi] = nil else row.checkbox.disabled = false end row:Show() else row.mailIndex = nil; row:Hide() end end local hasChecked = false for _, v in pairs(S.inboxChecked) do if v then hasChecked = true; break end end S.frame.collectSelectedBtn:SetDisabled(not hasChecked and not S.isCollecting) S.frame.collectAllBtn:SetDisabled(numItems == 0 and not S.isCollecting) if S.isCollecting then S.frame.collectAllBtn.label:SetText("收取中...") S.frame.collectSelectedBtn.label:SetText("收取中...") else S.frame.collectAllBtn.label:SetText("全部收取") S.frame.collectSelectedBtn.label:SetText("收取选中") end end -------------------------------------------------------------------------------- -- INBOX: Batch Collect -------------------------------------------------------------------------------- local function StopCollecting() S.isCollecting = false; S.collectQueue = {}; S.collectPendingDelete = nil if S.collectTimer then S.collectTimer:SetScript("OnUpdate", nil) end UpdateInbox() end local function ProcessCollectQueue() if S.collectPendingDelete then local mi = S.collectPendingDelete S.collectPendingDelete = nil if mi <= GetInboxNumItems() then DeleteInboxItem(mi) end return end if table.getn(S.collectQueue) == 0 then StopCollecting(); return end local mi = table.remove(S.collectQueue, 1) if mi > GetInboxNumItems() then ProcessCollectQueue(); return end local _, _, _, _, money, CODAmount, _, hasItem = GetInboxHeaderInfo(mi) if CODAmount and CODAmount > 0 then ProcessCollectQueue(); return end if hasItem then TakeInboxItem(mi) elseif money and money > 0 then TakeInboxMoney(mi) S.collectPendingDelete = mi else DeleteInboxItem(mi) end end local function StartCollecting(indices) if S.isCollecting then return end S.collectQueue = {} for i = table.getn(indices), 1, -1 do table.insert(S.collectQueue, indices[i]) end if table.getn(S.collectQueue) == 0 then return end S.isCollecting = true; S.collectElapsed = 0 if not S.collectTimer then S.collectTimer = CreateFrame("Frame") end local nextDelay = 0 S.collectTimer:SetScript("OnUpdate", function() if not S.isCollecting then this:SetScript("OnUpdate", nil); return end S.collectElapsed = S.collectElapsed + arg1 if S.collectElapsed >= nextDelay then S.collectElapsed = 0; nextDelay = 0.5; ProcessCollectQueue() end end) UpdateInbox() end local function CollectAll() local n = GetInboxNumItems() if n == 0 then return end local idx = {} for i = 1, n do local _, _, _, _, _, COD = GetInboxHeaderInfo(i) if not COD or COD == 0 then table.insert(idx, i) end end StartCollecting(idx) end local function CollectSelected() local idx = {} for k, v in pairs(S.inboxChecked) do if v then table.insert(idx, k) end end table.sort(idx) if table.getn(idx) == 0 then return end S.inboxChecked = {} StartCollecting(idx) end local function SelectAllInbox() local n = GetInboxNumItems() for i = 1, n do local _, _, _, _, _, COD = GetInboxHeaderInfo(i) if not COD or COD == 0 then S.inboxChecked[i] = true end end UpdateInbox() end local function DeselectAllInbox() S.inboxChecked = {}; UpdateInbox() end -------------------------------------------------------------------------------- -- SEND: Item Queue -------------------------------------------------------------------------------- local function AddSendItem(bag, slot) if table.getn(S.sendQueue) >= L.MAX_SEND then DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 发送队列已满 (" .. L.MAX_SEND .. " 件)") return end local link = GetContainerItemLink(bag, slot) if not link then return end local texture, count = GetContainerItemInfo(bag, slot) for i = 1, table.getn(S.sendQueue) do if S.sendQueue[i].bag == bag and S.sendQueue[i].slot == slot then DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 该物品已在发送列表中"); return end end table.insert(S.sendQueue, { bag = bag, slot = slot, link = link, texture = texture, count = count or 1 }) end -------------------------------------------------------------------------------- -- SEND: Accept cursor drag (find the locked bag slot the cursor picked up) -------------------------------------------------------------------------------- local function AcceptCursorItem() if not CursorHasItem() then return false end for bag = 0, 4 do local slots = GetContainerNumSlots(bag) for slot = 1, slots do local _, _, locked = GetContainerItemInfo(bag, slot) if locked then AddSendItem(bag, slot) ClearCursor() ML:UpdateSendPanel() return true end end end ClearCursor() return false end local function RemoveSendItem(index) if index >= 1 and index <= table.getn(S.sendQueue) then table.remove(S.sendQueue, index) end end local function ClearSendItems() S.sendQueue = {} end local function FlashStatus(text, color, duration) if not S.frame or not S.frame.sendStatus then return end S.frame.sendStatus:SetText(text) S.frame.sendStatus:SetTextColor(color[1], color[2], color[3]) S.statusFadeElapsed = 0 if not S.statusFadeTimer then S.statusFadeTimer = CreateFrame("Frame") end S.statusFadeTimer:SetScript("OnUpdate", function() S.statusFadeElapsed = S.statusFadeElapsed + arg1 if S.statusFadeElapsed >= (duration or 3) then S.frame.sendStatus:SetText("") this:SetScript("OnUpdate", nil) end end) end -------------------------------------------------------------------------------- -- SEND: Multi-Send Logic -------------------------------------------------------------------------------- local function ResetSendForm() if not S.frame then return end ClearSendItems() if S.frame.toEditBox then S.frame.toEditBox:SetText("") end if S.frame.subjectEditBox then S.frame.subjectEditBox:SetText("") end if S.frame.bodyEditBox then S.frame.bodyEditBox:SetText("") end if S.frame.goldEB then S.frame.goldEB:SetText("0") end if S.frame.silverEB then S.frame.silverEB:SetText("0") end if S.frame.copperEB then S.frame.copperEB:SetText("0") end ML:UpdateSendPanel() end local function FinishMultiSend() local ms = S.multiSend S.multiSend = nil S.isSending = false if S.updateFrame then S.updateFrame:SetScript("OnUpdate", nil) end ResetSendForm() if S.frame and S.frame.sendBtn then S.frame.sendBtn.label:SetText("发送"); S.frame.sendBtn:SetDisabled(false) end if ms then DEFAULT_CHAT_FRAME:AddMessage("|cFF88FF88[Nanami-Mail]|r 所有 " .. ms.total .. " 封邮件已发送完成") FlashStatus("全部发送完成!", T.successText, 3) end end local function AbortMultiSend(reason) S.multiSend = nil S.isSending = false if S.updateFrame then S.updateFrame:SetScript("OnUpdate", nil) end if S.frame and S.frame.sendBtn then S.frame.sendBtn.label:SetText("发送"); S.frame.sendBtn:SetDisabled(false) end if S.frame and S.frame.sendStatus then S.frame.sendStatus:SetText("") end if reason then DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r " .. reason) FlashStatus("发送失败!", T.errorText, 5) end end local function DoMultiSend(recipient, subject, body, money) if S.isSending then return end if not recipient or recipient == "" then DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 请输入收件人名字"); return end if not subject or subject == "" then subject = "Mail" end if S.frame and S.frame.sendBtn then S.frame.sendBtn.label:SetText("发送中..."); S.frame.sendBtn:SetDisabled(true) end local items = {} for i = 1, table.getn(S.sendQueue) do table.insert(items, S.sendQueue[i]) end -- No attachments: plain text / money mail if table.getn(items) == 0 then if money and money > 0 then SetSendMailMoney(money) end SendMail(recipient, subject, body or "") return end S.multiSend = { items = items, recipient = recipient, subject = subject or "", body = body or "", money = money, total = table.getn(items), sentCount = 0, phase = "attach", -- "attach" → "wait_send" → "cooldown" → "attach" ... elapsed = 0, } S.isSending = true if not S.updateFrame then S.updateFrame = CreateFrame("Frame") end S.updateFrame:SetScript("OnUpdate", function() local ms = S.multiSend if not ms then this:SetScript("OnUpdate", nil); return end ms.elapsed = ms.elapsed + arg1 --------------------------------------------------------------- -- Phase: attach — pick next item, attach to Blizzard mail slot, then send --------------------------------------------------------------- if ms.phase == "attach" then ms.sentCount = ms.sentCount + 1 if ms.sentCount > ms.total then FinishMultiSend() return end local item = ms.items[ms.sentCount] if not item then FinishMultiSend(); return end if S.frame and S.frame.sendStatus then if S.statusFadeTimer then S.statusFadeTimer:SetScript("OnUpdate", nil) end S.frame.sendStatus:SetText("发送中 " .. ms.sentCount .. "/" .. ms.total) S.frame.sendStatus:SetTextColor(T.successText[1], T.successText[2], T.successText[3]) end -- Ensure Blizzard send tab is active if MailFrameTab_OnClick then MailFrameTab_OnClick(2) end -- Check item still in bag if not GetContainerItemLink(item.bag, item.slot) then DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 第" .. ms.sentCount .. "件物品已不在背包,跳过") ms.elapsed = 0 return end -- Attach: clear → pick up → place _ClearCursor() _ClickSendMailItemButton() _ClearCursor() _PickupContainerItem(item.bag, item.slot) _ClickSendMailItemButton() -- Verify if not GetSendMailItem() then DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 第" .. ms.sentCount .. "件附件挂载失败,跳过") _ClearCursor() ms.elapsed = 0 return end -- Money only on first mail if ms.sentCount == 1 and ms.money and ms.money > 0 then SetSendMailMoney(ms.money) end -- Send this single-attachment mail ms.sendOk = false ms.phase = "wait_send" ms.elapsed = 0 SendMail(ms.recipient, ms.subject, ms.body) --------------------------------------------------------------- -- Phase: wait_send — waiting for MAIL_SEND_SUCCESS --------------------------------------------------------------- elseif ms.phase == "wait_send" then if ms.sendOk then ms.phase = "cooldown" ms.elapsed = 0 elseif ms.elapsed >= 15 then AbortMultiSend("发送超时,已停止") end --------------------------------------------------------------- -- Phase: cooldown — let server & Blizzard UI fully reset --------------------------------------------------------------- elseif ms.phase == "cooldown" then if ms.elapsed >= 0.6 then ms.phase = "attach" ms.elapsed = 0 end end end) end -------------------------------------------------------------------------------- -- SEND: Panel Update -------------------------------------------------------------------------------- function ML:UpdateSendPanel() if not S.frame or S.currentTab ~= 2 then return end if not S.frame.sendItemSlots then return end for i = 1, L.MAX_SEND do local slot = S.frame.sendItemSlots[i] if not slot then break end local entry = S.sendQueue[i] if entry then slot.icon:SetTexture(entry.texture); slot.icon:Show() slot.countFS:SetText(entry.count > 1 and entry.count or "") slot.removeBtn:Show(); slot.hasItem = true else slot.icon:SetTexture(nil); slot.icon:Hide() slot.countFS:SetText(""); slot.removeBtn:Hide(); slot.hasItem = false end end end -------------------------------------------------------------------------------- -- BUILD: Main frame + header + tabs -------------------------------------------------------------------------------- local function BuildMainFrame() local f = CreateFrame("Frame", "SFramesMailFrame", UIParent) f:SetWidth(L.W); f:SetHeight(L.H) f:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 16, -104) f:SetFrameStrata("HIGH"); f:SetToplevel(true) f:EnableMouse(true); f:SetMovable(true); f:RegisterForDrag("LeftButton") f:SetScript("OnDragStart", function() this:StartMoving() end) f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) SetRoundBackdrop(f); CreateShadow(f) S.frame = f local font = GetFont() local header = CreateFrame("Frame", nil, f) header:SetPoint("TOPLEFT", 0, 0); header:SetPoint("TOPRIGHT", 0, 0); header:SetHeight(L.HEADER) header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" }) header:SetBackdropColor(T.headerBg[1], T.headerBg[2], T.headerBg[3], T.headerBg[4]) local titleIco = SFrames:CreateIcon(header, "mail", 16) titleIco:SetDrawLayer("OVERLAY") titleIco:SetPoint("LEFT", header, "LEFT", L.PAD, 0) titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3]) local titleFS = header:CreateFontString(nil, "OVERLAY") titleFS:SetFont(font, 14, "OUTLINE"); titleFS:SetPoint("LEFT", titleIco, "RIGHT", 5, 0) titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]); titleFS:SetText("邮箱") local closeBtn = CreateFrame("Button", nil, header) closeBtn:SetWidth(20); closeBtn:SetHeight(20); closeBtn:SetPoint("RIGHT", header, "RIGHT", -8, 0) local closeTex = closeBtn:CreateTexture(nil, "ARTWORK") closeTex:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\icon") closeTex:SetTexCoord(0.25, 0.375, 0, 0.125); closeTex:SetAllPoints() closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) closeBtn:SetScript("OnClick", function() S.frame:Hide() end) closeBtn:SetScript("OnEnter", function() closeTex:SetVertexColor(1, 0.6, 0.7) end) closeBtn:SetScript("OnLeave", function() closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) end) local sep = f:CreateTexture(nil, "ARTWORK") sep:SetTexture("Interface\\Buttons\\WHITE8X8"); sep:SetHeight(1) sep:SetPoint("TOPLEFT", f, "TOPLEFT", 6, -L.HEADER) sep:SetPoint("TOPRIGHT", f, "TOPRIGHT", -6, -L.HEADER) sep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) local tabInbox = CreateTabBtn(f, "收件箱", 70) tabInbox:SetPoint("TOPLEFT", f, "TOPLEFT", L.PAD, -(L.HEADER + 6)) tabInbox:SetScript("OnClick", function() S.currentTab = 1; ML:ShowInboxPanel() end) f.tabInbox = tabInbox local tabSend = CreateTabBtn(f, "发送", 70) tabSend:SetPoint("LEFT", tabInbox, "RIGHT", 4, 0) tabSend:SetScript("OnClick", function() S.currentTab = 2; ML:ShowSendPanel() end) f.tabSend = tabSend f:Hide() end -------------------------------------------------------------------------------- -- BUILD: Inbox panel -------------------------------------------------------------------------------- local function BuildInboxPanel() local f = S.frame local panelTop = L.HEADER + 6 + L.TAB_H + 4 local ip = CreateFrame("Frame", nil, f) ip:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -panelTop) ip:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0) f.inboxPanel = ip local font = GetFont() local selAll = CreateActionBtn(ip, "全选", 50) selAll:SetHeight(20); selAll:SetPoint("TOPLEFT", ip, "TOPLEFT", L.PAD, 0) selAll:SetScript("OnClick", function() SelectAllInbox() end) local desel = CreateActionBtn(ip, "取消全选", 66) desel:SetHeight(20); desel:SetPoint("LEFT", selAll, "RIGHT", 4, 0) desel:SetScript("OnClick", function() DeselectAllInbox() end) local pageFS = ip:CreateFontString(nil, "OVERLAY") pageFS:SetFont(font, 10, "OUTLINE"); pageFS:SetPoint("LEFT", desel, "RIGHT", 12, 0) pageFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) f.inboxPageText = pageFS for i = 1, L.ROWS do local row = CreateInboxRow(ip, i) row:SetPoint("TOPLEFT", ip, "TOPLEFT", L.PAD, -(26 + (i - 1) * L.ROW_H)) S.inboxRows[i] = row end local bsep = ip:CreateTexture(nil, "ARTWORK") bsep:SetTexture("Interface\\Buttons\\WHITE8X8"); bsep:SetHeight(1) bsep:SetPoint("BOTTOMLEFT", ip, "BOTTOMLEFT", 6, L.BOTTOM) bsep:SetPoint("BOTTOMRIGHT", ip, "BOTTOMRIGHT", -6, L.BOTTOM) bsep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) local prev = CreateActionBtn(ip, "<", 28) prev:SetHeight(22); prev:SetPoint("BOTTOMLEFT", ip, "BOTTOMLEFT", L.PAD, 12) prev:SetScript("OnClick", function() S.inboxPage = S.inboxPage - 1; UpdateInbox() end) f.inboxPrevBtn = prev local nxt = CreateActionBtn(ip, ">", 28) nxt:SetHeight(22); nxt:SetPoint("LEFT", prev, "RIGHT", 4, 0) nxt:SetScript("OnClick", function() S.inboxPage = S.inboxPage + 1; UpdateInbox() end) f.inboxNextBtn = nxt local colSel = CreateActionBtn(ip, "收取选中", 80) colSel:SetHeight(24); colSel:SetPoint("BOTTOMRIGHT", ip, "BOTTOMRIGHT", -L.PAD, 10) colSel:SetScript("OnClick", function() if S.isCollecting then StopCollecting() else CollectSelected() end end) f.collectSelectedBtn = colSel local colAll = CreateActionBtn(ip, "全部收取", 80) colAll:SetHeight(24); colAll:SetPoint("RIGHT", colSel, "LEFT", -6, 0) colAll:SetScript("OnClick", function() if S.isCollecting then StopCollecting() else CollectAll() end end) f.collectAllBtn = colAll end -------------------------------------------------------------------------------- -- BUILD: Mail detail panel (overlays inbox when a mail is clicked) -------------------------------------------------------------------------------- local function BuildDetailPanel() local f = S.frame local panelTop = L.HEADER + 6 + L.TAB_H + 4 local dp = CreateFrame("Frame", nil, f) dp:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -panelTop) dp:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0) dp:Hide() f.detailPanel = dp local font = GetFont() local pad = L.PAD local backBtn = CreateActionBtn(dp, "< 返回", 60) backBtn:SetHeight(20); backBtn:SetPoint("TOPLEFT", dp, "TOPLEFT", pad, 0) backBtn:SetScript("OnClick", function() ML:HideMailDetail() end) local senderFS = dp:CreateFontString(nil, "OVERLAY") senderFS:SetFont(font, 11, "OUTLINE"); senderFS:SetJustifyH("LEFT") senderFS:SetPoint("TOPLEFT", dp, "TOPLEFT", pad, -26) senderFS:SetWidth(L.W - pad * 2) senderFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) dp.senderFS = senderFS local subjectFS = dp:CreateFontString(nil, "OVERLAY") subjectFS:SetFont(font, 11, "OUTLINE"); subjectFS:SetJustifyH("LEFT") subjectFS:SetPoint("TOPLEFT", senderFS, "BOTTOMLEFT", 0, -4) subjectFS:SetWidth(L.W - pad * 2) subjectFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) dp.subjectFS = subjectFS local sep1 = dp:CreateTexture(nil, "ARTWORK") sep1:SetTexture("Interface\\Buttons\\WHITE8X8"); sep1:SetHeight(1) sep1:SetPoint("TOPLEFT", subjectFS, "BOTTOMLEFT", 0, -6) sep1:SetPoint("RIGHT", dp, "RIGHT", -pad, 0) sep1:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) -- Body scroll frame local bsf = CreateFrame("ScrollFrame", "SFramesMailDetailScroll", dp, "UIPanelScrollFrameTemplate") bsf:SetPoint("TOPLEFT", sep1, "BOTTOMLEFT", 0, -4) bsf:SetWidth(L.W - pad * 2 - 24); bsf:SetHeight(180) local bodyFS = CreateFrame("Frame", nil, bsf) bodyFS:SetWidth(L.W - pad * 2 - 36); bodyFS:SetHeight(400) local bodyText = bodyFS:CreateFontString(nil, "OVERLAY") bodyText:SetFont(font, 11, "OUTLINE") bodyText:SetPoint("TOPLEFT", 0, 0) bodyText:SetWidth(L.W - pad * 2 - 36); bodyText:SetJustifyH("LEFT"); bodyText:SetJustifyV("TOP") bodyText:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) bsf:SetScrollChild(bodyFS) dp.bodyText = bodyText dp.bodyFrame = bodyFS local sep2 = dp:CreateTexture(nil, "ARTWORK") sep2:SetTexture("Interface\\Buttons\\WHITE8X8"); sep2:SetHeight(1) sep2:SetPoint("TOPLEFT", bsf, "BOTTOMLEFT", 0, -4) sep2:SetPoint("RIGHT", dp, "RIGHT", -pad, 0) sep2:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) -- Item + money info line local infoFS = dp:CreateFontString(nil, "OVERLAY") infoFS:SetFont(font, 10, "OUTLINE"); infoFS:SetJustifyH("LEFT") infoFS:SetPoint("TOPLEFT", sep2, "BOTTOMLEFT", 0, -6) infoFS:SetWidth(L.W - pad * 2) infoFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) dp.infoFS = infoFS local moneyLabel = dp:CreateFontString(nil, "OVERLAY") moneyLabel:SetFont(font, 10, "OUTLINE"); moneyLabel:SetTextColor(1, 0.84, 0) moneyLabel:SetPoint("TOPLEFT", infoFS, "BOTTOMLEFT", 0, -3) moneyLabel:Hide() dp.detailMoneyLabel = moneyLabel local detailMoney = CreateMoneyIcons(dp, 10, 11) detailMoney:SetPoint("LEFT", moneyLabel, "RIGHT", 4, 0) detailMoney:Hide() dp.detailMoney = detailMoney local codLabel = dp:CreateFontString(nil, "OVERLAY") codLabel:SetFont(font, 10, "OUTLINE"); codLabel:SetTextColor(1, 0.33, 0.33) codLabel:SetPoint("LEFT", detailMoney, "RIGHT", 8, 0) codLabel:Hide() dp.detailCodLabel = codLabel local detailCod = CreateMoneyIcons(dp, 10, 11) detailCod:SetPoint("LEFT", codLabel, "RIGHT", 4, 0) detailCod:Hide() dp.detailCod = detailCod -- Action buttons at bottom local takeItemBtn = CreateActionBtn(dp, "收取物品", 72) takeItemBtn:SetHeight(24); takeItemBtn:SetPoint("BOTTOMLEFT", dp, "BOTTOMLEFT", pad, 10) dp.takeItemBtn = takeItemBtn local takeMoneyBtn = CreateActionBtn(dp, "收取金币", 72) takeMoneyBtn:SetHeight(24); takeMoneyBtn:SetPoint("LEFT", takeItemBtn, "RIGHT", 4, 0) dp.takeMoneyBtn = takeMoneyBtn local returnBtn = CreateActionBtn(dp, "退回", 50) returnBtn:SetHeight(24); returnBtn:SetPoint("LEFT", takeMoneyBtn, "RIGHT", 4, 0) dp.returnBtn = returnBtn local deleteBtn = CreateActionBtn(dp, "删除", 50) deleteBtn:SetHeight(24); deleteBtn:SetPoint("LEFT", returnBtn, "RIGHT", 4, 0) dp.deleteBtn = deleteBtn local replyBtn = CreateActionBtn(dp, "回复", 50) replyBtn:SetHeight(24); replyBtn:SetPoint("BOTTOMRIGHT", dp, "BOTTOMRIGHT", -pad, 10) dp.replyBtn = replyBtn end -------------------------------------------------------------------------------- -- Show / Hide mail detail -------------------------------------------------------------------------------- function ML:ShowMailDetail(mailIndex) if not S.frame or not S.frame.detailPanel then return end local dp = S.frame.detailPanel S.detailMailIndex = mailIndex local _, _, sender, subject, money, CODAmount, daysLeft, hasItem, wasRead = GetInboxHeaderInfo(mailIndex) local bodyText = GetInboxText(mailIndex) dp.senderFS:SetText("发件人: |cFFFFFFFF" .. (sender or "未知") .. "|r " .. FormatExpiry(daysLeft)) dp.subjectFS:SetText("主题: |cFFFFFFFF" .. (subject or "(无主题)") .. "|r") dp.bodyText:SetText(bodyText or "(无正文)") local ok, textH = pcall(function() return dp.bodyText:GetStringHeight() end) if not ok or not textH then textH = 40 end dp.bodyFrame:SetHeight(math.max(textH + 10, 40)) local info = "" if hasItem then local itemName, itemTex = GetInboxItem(mailIndex) if itemName then info = info .. "|cFFFFD700附件:|r " .. itemName end end if info == "" and (not money or money <= 0) and (not CODAmount or CODAmount <= 0) then info = "无附件,无金币" end dp.infoFS:SetText(info) dp.detailMoneyLabel:Hide(); dp.detailMoney:Hide() dp.detailCodLabel:Hide(); dp.detailCod:Hide() if money and money > 0 then dp.detailMoneyLabel:SetText("金额:"); dp.detailMoneyLabel:Show() dp.detailMoney:SetMoney(money); dp.detailMoney:Show() end if CODAmount and CODAmount > 0 then dp.detailCodLabel:SetText("COD:"); dp.detailCodLabel:Show() dp.detailCod:SetMoney(CODAmount); dp.detailCod:Show() end -- Take items button local canTakeItem = hasItem and (not CODAmount or CODAmount == 0) dp.takeItemBtn:SetDisabled(not canTakeItem) dp.takeItemBtn:SetScript("OnClick", function() if S.detailMailIndex then TakeInboxItem(S.detailMailIndex) end end) -- Take money button local canTakeMoney = money and money > 0 and (not CODAmount or CODAmount == 0) dp.takeMoneyBtn:SetDisabled(not canTakeMoney) dp.takeMoneyBtn:SetScript("OnClick", function() if S.detailMailIndex then local idx = S.detailMailIndex local _, _, _, _, _, _, _, hi = GetInboxHeaderInfo(idx) TakeInboxMoney(idx) if not hi then if not S.deleteTimer then S.deleteTimer = CreateFrame("Frame") end S.deleteElapsed = 0 S.deleteTimer:SetScript("OnUpdate", function() S.deleteElapsed = S.deleteElapsed + arg1 if S.deleteElapsed >= 0.5 then this:SetScript("OnUpdate", nil) if idx <= GetInboxNumItems() then DeleteInboxItem(idx) end end end) end ML:HideMailDetail() end end) -- Return button dp.returnBtn:SetScript("OnClick", function() if S.detailMailIndex then ReturnInboxItem(S.detailMailIndex) ML:HideMailDetail() end end) -- Delete button dp.deleteBtn:SetScript("OnClick", function() if S.detailMailIndex then DeleteInboxItem(S.detailMailIndex) ML:HideMailDetail() end end) -- Reply button dp.replyBtn:SetScript("OnClick", function() if sender then S.currentTab = 2; ML:ShowSendPanel() if S.frame.toEditBox then S.frame.toEditBox:SetText(sender) end if S.frame.subjectEditBox then local re = subject or "" if string.sub(re, 1, 4) ~= "Re: " then re = "Re: " .. re end S.frame.subjectEditBox:SetText(re) end end end) S.frame.inboxPanel:Hide() dp:Show() end function ML:HideMailDetail() if not S.frame then return end S.detailMailIndex = nil if S.frame.detailPanel then S.frame.detailPanel:Hide() end S.frame.inboxPanel:Show() UpdateInbox() end -------------------------------------------------------------------------------- -- BUILD: Send panel -------------------------------------------------------------------------------- local function BuildSendPanel() local f = S.frame local panelTop = L.HEADER + 6 + L.TAB_H + 4 local sp = CreateFrame("Frame", "SFramesMailSendPanel", f) sp:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -panelTop) sp:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0) sp:EnableMouse(true) sp:SetScript("OnReceiveDrag", function() AcceptCursorItem() end) sp:SetScript("OnMouseUp", function() if CursorHasItem() then AcceptCursorItem() end end) sp:Hide() f.sendPanel = sp local font = GetFont() -- Recipient local labelW = 50 local ebW = L.W - L.PAD * 2 - labelW - 6 local toLabel = sp:CreateFontString(nil, "OVERLAY") toLabel:SetFont(font, 11, "OUTLINE"); toLabel:SetPoint("TOPLEFT", sp, "TOPLEFT", L.PAD, -6) toLabel:SetWidth(labelW); toLabel:SetJustifyH("RIGHT") toLabel:SetText("收件人:"); toLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3]) local toEB = CreateStyledEditBox(sp, ebW, 22) toEB:SetPoint("LEFT", toLabel, "RIGHT", 6, 0) f.toEditBox = toEB -- Autocomplete dropdown for recipient local AC_MAX = 8 local acBox = CreateFrame("Frame", "SFramesMailAutoComplete", f) acBox:SetWidth(ebW); acBox:SetFrameStrata("TOOLTIP") acBox:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 }, }) acBox:SetBackdropColor(0.05, 0.05, 0.1, 0.95) acBox:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.9) acBox:SetPoint("TOPLEFT", toEB, "BOTTOMLEFT", 0, -2) acBox:Hide() acBox:EnableMouse(true) f.acBox = acBox local acButtons = {} for ai = 1, AC_MAX do local btn = CreateFrame("Button", nil, acBox) btn:SetHeight(18); btn:SetPoint("TOPLEFT", acBox, "TOPLEFT", 4, -4 - (ai - 1) * 18) btn:SetPoint("RIGHT", acBox, "RIGHT", -4, 0) btn:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") local bfs = btn:CreateFontString(nil, "OVERLAY") bfs:SetFont(font, 11, "OUTLINE"); bfs:SetAllPoints(btn) bfs:SetJustifyH("LEFT") btn.label = bfs btn.sourceName = nil btn:SetScript("OnClick", function() if btn.sourceName then toEB:SetText(btn.sourceName) end acBox:Hide() f.subjectEditBox:SetFocus() end) btn:Hide() acButtons[ai] = btn end f.acButtons = acButtons local friendCache = {} local function BuildFriendCache() friendCache = {} for fi = 1, GetNumFriends() do local name = GetFriendInfo(fi) if name then friendCache[name] = true end end end local function ShowSuggestions() local input = toEB:GetText() if not input or input == "" then acBox:Hide(); return end local upper = string.upper(input) BuildFriendCache() local seen = { [UnitName("player")] = true } local results = {} local function addName(name, source) if not name or name == "" or seen[name] then return end if string.find(string.upper(name), upper, 1, true) == 1 then table.insert(results, { name = name, source = source }) end seen[name] = true end for fi = 1, GetNumFriends() do local name = GetFriendInfo(fi) addName(name, "friend") end if GetNumGuildMembers then for gi = 1, GetNumGuildMembers(true) do local name = GetGuildRosterInfo(gi) addName(name, "guild") end end local count = math.min(table.getn(results), AC_MAX) if count == 0 then acBox:Hide(); return end if count == 1 and results[1].name == input then acBox:Hide(); return end for ai = 1, AC_MAX do local btn = acButtons[ai] if ai <= count then local r = results[ai] local col = T.whoColor local tag = "" if r.source == "friend" then col = T.friendColor; tag = " |cFF66FF88[好友]|r" elseif r.source == "guild" then col = T.guildColor; tag = " |cFF66CCFF[公会]|r" end btn.label:SetText("|cFF" .. string.format("%02x%02x%02x", col[1] * 255, col[2] * 255, col[3] * 255) .. r.name .. "|r" .. tag) btn.sourceName = r.name btn:Show() else btn:Hide() end end acBox:SetHeight(count * 18 + 8) acBox:Show() end toEB:SetScript("OnTextChanged", function() ShowSuggestions() end) toEB:SetScript("OnEscapePressed", function() acBox:Hide(); this:ClearFocus() end) toEB:SetScript("OnEnterPressed", function() acBox:Hide(); f.subjectEditBox:SetFocus() end) -- Subject (second line, aligned with recipient) local subLabel = sp:CreateFontString(nil, "OVERLAY") subLabel:SetFont(font, 11, "OUTLINE"); subLabel:SetPoint("TOPLEFT", sp, "TOPLEFT", L.PAD, -32) subLabel:SetWidth(labelW); subLabel:SetJustifyH("RIGHT") subLabel:SetText("主题:"); subLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3]) local subEB = CreateStyledEditBox(sp, ebW, 22) subEB:SetPoint("LEFT", subLabel, "RIGHT", 6, 0) f.subjectEditBox = subEB toEB:SetScript("OnTabPressed", function() acBox:Hide(); f.subjectEditBox:SetFocus() end) subEB:SetScript("OnTabPressed", function() f.toEditBox:SetFocus() end) -- Body local bodyLabel = sp:CreateFontString(nil, "OVERLAY") bodyLabel:SetFont(font, 11, "OUTLINE"); bodyLabel:SetPoint("TOPLEFT", sp, "TOPLEFT", L.PAD, -58) bodyLabel:SetText("正文:"); bodyLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3]) local bsf = CreateFrame("ScrollFrame", "SFramesMailBodyScroll", sp, "UIPanelScrollFrameTemplate") bsf:SetPoint("TOPLEFT", bodyLabel, "BOTTOMLEFT", 0, -4) bsf:SetWidth(L.W - L.PAD * 2 - 28); bsf:SetHeight(100) bsf:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 }, }) bsf:SetBackdropColor(T.inputBg[1], T.inputBg[2], T.inputBg[3], T.inputBg[4] or 0.95) bsf:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], 0.8) local bodyEB = CreateFrame("EditBox", "SFramesMailBodyEditBox", bsf) bodyEB:SetWidth(L.W - L.PAD * 2 - 40); bodyEB:SetHeight(200) bodyEB:SetFont(font, 11, "OUTLINE"); bodyEB:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) bodyEB:SetAutoFocus(false); bodyEB:SetMultiLine(true); bodyEB:SetMaxLetters(500) bodyEB:SetTextInsets(6, 6, 4, 4) bsf:SetScrollChild(bodyEB) f.bodyEditBox = bodyEB -- Money row local mLabel = sp:CreateFontString(nil, "OVERLAY") mLabel:SetFont(font, 11, "OUTLINE"); mLabel:SetPoint("TOPLEFT", bsf, "BOTTOMLEFT", 0, -10) mLabel:SetText("附加金币:"); mLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3]) local gL = sp:CreateFontString(nil, "OVERLAY") gL:SetFont(font, 10, "OUTLINE"); gL:SetPoint("LEFT", mLabel, "RIGHT", 6, 0) gL:SetText("金"); gL:SetTextColor(T.moneyGold[1], T.moneyGold[2], T.moneyGold[3]) local gEB = CreateStyledEditBox(sp, 60, 20, true); gEB:SetPoint("LEFT", gL, "RIGHT", 4, 0); gEB:SetText("0"); f.goldEB = gEB local sL = sp:CreateFontString(nil, "OVERLAY") sL:SetFont(font, 10, "OUTLINE"); sL:SetPoint("LEFT", gEB, "RIGHT", 6, 0) sL:SetText("银"); sL:SetTextColor(T.moneySilver[1], T.moneySilver[2], T.moneySilver[3]) local sEB = CreateStyledEditBox(sp, 40, 20, true); sEB:SetPoint("LEFT", sL, "RIGHT", 4, 0); sEB:SetText("0"); f.silverEB = sEB local cL = sp:CreateFontString(nil, "OVERLAY") cL:SetFont(font, 10, "OUTLINE"); cL:SetPoint("LEFT", sEB, "RIGHT", 6, 0) cL:SetText("铜"); cL:SetTextColor(T.moneyCopper[1], T.moneyCopper[2], T.moneyCopper[3]) local cEB = CreateStyledEditBox(sp, 40, 20, true); cEB:SetPoint("LEFT", cL, "RIGHT", 4, 0); cEB:SetText("0"); f.copperEB = cEB -- Attachments local aLabel = sp:CreateFontString(nil, "OVERLAY") aLabel:SetFont(font, 11, "OUTLINE"); aLabel:SetPoint("TOPLEFT", mLabel, "TOPLEFT", 0, -28) aLabel:SetText("附件 (右击/拖放背包物品添加):"); aLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3]) local clrBtn = CreateActionBtn(sp, "清空", 50) clrBtn:SetHeight(20); clrBtn:SetPoint("LEFT", aLabel, "RIGHT", 8, 0) clrBtn:SetScript("OnClick", function() ClearSendItems(); ML:UpdateSendPanel() end) -- Item slot grid f.sendItemSlots = {} BuildSendSlots(sp, aLabel, f) -- Send button & status local status = sp:CreateFontString(nil, "OVERLAY") status:SetFont(font, 11, "OUTLINE"); status:SetPoint("BOTTOMLEFT", sp, "BOTTOMLEFT", L.PAD, 14) status:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) f.sendStatus = status local sendBtn = CreateActionBtn(sp, "发送", 80) sendBtn:SetHeight(26); sendBtn:SetPoint("BOTTOMRIGHT", sp, "BOTTOMRIGHT", -L.PAD, 10) sendBtn:SetScript("OnClick", function() local r = f.toEditBox:GetText() local sub = f.subjectEditBox:GetText() local bd = f.bodyEditBox:GetText() local g = tonumber(f.goldEB:GetText()) or 0 local sv = tonumber(f.silverEB:GetText()) or 0 local c = tonumber(f.copperEB:GetText()) or 0 DoMultiSend(r, sub, bd, g * 10000 + sv * 100 + c) end) f.sendBtn = sendBtn end -------------------------------------------------------------------------------- -- BUILD: Send item slots (separate function to stay under upvalue limit) -------------------------------------------------------------------------------- function BuildSendSlots(parent, anchor, f) local slotSize, slotGap, perRow = 34, 4, 6 for i = 1, L.MAX_SEND do local row = math.floor((i - 1) / perRow) local col = math.mod((i - 1), perRow) local sf = CreateFrame("Button", "SFramesMailSendSlot" .. i, parent) sf:SetWidth(slotSize); sf:SetHeight(slotSize) sf:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", col * (slotSize + slotGap), -(4 + row * (slotSize + slotGap))) sf:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 }, }) sf:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) sf:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) local ico = sf:CreateTexture(nil, "ARTWORK") ico:SetTexCoord(0.08, 0.92, 0.08, 0.92) ico:SetPoint("TOPLEFT", 3, -3); ico:SetPoint("BOTTOMRIGHT", -3, 3); ico:Hide() sf.icon = ico local cnt = sf:CreateFontString(nil, "OVERLAY") cnt:SetFont("Fonts\\ARIALN.TTF", 11, "OUTLINE") cnt:SetPoint("BOTTOMRIGHT", sf, "BOTTOMRIGHT", -2, 2); cnt:SetJustifyH("RIGHT") sf.countFS = cnt local rb = CreateFrame("Button", nil, sf) rb:SetWidth(14); rb:SetHeight(14); rb:SetPoint("TOPRIGHT", sf, "TOPRIGHT", 2, 2) local rtx = rb:CreateTexture(nil, "OVERLAY") rtx:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\icon") rtx:SetTexCoord(0.25, 0.375, 0, 0.125); rtx:SetAllPoints(); rtx:SetVertexColor(1, 0.4, 0.4) rb:Hide(); sf.removeBtn = rb; sf.hasItem = false local si = i rb:SetScript("OnClick", function() RemoveSendItem(si); ML:UpdateSendPanel() end) sf:SetScript("OnReceiveDrag", function() AcceptCursorItem() end) sf:SetScript("OnClick", function() if CursorHasItem() then AcceptCursorItem(); return end if arg1 == "RightButton" and sf.hasItem then RemoveSendItem(si); ML:UpdateSendPanel() end end) sf:RegisterForClicks("LeftButtonUp", "RightButtonUp") sf:SetScript("OnEnter", function() this:SetBackdropBorderColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4]) local e = S.sendQueue[si] if e and e.link then GameTooltip:SetOwner(this, "ANCHOR_RIGHT"); pcall(GameTooltip.SetHyperlink, GameTooltip, e.link); GameTooltip:Show() end end) sf:SetScript("OnLeave", function() this:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) GameTooltip:Hide() end) f.sendItemSlots[i] = sf end end -------------------------------------------------------------------------------- -- BUILD: Events & hooks -------------------------------------------------------------------------------- local function SetupEvents() local f = S.frame f:SetScript("OnHide", function() StopCollecting(); if S.multiSend then AbortMultiSend() end; pcall(CloseMail) end) f:SetScript("OnEvent", function() if event == "MAIL_SHOW" then if SFramesDB and SFramesDB.enableMail == false then return end S.currentTab = 1; S.inboxPage = 1; S.inboxChecked = {} CheckInbox(); f:Show(); ML:ShowInboxPanel() elseif event == "MAIL_INBOX_UPDATE" then if f:IsVisible() then if S.detailMailIndex and f.detailPanel and f.detailPanel:IsVisible() then if S.detailMailIndex <= GetInboxNumItems() then ML:ShowMailDetail(S.detailMailIndex) else ML:HideMailDetail() end else UpdateInbox() end end elseif event == "MAIL_CLOSED" then if S.multiSend then AbortMultiSend("邮箱已关闭") end f:Hide() elseif event == "MAIL_SEND_SUCCESS" then if S.multiSend then S.multiSend.sendOk = true S.multiSend.phase = "cooldown" S.multiSend.elapsed = 0 else FlashStatus("发送成功!", T.successText, 3) ResetSendForm() if f.sendBtn then f.sendBtn.label:SetText("发送"); f.sendBtn:SetDisabled(false) end end elseif event == "MAIL_SEND_INFO_UPDATE" then -- noop elseif event == "MAIL_FAILED" then if S.multiSend and S.multiSend.phase == "wait_send" and not S.multiSend.sendOk then AbortMultiSend("发送失败") elseif not S.multiSend then if f.sendBtn then f.sendBtn.label:SetText("发送"); f.sendBtn:SetDisabled(false) end FlashStatus("发送失败!", T.errorText, 5) end end end) f:RegisterEvent("MAIL_SHOW"); f:RegisterEvent("MAIL_INBOX_UPDATE") f:RegisterEvent("MAIL_CLOSED"); f:RegisterEvent("MAIL_SEND_SUCCESS") f:RegisterEvent("MAIL_SEND_INFO_UPDATE"); f:RegisterEvent("MAIL_FAILED") if MailFrame then local origMailOnShow = MailFrame:GetScript("OnShow") MailFrame:SetScript("OnShow", function() if origMailOnShow then origMailOnShow() end this:ClearAllPoints() this:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -10000, 10000) this:EnableMouse(false) end) for i = table.getn(UISpecialFrames), 1, -1 do if UISpecialFrames[i] == "MailFrame" then table.remove(UISpecialFrames, i) end end end if OpenMailFrame then OpenMailFrame:UnregisterAllEvents(); OpenMailFrame:Hide() end tinsert(UISpecialFrames, "SFramesMailFrame") SFrames.Mail.TryAddItemFromBag = function(bag, slot) if S.frame and S.frame:IsVisible() and not S.isSending then if bag and slot then if S.currentTab ~= 2 then S.currentTab = 2; ML:ShowSendPanel() end AddSendItem(bag, slot); ML:UpdateSendPanel() return true end end return false end if ContainerFrameItemButton_OnClick then local orig = ContainerFrameItemButton_OnClick ContainerFrameItemButton_OnClick = function(button, ignoreShift) if arg1 == "RightButton" then if SFrames.Mail.TryAddItemFromBag(this:GetParent():GetID(), this:GetID()) then return end end orig(button, ignoreShift) end end end -------------------------------------------------------------------------------- -- Initialize (calls sub-builders) -------------------------------------------------------------------------------- function ML:Initialize() if S.frame then return end BuildMainFrame() BuildInboxPanel() BuildDetailPanel() BuildSendPanel() SetupEvents() end -------------------------------------------------------------------------------- -- Panel Switching -------------------------------------------------------------------------------- function ML:ShowInboxPanel() if not S.frame then return end S.frame.tabInbox:SetActive(true); S.frame.tabSend:SetActive(false) if S.frame.detailPanel then S.frame.detailPanel:Hide() end S.frame.inboxPanel:Show(); S.frame.sendPanel:Hide() S.detailMailIndex = nil UpdateInbox() end function ML:ShowSendPanel() if not S.frame then return end S.frame.tabInbox:SetActive(false); S.frame.tabSend:SetActive(true) if S.frame.detailPanel then S.frame.detailPanel:Hide() end S.detailMailIndex = nil S.frame.inboxPanel:Hide(); S.frame.sendPanel:Show() if S.frame.sendStatus then S.frame.sendStatus:SetText("") end if S.statusFadeTimer then S.statusFadeTimer:SetScript("OnUpdate", nil) end ML:UpdateSendPanel() end -------------------------------------------------------------------------------- -- Bootstrap -------------------------------------------------------------------------------- local bootstrap = CreateFrame("Frame") bootstrap:RegisterEvent("PLAYER_LOGIN") bootstrap:SetScript("OnEvent", function() if event == "PLAYER_LOGIN" then if SFramesDB.enableMail == nil then SFramesDB.enableMail = true end if SFramesDB.enableMail ~= false then ML:Initialize() end end end)