diff --git a/Core/Utils.lua b/Core/Utils.lua index 4997fe8..05cd468 100644 --- a/Core/Utils.lua +++ b/Core/Utils.lua @@ -95,6 +95,31 @@ function Utils:GetItemInfo(itemLink) return nil end +-- Create a hidden tooltip for scanning +local scanTooltip = CreateFrame("GameTooltip", "GudaBagScanTooltip", nil, "GameTooltipTemplate") +scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") + +-- Get item link from mailbox attachment (WoW 1.12.1 workaround) +function Utils:GetInboxItemLink(index, itemIndex) + -- In 1.12.1, GameTooltip:GetHyperlink() does not exist. + -- Actually, in 1.12.1, there's no official API to get the link from a mailbox item + -- if GetInboxItemLink(index) is not available (it's often NOT available in Vanilla). + + -- Let's try to use GetItemInfo(name) as the primary way. + local name = GetInboxItem(index, itemIndex) + if name then + local _, link = GetItemInfo(name) + if link then + return link + end + end + + -- Fallback: If we still don't have it, we might try to scan the tooltip for the name + -- but GetInboxItem already gave us the name. + + return nil +end + -- Get quality color function Utils:GetQualityColor(quality) local color = addon.Constants.QUALITY_COLORS[quality] or addon.Constants.QUALITY_COLORS[1] @@ -156,12 +181,7 @@ function Utils:FormatTimeAgo(timestamp) end -- Create hidden tooltip for scanning (only once) -local scanTooltip = nil local function GetScanTooltip() - if not scanTooltip then - scanTooltip = CreateFrame("GameTooltip", "GudaBagScanTooltip", nil, "GameTooltipTemplate") - scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") - end return scanTooltip end diff --git a/Data/MailboxScanner.lua b/Data/MailboxScanner.lua index 1c484fe..0c2e982 100644 --- a/Data/MailboxScanner.lua +++ b/Data/MailboxScanner.lua @@ -27,42 +27,40 @@ end -- Scan a single mail item function MailboxScanner:ScanMailItem(index) - -- GetInboxItem(index) returns: packageIcon, stationeryIcon, sender, subject, money, CODAmount, daysLeft, hasItem, wasRead, wasReturned, textCreated, canReply, isGM - local packageIcon, stationeryIcon, sender, subject, money, CODAmount, daysLeft, hasItem, wasRead, wasReturned, textCreated, canReply, isGM = GetInboxItem(index) - - local itemData = nil + -- GetInboxHeaderInfo(index) returns: packageIcon, stationeryIcon, sender, subject, money, CODAmount, daysLeft, hasItem, wasRead, wasReturned, textCreated, canReply, isGM + local packageIcon, stationeryIcon, sender, subject, money, CODAmount, daysLeft, hasItem, wasRead, wasReturned, textCreated, canReply, isGM = GetInboxHeaderInfo(index) + + local items = {} if hasItem then - addon:Debug("Scanning mail item %d", index) - -- In 1.12.1, GetInboxItemLink(index) returns the link - local itemLink = GetInboxItemLink(index) - - -- GetInboxItemInfo(index) returns: name, texture, count, quality, canUse - local infoName, infoTexture, infoCount, infoQuality, infoCanUse = GetInboxItemInfo(index) - - addon:Debug("Mail item %d: infoName=%s, infoTexture=%s", index, tostring(infoName), tostring(infoTexture)) + -- Turtle WoW supports up to 12 attachments per mail + -- GetInboxItem(index, itemIndex) returns: name, texture, count, quality, canUse + local name, texture, count, quality, canUse = GetInboxItem(index, itemIndex) + if name then + local itemLink = addon.Modules.Utils:GetInboxItemLink(index, itemIndex) - -- Even if link is missing (not cached), we can store what we have from GetInboxItemInfo - itemData = { - link = itemLink, - texture = infoTexture or packageIcon or "Interface\\Icons\\INV_Misc_Bag_08", - count = infoCount or 1, - quality = infoQuality or 0, - name = infoName or subject or "Unknown Item", - } + local itemData = { + link = itemLink, + texture = texture or "Interface\\Icons\\INV_Misc_Bag_08", + count = count or 1, + quality = quality or 0, + name = name, + } - -- If we have a link, try to get more detailed info - if itemLink then - local itemName, link, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(itemLink) - if itemName then - itemData.name = itemName - itemData.quality = itemQuality or itemData.quality - itemData.iLevel = iLevel - itemData.type = itemType - itemData.class = itemCategory - itemData.subclass = itemSubType - itemData.equipSlot = itemEquipLoc - if itemTexture then itemData.texture = itemTexture end - end + -- If we have a link, try to get more detailed info + if itemLink then + local itemName, link, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(itemLink) + if itemName then + itemData.name = itemName + itemData.quality = itemQuality or itemData.quality + itemData.iLevel = iLevel + itemData.type = itemType + itemData.class = itemCategory + itemData.subclass = itemSubType + itemData.equipSlot = itemEquipLoc + if itemTexture then itemData.texture = itemTexture end + end + end + table.insert(items, itemData) end end @@ -72,8 +70,9 @@ function MailboxScanner:ScanMailItem(index) money = money, CODAmount = CODAmount, daysLeft = daysLeft, - hasItem = hasItem, - item = itemData, + hasItem = (table.getn(items) > 0), + item = items[1], + items = items, wasRead = wasRead, packageIcon = packageIcon, } @@ -111,6 +110,17 @@ function MailboxScanner:Initialize() end) end, "MailboxScanner") + -- Register for GET_ITEM_INFO_RECEIVED to refresh if item data arrives + addon.Modules.Events:Register("GET_ITEM_INFO_RECEIVED", function() + if mailboxOpen then + addon:Debug("GET_ITEM_INFO_RECEIVED: Refreshing mailbox") + MailboxScanner:SaveToDatabase() + end + end, "MailboxScanner") + + -- Register for UI_ERROR_MESSAGE to handle "item not found" situations if needed + -- (Some items might not be in cache and fail silently otherwise) + -- Mailbox closed addon.Modules.Events:OnMailClosed(function() -- Final save on close diff --git a/UI/ItemButton.lua b/UI/ItemButton.lua index fd20345..0f44585 100644 --- a/UI/ItemButton.lua +++ b/UI/ItemButton.lua @@ -11,6 +11,8 @@ scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") -- Check if an item is a quest item by scanning its tooltip -- Check if an item is a quest item by scanning its tooltip and determine type local function IsQuestItem(bagID, slotID, isBank) + bagID = tonumber(bagID) + slotID = tonumber(slotID) if not bagID or not slotID then return false end scanTooltip:ClearLines() @@ -117,6 +119,8 @@ end -- Scan tooltip for red text that is NOT a durability line local function IsItemUnusable(bagID, slotID, isBank) + bagID = tonumber(bagID) + slotID = tonumber(slotID) if not bagID or not slotID then return false end -- Some clients require SetOwner before every SetBagItem/SetInventoryItem to populate lines @@ -425,6 +429,10 @@ end -- Set item data function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCharName, matchesFilter, isReadOnly) + -- Proactively convert to number to avoid comparisons with strings in downstream functions + bagID = tonumber(bagID) + slotID = tonumber(slotID) + -- Proactively clear any previous cooldown overlay state before reassigning this pooled button do local cd = getglobal(self:GetName().."Cooldown") or self.cooldown @@ -459,6 +467,8 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha self.otherChar = otherCharName self.isReadOnly = isReadOnly or false -- Track if this is read-only mode self.isMail = false -- Clear mailbox flag by default + self.mailIndex = nil + self.mailItemIndex = nil -- Re-register for drag/drop every time (crucial for button reuse in Classic/Vanilla) if not self.isReadOnly and not self.otherChar then @@ -486,6 +496,8 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha matchesFilter = true end + self.mailData = nil -- Clear mail metadata by default + -- Use Blizzard's default count fontstring (ContainerFrameItemButtonTemplate creates $parentCount) local countText = getglobal(self:GetName().."Count") local emptySlotBg = getglobal(self:GetName().."_EmptySlotBg") @@ -947,17 +959,7 @@ end -- OnEnter handler (show tooltip) function Guda_ItemButton_OnEnter(self) - -- DETACH from Blizzard's internal tooltip logic for mailbox items - if self.isMail or this.isMail or (this:GetParent() and this:GetParent():GetName() == "Guda_MailboxFrame_ItemContainer") then - return - end - - -- Prevent Blizzard's ContainerFrameItemButton_OnEnter from running if it somehow got through - if not self.bagID or self.bagID == -100 then - return - end - --- Highlight the corresponding bag button in the footer (works for empty and filled slots) + -- Highlight the corresponding bag button in the footer (works for empty and filled slots) if not self.otherChar and self.bagID then if self.isBank then -- Bank item - highlight bank bag button @@ -969,7 +971,7 @@ function Guda_ItemButton_OnEnter(self) end -- Early return for empty slots (no tooltip needed) - if not self.hasItem then + if not self.hasItem and not self.isMail then return end @@ -977,7 +979,50 @@ function Guda_ItemButton_OnEnter(self) -- important position of tooltip GameTooltip:SetPoint("BOTTOMRIGHT", self, "TOPLEFT", 10, 0) - if self.otherChar or self.isReadOnly then + -- Mailbox tooltip handling + if self.isMail then + local currentPlayerName = addon.Modules.DB:GetPlayerFullName() + if (not self.otherChar or self.otherChar == currentPlayerName) and self.mailIndex then + -- Live mailbox for current character + GameTooltip:SetInboxItem(self.mailIndex, self.mailItemIndex or 1) + elseif self.itemData and self.itemData.link then + -- Read-only / other character mailbox + GameTooltip.GudaViewedCharacter = self.otherChar + GameTooltip:SetHyperlink(self.itemData.link) + elseif self.itemData and self.itemData.name then + -- Money or generic mail + GameTooltip:AddLine(self.itemData.name, 1, 1, 1) + end + + -- Add mailbox metadata if available + local mailData = self.mailData + if mailData then + -- If we already added a line (for money/generic), or SetHyperlink/SetInboxItem added lines, + -- we might want a separator if we're adding sender info. + if GameTooltip:NumLines() > 0 then + GameTooltip:AddLine(" ") + end + + if mailData.sender then + GameTooltip:AddLine("From: " .. mailData.sender, 1, 1, 1) + end + if mailData.subject then + GameTooltip:AddLine("Subject: " .. mailData.subject, 1, 1, 0.8) + end + if (mailData.money or 0) > 0 and not (self.itemData and self.itemData.name == "Money") then + GameTooltip:AddLine("Money: " .. addon.Modules.Utils:FormatMoney(mailData.money), 1, 1, 1) + end + if (mailData.CODAmount or 0) > 0 then + GameTooltip:AddLine("COD: " .. addon.Modules.Utils:FormatMoney(mailData.CODAmount), 1, 0, 0) + end + if mailData.daysLeft then + GameTooltip:AddLine("Days left: " .. math.floor(mailData.daysLeft), 0.5, 0.5, 0.5) + end + end + + GameTooltip:Show() + return + elseif self.otherChar or self.isReadOnly then GameTooltip.GudaViewedCharacter = self.otherChar if self.itemData and self.itemData.link then GameTooltip:SetHyperlink(self.itemData.link) diff --git a/UI/MailboxFrame.lua b/UI/MailboxFrame.lua index f55cf51..d13ab5b 100644 --- a/UI/MailboxFrame.lua +++ b/UI/MailboxFrame.lua @@ -10,6 +10,9 @@ local currentViewChar = nil local searchText = "" local isReadOnlyMode = true -- Mailbox is always read-only in this addon (viewing offline data) local itemButtons = {} +local mailboxRows = {} +local currentPage = 1 +local ITEMS_PER_PAGE = 7 local mailboxClickCatcher = nil -- OnLoad @@ -70,9 +73,23 @@ end -- Show specific character's mailbox function MailboxFrame:ShowCharacter(fullName) currentViewChar = fullName + currentPage = 1 self:Update() end +-- Pagination +function MailboxFrame:NextPage() + currentPage = currentPage + 1 + self:Update() +end + +function MailboxFrame:PrevPage() + if currentPage > 1 then + currentPage = currentPage - 1 + self:Update() + end +end + -- Initialize module function MailboxFrame:Initialize() -- Register events if needed @@ -105,13 +122,38 @@ function MailboxFrame:Update() matchesSearch = true elseif mail.subject and string.find(string.lower(mail.subject), searchText) then matchesSearch = true + elseif mail.items then + for _, item in ipairs(mail.items) do + if item.name and string.find(string.lower(item.name), searchText) then + matchesSearch = true + break + end + end elseif mail.item and mail.item.name and string.find(string.lower(mail.item.name), searchText) then matchesSearch = true end end if matchesSearch then - table.insert(filteredItems, mail) + -- Flatten mails with multiple items so they all show up in the grid + if mail.items and table.getn(mail.items) > 1 then + for itemIndex, item in ipairs(mail.items) do + local mailCopy = {} + for k, v in pairs(mail) do mailCopy[k] = v end + mailCopy.item = item + mailCopy.mailIndex = i + mailCopy.itemIndex = itemIndex + -- Clear items list in copy to avoid recursive flattening + mailCopy.items = nil + table.insert(filteredItems, mailCopy) + end + else + local mailCopy = {} + for k, v in pairs(mail) do mailCopy[k] = v end + mailCopy.mailIndex = i + mailCopy.itemIndex = 1 + table.insert(filteredItems, mailCopy) + end end end @@ -135,61 +177,76 @@ function MailboxFrame:Update() MoneyFrame_Update("Guda_MailboxFrame_MoneyFrame", totalMoney) end --- Display mailbox items in a grid +-- Display mailbox items in a list function MailboxFrame:DisplayItems(items, charFullName) local container = getglobal("Guda_MailboxFrame_ItemContainer") - -- Explicitly set container ID to -100 to avoid being picked up as a bag - if container.SetID then container:SetID(-100) end - local columns = 10 - local buttonSize = 34 - local spacing = 2 - - -- Hide all existing buttons first - for _, button in pairs(itemButtons) do - button:Hide() - button.inUse = false + -- Hide all existing rows first + for _, row in pairs(mailboxRows) do + row:Hide() end - local row = 0 - local col = 0 + local totalItems = table.getn(items) + local totalPages = math.max(1, math.ceil(totalItems / ITEMS_PER_PAGE)) - for i, mail in ipairs(items) do - local button = itemButtons[i] - if not button then - button = Guda_GetItemButton(container) - itemButtons[i] = button + if currentPage > totalPages then + currentPage = totalPages + end + + local startIndex = (currentPage - 1) * ITEMS_PER_PAGE + 1 + local endIndex = math.min(startIndex + ITEMS_PER_PAGE - 1, totalItems) + + local rowIndex = 1 + for i = startIndex, endIndex do + local mail = items[i] + local row = mailboxRows[rowIndex] + if not row then + row = CreateFrame("Frame", "Guda_MailboxRow" .. rowIndex, container, "Guda_MailboxRowTemplate") + mailboxRows[rowIndex] = row end - - -- Consistently set button size for the mailbox grid - button:SetWidth(buttonSize) - button:SetHeight(buttonSize) - - button:ClearAllPoints() - button:SetPoint("TOPLEFT", container, "TOPLEFT", col * (buttonSize + spacing), -row * (buttonSize + spacing)) - -- Set button data - button.isBank = false - button.otherChar = charFullName - button.isMail = true -- Custom flag for mailbox items - button.inUse = true + row:ClearAllPoints() + row:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -(rowIndex - 1) * 55) + + -- Fill row data + getglobal(row:GetName() .. "_Sender"):SetText(mail.sender or "Unknown") + getglobal(row:GetName() .. "_Subject"):SetText(mail.subject or "No Subject") + + local expireText = "" + if mail.daysLeft then + if mail.daysLeft < 1 then + expireText = string.format("|cffff0000%dh|r", math.floor(mail.daysLeft * 24)) + else + expireText = string.format("%dd", math.floor(mail.daysLeft)) + end + end + getglobal(row:GetName() .. "_ExpireTime"):SetText(expireText) + + -- Item Button + local itemButton = getglobal(row:GetName() .. "_ItemButton") + -- Re-assign shared namespace function if needed, but it should be available globally or via addon + + itemButton.isBank = false + itemButton.otherChar = charFullName + itemButton.mailData = mail + itemButton.isMail = true + itemButton.mailIndex = mail.mailIndex + itemButton.mailItemIndex = mail.itemIndex or 1 if mail.item and (mail.item.texture or mail.item.link) then - Guda_ItemButton_SetItem(button, nil, nil, mail.item, false, charFullName, true, true) - -- Re-enforce size because SetItem overrides it with global settings - button:SetWidth(buttonSize) - button:SetHeight(buttonSize) - button.isMail = true -- Re-apply after SetItem clears it + Guda_ItemButton_SetItem(itemButton, nil, nil, mail.item, false, charFullName, true, true) + itemButton.isMail = true + itemButton.mailData = mail + itemButton.mailIndex = mail.mailIndex + itemButton.mailItemIndex = mail.itemIndex or 1 else - -- Use SetItem with nil itemData to clear the button properly first - Guda_ItemButton_SetItem(button, nil, nil, nil, false, charFullName, true, true) - button:SetWidth(buttonSize) - button:SetHeight(buttonSize) - button.isMail = true -- Re-apply after SetItem clears it - - -- Then show our custom icon for money/mail - button.itemData = nil - local icon = getglobal(button:GetName().."IconTexture") or getglobal(button:GetName().."Icon") + Guda_ItemButton_SetItem(itemButton, nil, nil, nil, false, charFullName, true, true) + itemButton.isMail = true + itemButton.mailData = mail + itemButton.mailIndex = mail.mailIndex + itemButton.mailItemIndex = 1 + + local icon = getglobal(itemButton:GetName().."IconTexture") or getglobal(itemButton:GetName().."Icon") if icon then if (mail.money or 0) > 0 then icon:SetTexture("Interface\\Icons\\INV_Misc_Coin_01") @@ -201,73 +258,42 @@ function MailboxFrame:DisplayItems(items, charFullName) icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) icon:Show() end - - if button.qualityBorder then button.qualityBorder:Hide() end + if itemButton.qualityBorder then itemButton.qualityBorder:Hide() end end - - -- Custom Tooltip for mail items - button.mailData = mail -- Store data on button to avoid closure issues in Lua 5.0 - button.isMail = true -- Redundant set to be safe - button:SetScript("OnEnter", function() - -- Force detachment from Blizzard logic inside the closure as well - if this.SetID then this:SetID(0) end - this.isMail = true - - local mailData = this.mailData - if not mailData then return end - - -- Explicitly use GameTooltip:ClearLines() and SetOwner to ensure a clean tooltip - GameTooltip:SetOwner(this, "ANCHOR_RIGHT") - GameTooltip:ClearLines() - - if mailData.item and mailData.item.link then - GameTooltip:SetHyperlink(mailData.item.link) - -- Add a gap if there's also sender/subject info to show - GameTooltip:AddLine(" ") - elseif mailData.item and mailData.item.name then - -- Fallback if we have item data but no link (e.g. not in cache yet) - GameTooltip:AddLine(mailData.item.name, 1, 1, 1) - GameTooltip:AddLine(" ") - end - - GameTooltip:AddLine("From: " .. (mailData.sender or "Unknown"), 1, 1, 1) - GameTooltip:AddLine("Subject: " .. (mailData.subject or "No Subject"), 1, 1, 0.8) - - if (mailData.money or 0) > 0 then - GameTooltip:AddLine("Money: " .. addon.Modules.Utils:FormatMoney(mailData.money), 1, 1, 1) - end - if (mailData.CODAmount or 0) > 0 then - GameTooltip:AddLine("COD: " .. addon.Modules.Utils:FormatMoney(mailData.CODAmount), 1, 0, 0) - end - - if mailData.daysLeft then - GameTooltip:AddLine("Days left: " .. math.floor(mailData.daysLeft), 0.5, 0.5, 0.5) - end - - GameTooltip:Show() - end) - button:SetScript("OnLeave", function() - GameTooltip:Hide() - end) - -- Mailbox is read-only, so no clicking/dragging - button:RegisterForClicks() - button:SetScript("OnClick", nil) - button:SetScript("OnDragStart", nil) - button:SetScript("OnReceiveDrag", nil) - - button:Show() - - col = col + 1 - if col >= columns then - col = 0 - row = row + 1 + -- Row Money Frame + local moneyFrame = getglobal(row:GetName() .. "_MoneyFrame") + if (mail.money or 0) > 0 then + MoneyFrame_Update(moneyFrame:GetName(), mail.money) + moneyFrame:Show() + else + moneyFrame:Hide() end + + row:Show() + rowIndex = rowIndex + 1 end - -- Adjust container height - local totalRows = row + (col > 0 and 1 or 0) - container:SetHeight(math.max(420, totalRows * (buttonSize + spacing))) + -- Update pagination buttons + local prevBtn = getglobal("Guda_MailboxFrame_PrevPageButton") + local nextBtn = getglobal("Guda_MailboxFrame_NextPageButton") + + if currentPage > 1 then + prevBtn:Enable() + else + prevBtn:Disable() + end + + if currentPage < totalPages then + nextBtn:Enable() + else + nextBtn:Disable() + end + + -- Update page text in footer + local footerFontString = getglobal("Guda_MailboxFrame_Footer_Text") + local footerText = footerFontString:GetText() or "" + footerFontString:SetText(footerText .. " (Page " .. currentPage .. " of " .. totalPages .. ")") end -- Search text changed @@ -279,6 +305,7 @@ function Guda_MailboxFrame_OnSearchTextChanged() else searchText = string.lower(text) end + currentPage = 1 MailboxFrame:Update() end diff --git a/UI/MailboxFrame.xml b/UI/MailboxFrame.xml index a2dc88d..619fdee 100644 --- a/UI/MailboxFrame.xml +++ b/UI/MailboxFrame.xml @@ -1,6 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -224,6 +300,43 @@ + + + + +