diff --git a/Assets/Envelope.blp b/Assets/Envelope.blp
new file mode 100644
index 0000000..5d5ea4f
Binary files /dev/null and b/Assets/Envelope.blp differ
diff --git a/Core/Database.lua b/Core/Database.lua
index 533b830..f77621e 100644
--- a/Core/Database.lua
+++ b/Core/Database.lua
@@ -131,6 +131,7 @@ function DB:Initialize()
money = 0,
bags = {},
bank = {},
+ mailbox = {}, -- Add mailbox storage
equipped = {}, -- Add equipped items storage
character = {}, -- Add character info storage
lastUpdate = time(),
@@ -153,6 +154,10 @@ function DB:Initialize()
char.character = {}
addon:Debug("Added character field to existing character")
end
+ if not char.mailbox then
+ char.mailbox = {}
+ addon:Debug("Added mailbox field to existing character")
+ end
end
addon:Debug("Database initialized for %s", fullName)
@@ -215,6 +220,58 @@ function DB:SaveMoney(copper)
end
end
+-- Save mailbox data
+function DB:SaveMailbox(mailboxData)
+ local char = self:GetCurrentCharacter()
+ if char then
+ char.mailbox = mailboxData
+ char.lastUpdate = time()
+ addon:Debug("Saved mailbox data")
+ end
+end
+
+-- Add a single mail entry to a character's mailbox
+function DB:AddMailToCharacter(name, realm, mailRow)
+ local fullName = name .. "-" .. (realm or playerRealm)
+ local char = Guda_DB.characters[fullName]
+
+ if char then
+ if not char.mailbox then
+ char.mailbox = {}
+ end
+
+ -- Check if this exact mail already exists (simplistic check)
+ local exists = false
+ for _, m in ipairs(char.mailbox) do
+ if m.sender == mailRow.sender and m.subject == mailRow.subject and m.money == mailRow.money then
+ if (not m.item and not mailRow.item) or (m.item and mailRow.item and m.item.name == mailRow.item.name and m.item.count == mailRow.item.count) then
+ exists = true
+ -- Update link/itemID if missing in existing but present in new
+ if mailRow.item and m.item then
+ if not m.item.link and mailRow.item.link then
+ m.item.link = mailRow.item.link
+ addon:Debug("Updated link for existing mail item")
+ end
+ if not m.item.itemID and mailRow.item.itemID then
+ m.item.itemID = mailRow.item.itemID
+ addon:Debug("Updated itemID for existing mail item")
+ end
+ end
+ break
+ end
+ end
+ end
+
+ if not exists then
+ table.insert(char.mailbox, 1, mailRow) -- Add to beginning
+ char.lastUpdate = time()
+ addon:Debug("Added outgoing mail to %s's mailbox", fullName)
+ return true
+ end
+ end
+ return false
+end
+
-- Get all characters (optionally filter by faction and/or realm)
function DB:GetAllCharacters(sameFactionOnly, currentRealmOnly)
local chars = {}
@@ -256,6 +313,65 @@ function DB:GetCharacterBank(fullName)
return char and char.bank or {}
end
+-- Get character's mailbox
+function DB:GetCharacterMailbox(fullName)
+ local char = Guda_DB.characters[fullName]
+ return char and char.mailbox or {}
+end
+
+-- Find an item ID and link by name in any character's data
+function DB:FindItemByName(name)
+ if not name or name == "" or not Guda_DB or not Guda_DB.characters then return nil, nil end
+
+ for fullName, char in pairs(Guda_DB.characters) do
+ -- Check bags
+ if char.bags then
+ for bagID, bagData in pairs(char.bags) do
+ if type(bagData) == "table" and bagData.slots then
+ for slotID, item in pairs(bagData.slots) do
+ if item and item.name == name and item.link then
+ local itemID = addon.Modules.Utils:ExtractItemID(item.link)
+ if itemID then return itemID, item.link end
+ end
+ end
+ end
+ end
+ end
+ -- Check bank
+ if char.bank then
+ for bagID, bagData in pairs(char.bank) do
+ if type(bagData) == "table" and bagData.slots then
+ for slotID, item in pairs(bagData.slots) do
+ if item and item.name == name and item.link then
+ local itemID = addon.Modules.Utils:ExtractItemID(item.link)
+ if itemID then return itemID, item.link end
+ end
+ end
+ end
+ end
+ end
+ -- Check equipped
+ if char.equipped then
+ for slot, item in pairs(char.equipped) do
+ if item and item.name == name and item.link then
+ local itemID = addon.Modules.Utils:ExtractItemID(item.link)
+ if itemID then return itemID, item.link end
+ end
+ end
+ end
+ -- Check mailbox
+ if char.mailbox then
+ for _, mail in ipairs(char.mailbox) do
+ if mail.item and mail.item.name == name and mail.item.link then
+ local itemID = addon.Modules.Utils:ExtractItemID(mail.item.link)
+ if itemID then return itemID, mail.item.link end
+ end
+ end
+ end
+ end
+ return nil, nil
+end
+
-- Get character's equipped items
function DB:GetCharacterEquipped(fullName)
local char = Guda_DB.characters[fullName]
diff --git a/Core/Events.lua b/Core/Events.lua
index ec4b7a4..b690f16 100644
--- a/Core/Events.lua
+++ b/Core/Events.lua
@@ -79,3 +79,11 @@ end
function Events:OnPlayerLogout(callback, owner)
self:Register("PLAYER_LOGOUT", callback, owner)
end
+
+function Events:OnMailShow(callback, owner)
+ self:Register("MAIL_SHOW", callback, owner)
+end
+
+function Events:OnMailClosed(callback, owner)
+ self:Register("MAIL_CLOSED", callback, owner)
+end
diff --git a/Core/Init.lua b/Core/Init.lua
index aa68ab4..9066e04 100644
--- a/Core/Init.lua
+++ b/Core/Init.lua
@@ -102,11 +102,13 @@ addon.Modules = {
Tooltip = {},
BagScanner = {},
BankScanner = {},
+ MailboxScanner = {},
MoneyTracker = {},
EquipmentScanner = {},
SortEngine = {},
BagFrame = {},
BankFrame = {},
+ MailboxFrame = {},
QuestItemBar = {},
TrackedItemBar = {},
SettingsPopup = {},
diff --git a/Core/Main.lua b/Core/Main.lua
index 9316884..2025155 100644
--- a/Core/Main.lua
+++ b/Core/Main.lua
@@ -18,6 +18,7 @@ function Main:Initialize()
-- Initialize scanners
addon.Modules.BagScanner:Initialize()
addon.Modules.BankScanner:Initialize()
+ addon.Modules.MailboxScanner:Initialize()
addon.Modules.MoneyTracker:Initialize()
addon.Modules.EquipmentScanner:Initialize()
@@ -25,6 +26,7 @@ function Main:Initialize()
addon:Print("Initializing UI...")
addon.Modules.BagFrame:Initialize()
addon.Modules.BankFrame:Initialize()
+ addon.Modules.MailboxFrame:Initialize()
addon:Debug("Checking QuestItemBar module...")
if addon.Modules.QuestItemBar and addon.Modules.QuestItemBar.isLoaded then
@@ -103,6 +105,10 @@ function Main:SetupSlashCommands()
-- Toggle bank
addon.Modules.BankFrame:Toggle()
+ elseif msg == "mail" or msg == "mailbox" then
+ -- Toggle mailbox
+ addon.Modules.MailboxFrame:Toggle()
+
elseif msg == "sort" then
-- Sort bags
addon.Modules.SortEngine:SortBags()
@@ -143,6 +149,7 @@ function Main:SetupSlashCommands()
addon:Print("Commands:")
addon:Print("/guda - Toggle bags")
addon:Print("/guda bank - Toggle bank")
+ addon:Print("/guda mail - Toggle mailbox")
addon:Print("/guda sort - Sort bags")
addon:Print("/guda sortbank - Sort bank")
addon:Print("/guda track - Toggle item tracking")
diff --git a/Core/Tooltip.lua b/Core/Tooltip.lua
index 8663ce8..ba118f5 100644
--- a/Core/Tooltip.lua
+++ b/Core/Tooltip.lua
@@ -7,12 +7,16 @@ addon.Modules.Tooltip = Tooltip
-- Helper function to get item ID from link (Lua 5.0 compatible)
local function GetItemIDFromLink(link)
if not link then return nil end
- local _, _, itemID = strfind(link, "item:(%d+):?")
+ if type(link) == "number" then return link end
+
+ -- Try to find itemID in a standard link or a raw item:ID string
+ local _, _, itemID = string.find(link, "item:(%d+)")
return itemID and tonumber(itemID) or nil
end
local function CountCurrentCharacterItems(itemID)
local bagCount = 0
local bankCount = 0
+ local mailCount = 0
local equippedCount = 0
-- Count current character's bags in real-time
@@ -83,6 +87,74 @@ local function CountCurrentCharacterItems(itemID)
end
end
+ -- Count current character's mailbox in real-time if mailbox is open
+ if addon.Modules.MailboxScanner and addon.Modules.MailboxScanner:IsMailboxOpen() then
+ local numInboxItems = GetInboxNumItems()
+ for i = 1, numInboxItems do
+ local _, _, _, _, _, _, _, hasItem = GetInboxHeaderInfo(i)
+ if hasItem then
+ -- Turtle WoW supports up to 12 attachments per mail.
+ -- We use GetInboxNumAttachments if available to avoid over-scanning.
+ local numAttachments = 0
+ if GetInboxNumAttachments then
+ numAttachments = GetInboxNumAttachments(i) or 0
+ end
+
+ -- Fallback: if we don't have the count but header says there's an item, assume at least 1.
+ if numAttachments == 0 and hasItem then
+ numAttachments = 1
+ end
+
+ for j = 1, numAttachments do -- Turtle WoW supports up to 12 attachments
+ local name, _, count = GetInboxItem(i, j)
+ if name then
+ local itemLink = addon.Modules.Utils:GetInboxItemLink(i, j)
+ if itemLink then
+ local slotItemID = GetItemIDFromLink(itemLink)
+ if slotItemID == itemID then
+ mailCount = mailCount + (count or 1)
+ end
+ end
+ end
+ end
+ end
+ end
+ else
+ -- Mailbox not open - use saved data
+ local playerName = addon.Modules.DB:GetPlayerFullName()
+ local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[playerName]
+ if charData and charData.mailbox and type(charData.mailbox) == "table" then
+ for _, mail in ipairs(charData.mailbox) do
+ if mail.items then
+ for _, item in ipairs(mail.items) do
+ local slotItemID = item.link and GetItemIDFromLink(item.link)
+ if slotItemID == itemID then
+ mailCount = mailCount + (item.count or 1)
+ elseif not slotItemID and item.name then
+ -- Fallback to name matching if link is missing
+ local targetName = GetItemInfo(itemID)
+ if targetName == item.name then
+ mailCount = mailCount + (item.count or 1)
+ end
+ end
+ end
+ elseif mail.item then -- Fallback for single item data structure
+ local item = mail.item
+ local slotItemID = item.link and GetItemIDFromLink(item.link)
+ if slotItemID == itemID then
+ mailCount = mailCount + (item.count or 1)
+ elseif not slotItemID and item.name then
+ -- Fallback to name matching if link is missing
+ local targetName = GetItemInfo(itemID)
+ if targetName == item.name then
+ mailCount = mailCount + (item.count or 1)
+ end
+ end
+ end
+ end
+ end
+ end
+
-- Count equipped items in real-time
for slotID = 1, 19 do -- All equipment slots
local link = GetInventoryItemLink("player", slotID)
@@ -94,7 +166,7 @@ local function CountCurrentCharacterItems(itemID)
end
end
- return bagCount, bankCount, equippedCount
+ return bagCount, bankCount, equippedCount, mailCount
end
-- Count items for a specific character with real-time data for current character
@@ -107,6 +179,7 @@ local function CountItemsForCharacter(itemID, characterData, isCurrentChar)
-- For other characters, use saved data
local bagCount = 0
local bankCount = 0
+ local mailCount = 0
local equippedCount = 0
-- Count bags from saved data
@@ -141,6 +214,38 @@ local function CountItemsForCharacter(itemID, characterData, isCurrentChar)
end
end
+ -- Count mailbox from saved data
+ if characterData.mailbox and type(characterData.mailbox) == "table" then
+ for _, mail in ipairs(characterData.mailbox) do
+ if mail.items then
+ for _, item in ipairs(mail.items) do
+ local slotItemID = item.link and GetItemIDFromLink(item.link)
+ if slotItemID == itemID then
+ mailCount = mailCount + (item.count or 1)
+ elseif not slotItemID and item.name then
+ -- Fallback to name matching if link is missing
+ local targetName = GetItemInfo(itemID)
+ if targetName == item.name then
+ mailCount = mailCount + (item.count or 1)
+ end
+ end
+ end
+ elseif mail.item then -- Fallback for single item data structure
+ local item = mail.item
+ local slotItemID = item.link and GetItemIDFromLink(item.link)
+ if slotItemID == itemID then
+ mailCount = mailCount + (item.count or 1)
+ elseif not slotItemID and item.name then
+ -- Fallback to name matching if link is missing
+ local targetName = GetItemInfo(itemID)
+ if targetName == item.name then
+ mailCount = mailCount + (item.count or 1)
+ end
+ end
+ end
+ end
+ end
+
-- Count equipped items from saved data
if characterData.equipped and type(characterData.equipped) == "table" then
for slotName, itemData in pairs(characterData.equipped) do
@@ -153,7 +258,7 @@ local function CountItemsForCharacter(itemID, characterData, isCurrentChar)
end
end
- return bagCount, bankCount, equippedCount
+ return bagCount, bankCount, equippedCount, mailCount
end
@@ -187,8 +292,15 @@ function Tooltip:AddInventoryInfo(tooltip, link)
return
end
+ -- Guard against double-adding for the same item on the same tooltip
+ if tooltip.GudaInventoryAdded == itemID then
+ return
+ end
+ tooltip.GudaInventoryAdded = itemID
+
local totalBags = 0
local totalBank = 0
+ local totalMail = 0
local totalEquipped = 0
local characterCounts = {}
local hasAnyItems = false
@@ -201,18 +313,20 @@ function Tooltip:AddInventoryInfo(tooltip, link)
-- Ensure charData is actually a table and on current realm
if type(charData) == "table" and charData.realm == currentRealm then
local isCurrentChar = (charName == currentPlayerName)
- local bagCount, bankCount, equippedCount = CountItemsForCharacter(itemID, charData, isCurrentChar)
+ local bagCount, bankCount, equippedCount, mailCount = CountItemsForCharacter(itemID, charData, isCurrentChar)
- if bagCount > 0 or bankCount > 0 or equippedCount > 0 then
+ if bagCount > 0 or bankCount > 0 or equippedCount > 0 or mailCount > 0 then
hasAnyItems = true
totalBags = totalBags + bagCount
totalBank = totalBank + bankCount
+ totalMail = totalMail + mailCount
totalEquipped = totalEquipped + equippedCount
table.insert(characterCounts, {
name = charData.name or charName,
classToken = charData.classToken,
bagCount = bagCount,
bankCount = bankCount,
+ mailCount = mailCount,
equippedCount = equippedCount,
isCurrent = isCurrentChar
})
@@ -221,7 +335,7 @@ function Tooltip:AddInventoryInfo(tooltip, link)
-- If charData is not a table (string, number, etc.), just skip it
end
- local totalCount = totalBags + totalBank + totalEquipped
+ local totalCount = totalBags + totalBank + totalMail + totalEquipped
if hasAnyItems then
@@ -233,7 +347,16 @@ function Tooltip:AddInventoryInfo(tooltip, link)
-- Total line with cyan label and white count
local totalText = "|cFF00FFFFTotal|r: |cFFFFFFFF" .. totalCount .. "|r"
- local breakdownText = "(|cFF00FFFFBags|r: |cFFFFFFFF" .. totalBags .. "|r | |cFF00FFFFBank|r: |cFFFFFFFF" .. totalBank .. "|r)"
+ local breakdownParts = {}
+ if totalBags > 0 then table.insert(breakdownParts, "|cFF00FFFFBags|r: |cFFFFFFFF" .. totalBags .. "|r") end
+ if totalBank > 0 then table.insert(breakdownParts, "|cFF00FFFFBank|r: |cFFFFFFFF" .. totalBank .. "|r") end
+ if totalMail > 0 then table.insert(breakdownParts, "|cFF00FFFFMail|r: |cFFFFFFFF" .. totalMail .. "|r") end
+ if totalEquipped > 0 then table.insert(breakdownParts, "|cFF00FFFFEquipped|r: |cFFFFFFFF" .. totalEquipped .. "|r") end
+
+ local breakdownText = ""
+ if table.getn(breakdownParts) > 0 then
+ breakdownText = "(" .. table.concat(breakdownParts, " | ") .. ")"
+ end
tooltip:AddDoubleLine(totalText, breakdownText, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)
-- Sort with current character first
@@ -254,6 +377,9 @@ function Tooltip:AddInventoryInfo(tooltip, link)
if charInfo.bankCount > 0 then
table.insert(parts, "|cFF00FFFFBank|r: |cFFFFFFFF" .. charInfo.bankCount .. "|r")
end
+ if charInfo.mailCount > 0 then
+ table.insert(parts, "|cFF00FFFFMail|r: |cFFFFFFFF" .. charInfo.mailCount .. "|r")
+ end
if charInfo.equippedCount > 0 then
table.insert(parts, "|cFF00FFFFEquipped|r: |cFFFFFFFF" .. charInfo.equippedCount .. "|r")
end
@@ -453,6 +579,19 @@ function Tooltip:Initialize()
end)
end
+ -- Hook SetInboxItem for mailbox
+ local oldSetInboxItem = GameTooltip.SetInboxItem
+ function GameTooltip:SetInboxItem(index, itemIndex)
+ return WithDeferredMoney(self, function()
+ local ret = oldSetInboxItem(self, index, itemIndex)
+ local link = addon.Modules.Utils:GetInboxItemLink(index, itemIndex)
+ if link then
+ Tooltip:AddInventoryInfo(self, link)
+ end
+ return ret
+ end)
+ end
+
-- Hook SetTradeSkillItem for profession reagents and items
local oldSetTradeSkillItem = GameTooltip.SetTradeSkillItem
function GameTooltip:SetTradeSkillItem(skillIndex, reagentIndex)
diff --git a/Core/Utils.lua b/Core/Utils.lua
index 4997fe8..6e65be3 100644
--- a/Core/Utils.lua
+++ b/Core/Utils.lua
@@ -95,6 +95,36 @@ 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)
+ -- Try global function first (if it exists on this server/version)
+ if GetInboxItemLink then
+ -- Turtle WoW might support (index, itemIndex) for multiple attachments
+ local link = GetInboxItemLink(index, itemIndex or 1)
+ if link then return link end
+
+ -- Fallback to single argument if that failed
+ link = GetInboxItemLink(index)
+ if link then return link end
+ end
+
+ -- In 1.12.1, GameTooltip:GetHyperlink() does not exist.
+ -- Let's try to use GetItemInfo(name) as the primary way.
+ local name, texture, count, quality = GetInboxItem(index, itemIndex or 1)
+ if name then
+ local itemName, link = GetItemInfo(name)
+ if link then
+ return link
+ end
+ end
+
+ 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 +186,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
new file mode 100644
index 0000000..bcf2644
--- /dev/null
+++ b/Data/MailboxScanner.lua
@@ -0,0 +1,323 @@
+-- Guda Mailbox Scanner
+-- Scans and stores mailbox contents
+
+local addon = Guda
+
+local MailboxScanner = {}
+addon.Modules.MailboxScanner = MailboxScanner
+
+local mailboxOpen = false
+
+-- Scan all mailbox items and return data
+function MailboxScanner:ScanMailbox()
+ if not mailboxOpen then
+ addon:Debug("Cannot scan mailbox - not open")
+ return {}
+ end
+
+ local mailboxData = {}
+ local numItems = GetInboxNumItems()
+
+ for i = 1, numItems do
+ local mailRows = self:ScanMailItemRows(i)
+ for _, row in ipairs(mailRows) do
+ table.insert(mailboxData, row)
+ end
+ end
+
+ return mailboxData
+end
+
+-- Scan a single mail into one or more rows (flattened)
+function MailboxScanner:ScanMailItemRows(index)
+ -- 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 rows = {}
+
+ if hasItem then
+ -- Turtle WoW supports up to 12 attachments per mail.
+ -- We use GetInboxNumAttachments if available to avoid over-scanning on servers
+ -- that might ignore the second argument of GetInboxItem.
+ local numAttachments = 0
+ if GetInboxNumAttachments then
+ numAttachments = GetInboxNumAttachments(index) or 0
+ end
+
+ -- Fallback: if we don't have the count but header says there's an item, assume at least 1.
+ if numAttachments == 0 and hasItem then
+ numAttachments = 1
+ end
+
+ for itemIndex = 1, numAttachments do
+ -- GetInboxItem(index, itemIndex) returns: name, texture, count, quality, canUse
+ local name, texture, count, quality, canUse = GetInboxItem(index, itemIndex)
+ if not name then break end
+
+ local itemLink = addon.Modules.Utils:GetInboxItemLink(index, itemIndex)
+ local itemID = itemLink and addon.Modules.Utils:ExtractItemID(itemLink)
+
+ -- Fallback 1: If link/itemID is missing, try GetItemInfo(name) which might be cached now
+ if not itemID or not itemLink then
+ local _, link = GetItemInfo(name)
+ if link then
+ itemLink = link
+ itemID = addon.Modules.Utils:ExtractItemID(link)
+ addon:Debug("Recovered link from GetItemInfo for %s", name)
+ end
+ end
+
+ -- Fallback 2: If still missing, try to recover from existing database (any character)
+ if not itemID or not itemLink then
+ itemID, itemLink = addon.Modules.DB:FindItemByName(name)
+ if itemID then
+ addon:Debug("Recovered link from DB for %s", name)
+ end
+ end
+
+ local itemData = {
+ link = itemLink,
+ texture = texture or "Interface\\Icons\\INV_Misc_Bag_08",
+ count = count or 1,
+ quality = quality or 0,
+ name = name,
+ itemID = itemID,
+ }
+
+ -- Ensure we have a link if we have an itemID
+ if itemData.itemID and not itemData.link then
+ itemData.link = "item:" .. itemData.itemID .. ":0:0:0"
+ end
+
+ -- If we have a link, try to get more detailed info from cache
+ if itemData.link then
+ local itemName, link, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(itemData.link)
+ 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(rows, {
+ sender = sender,
+ subject = subject,
+ money = (itemIndex == 1) and money or 0, -- Attach money only to the first row of this mail
+ CODAmount = (itemIndex == 1) and CODAmount or 0,
+ daysLeft = daysLeft,
+ hasItem = true,
+ item = itemData,
+ mailIndex = index,
+ itemIndex = itemIndex,
+ wasRead = wasRead,
+ packageIcon = packageIcon,
+ })
+ end
+ end
+
+ -- If no items found but there is money or it's just a letter
+ if table.getn(rows) == 0 then
+ table.insert(rows, {
+ sender = sender,
+ subject = subject,
+ money = money,
+ CODAmount = CODAmount,
+ daysLeft = daysLeft,
+ hasItem = false,
+ item = nil,
+ mailIndex = index,
+ itemIndex = 1,
+ wasRead = wasRead,
+ packageIcon = packageIcon,
+ })
+ end
+
+ return rows
+end
+
+-- Save current mailbox to database
+function MailboxScanner:SaveToDatabase()
+ if not mailboxOpen then
+ return
+ end
+
+ local mailboxData = self:ScanMailbox()
+ addon.Modules.DB:SaveMailbox(mailboxData)
+ addon:Debug("Mailbox data saved")
+end
+
+-- Handle outgoing mail
+function MailboxScanner:OnSendMail(recipient, subject, body)
+ if not recipient or recipient == "" then return end
+
+ -- In WoW 1.12.1, SendMail(recipient, subject, body) is the signature.
+ -- To get the attached item, we use GetSendMailItem().
+ -- GetSendMailItem() returns: name, texture, count, quality
+ local name, texture, count, quality = GetSendMailItem()
+ local moneyAmount = GetSendMailMoney()
+
+ local itemData = nil
+ if name then
+ local _, link = GetItemInfo(name)
+ itemData = {
+ name = name,
+ texture = texture or "Interface\\Icons\\INV_Misc_Bag_08",
+ count = count or 1,
+ quality = quality or 0,
+ link = link,
+ itemID = addon.Modules.Utils:ExtractItemID(link),
+ }
+
+ -- Try to get more info if it's in cache
+ local itemName, retLink, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(name)
+ if itemName then
+ itemData.link = retLink or itemData.link
+ itemData.itemID = addon.Modules.Utils:ExtractItemID(itemData.link) or itemData.itemID
+ 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
+
+ -- Double check itemID
+ if not itemData.itemID and itemData.link then
+ itemData.itemID = addon.Modules.Utils:ExtractItemID(itemData.link)
+ end
+ end
+
+ if itemData or moneyAmount > 0 then
+ local mailRow = {
+ sender = UnitName("player"),
+ subject = (subject and subject ~= "") and subject or "No Subject",
+ money = moneyAmount,
+ CODAmount = 0,
+ daysLeft = 30, -- Outgoing mail typically has 30 days
+ hasItem = itemData ~= nil,
+ item = itemData,
+ wasRead = false,
+ }
+
+ addon.Modules.DB:AddMailToCharacter(recipient, nil, mailRow)
+ end
+end
+
+-- Handle auction house buyouts
+function MailboxScanner:OnAuctionBid(type, index, bid)
+ local name, texture, count, quality, canUse, level, minBid, minIncrement, buyoutPrice, bidAmount, highBidder, owner = GetAuctionItemInfo(type, index)
+
+ if name and buyoutPrice > 0 and bid >= buyoutPrice then
+ local link = GetAuctionItemLink(type, index)
+ local itemData = {
+ name = name,
+ texture = texture or "Interface\\Icons\\INV_Misc_Bag_08",
+ count = count or 1,
+ quality = quality or 0,
+ link = link,
+ itemID = addon.Modules.Utils:ExtractItemID(link),
+ }
+
+ -- Try to get more info if it's in cache
+ local itemName, retLink, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(link or name)
+ if itemName then
+ itemData.link = retLink or itemData.link
+ itemData.itemID = addon.Modules.Utils:ExtractItemID(itemData.link) or itemData.itemID
+ 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
+
+ local mailRow = {
+ sender = "Auction House",
+ subject = "Auction won: " .. name,
+ money = 0,
+ CODAmount = 0,
+ daysLeft = 30,
+ hasItem = true,
+ item = itemData,
+ wasRead = false,
+ }
+
+ addon.Modules.DB:AddMailToCharacter(UnitName("player"), nil, mailRow)
+ addon:Debug("Captured AH buyout: %s", name)
+ end
+end
+
+-- Initialize mailbox scanner
+function MailboxScanner:Initialize()
+ -- Hook SendMail to capture outgoing mail to alts
+ local originalSendMail = SendMail
+ SendMail = function(recipient, subject, body)
+ MailboxScanner:OnSendMail(recipient, subject, body)
+ return originalSendMail(recipient, subject, body)
+ end
+
+ -- Hook PlaceAuctionBid to capture AH buyouts
+ local originalPlaceAuctionBid = PlaceAuctionBid
+ PlaceAuctionBid = function(type, index, bid)
+ MailboxScanner:OnAuctionBid(type, index, bid)
+ return originalPlaceAuctionBid(type, index, bid)
+ end
+
+ -- Mailbox opened
+ addon.Modules.Events:OnMailShow(function()
+ mailboxOpen = true
+ addon:Debug("Mailbox opened")
+
+ -- Delay scan slightly to ensure item info is available
+ local frame = CreateFrame("Frame")
+ local elapsed = 0
+ frame:SetScript("OnUpdate", function()
+ elapsed = elapsed + arg1
+ if elapsed >= 0.5 then
+ frame:SetScript("OnUpdate", nil)
+ if mailboxOpen then
+ MailboxScanner:SaveToDatabase()
+ end
+ end
+ 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 MAIL_INBOX_UPDATE to detect when mail content changes
+ addon.Modules.Events:Register("MAIL_INBOX_UPDATE", function()
+ if mailboxOpen then
+ addon:Debug("MAIL_INBOX_UPDATE: 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
+ MailboxScanner:SaveToDatabase()
+ mailboxOpen = false
+ addon:Debug("Mailbox closed")
+ end, "MailboxScanner")
+end
+
+-- Check if mailbox is currently open
+function MailboxScanner:IsMailboxOpen()
+ return mailboxOpen
+end
diff --git a/Guda.toc b/Guda.toc
index e5347af..dab2d1e 100644
--- a/Guda.toc
+++ b/Guda.toc
@@ -2,7 +2,7 @@
## Title: Guda
## Notes: All-in-one bag and bank addon for Turtle WoW
## Author: Vati
-## Version: 1.4.8
+## Version: 1.5.0
## SavedVariables: Guda_DB
## SavedVariablesPerCharacter: Guda_CharDB
@@ -16,6 +16,7 @@ Core\Tooltip.lua
Data\BagScanner.lua
Data\BankScanner.lua
+Data\MailboxScanner.lua
Data\MoneyTracker.lua
Data\EquipmentScanner.lua
@@ -23,14 +24,17 @@ Sorting\SortEngine.lua
# UI Lua must load before XML so OnLoad handlers exist
UI\ItemButton.lua
+UI\FrameHelpers.lua
UI\BagFrame.lua
UI\BankFrame.lua
+UI\MailboxFrame.lua
UI\QuestItemBar.lua
UI\TrackedItemBar.lua
UI\SettingsPopup.lua
UI\ItemButton.xml
UI\BagFrame.xml
UI\BankFrame.xml
+UI\MailboxFrame.xml
UI\SettingsPopup.xml
Core\Main.lua
diff --git a/Sorting/SortEngine.lua b/Sorting/SortEngine.lua
index 3623a60..e2747d0 100644
--- a/Sorting/SortEngine.lua
+++ b/Sorting/SortEngine.lua
@@ -57,6 +57,7 @@ local CATEGORY_ORDER = {
["Weapon"] = 4, -- Non-equippable weapons
["Armor"] = 5, -- Non-equippable armor
["Tools"] = 6,
+ ["Quest"] = 7,
["Quiver"] = 8,
["Reagent"] = 9,
["Trade Goods"] = 10,
@@ -64,7 +65,6 @@ local CATEGORY_ORDER = {
["Container"] = 12,
["Key"] = 14,
["Miscellaneous"] = 15,
- ["Quest"] = 16,
["Junk"] = 17,
["Class Items"] = 18,
}
@@ -97,100 +97,137 @@ local GEM_PATTERNS = {
local scanTooltip = CreateFrame("GameTooltip", "Guda_SortScanTooltip", nil, "GameTooltipTemplate")
scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
--- Check if an item is a quest item by scanning its tooltip
-local function IsQuestItemTooltip(bagID, slotID)
- if not bagID or not slotID then return false end
+-- Property cache to prevent race conditions during rapid moves
+local propertyCache = {}
- scanTooltip:ClearLines()
- scanTooltip:SetBagItem(bagID, slotID)
+function SortEngine:ClearCache()
+ propertyCache = {}
+end
- -- Check all tooltip lines for quest-related text (case-insensitive)
- for i = 1, scanTooltip:NumLines() do
- local line = getglobal("Guda_SortScanTooltipTextLeft" .. i)
- if line then
- local text = line:GetText()
- if text then
- local tl = string.lower(text)
- if string.find(tl, "quest starter") or
- string.find(tl, "this item begins a quest") or
- string.find(tl, "starts a quest") or
- string.find(tl, "quest item") or
- string.find(tl, "manual") then
- return true
+local function GetItemProperties(bagID, slotID, itemLink)
+ if not itemLink then return nil end
+
+ -- Extract base item link for stable caching (item:ID:Enchant:...)
+ local _, _, baseLink = string.find(itemLink, "(item:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+)")
+ local cacheKey = baseLink or itemLink
+
+ if propertyCache[cacheKey] then
+ return propertyCache[cacheKey]
+ end
+
+ local props = {
+ isQuest = false,
+ isQuestStarter = false,
+ isQuestUsable = false,
+ isGray = false,
+ restoreTag = nil
+ }
+
+ -- Quality check from link color code (|cff9d9d9d is gray)
+ if string.find(itemLink, "|cff9d9d9d") then
+ props.isGray = true
+ end
+
+ if bagID and slotID then
+ scanTooltip:ClearLines()
+ scanTooltip:SetBagItem(bagID, slotID)
+
+ local numLines = scanTooltip:NumLines()
+ if numLines and numLines > 0 then
+ for i = 1, numLines do
+ local line = getglobal("Guda_SortScanTooltipTextLeft" .. i)
+ if line then
+ local text = line:GetText()
+ if text then
+ local tl = string.lower(text)
+
+ -- Quest item check
+ if string.find(tl, "quest starter") or
+ string.find(tl, "this item begins a quest") or
+ string.find(tl, "starts a quest") or
+ string.find(tl, "quest item") or
+ string.find(tl, "manual") then
+ props.isQuest = true
+ end
+
+ -- Starter check
+ if string.find(tl, "quest starter") or
+ string.find(tl, "this item begins a quest") or
+ string.find(tl, "starts a quest") then
+ props.isQuestStarter = true
+ end
+
+ -- Usable check
+ if string.find(tl, "use:") or
+ string.find(tl, "right%-click") or
+ string.find(tl, "right click") or
+ string.find(tl, "click to") then
+ props.isQuestUsable = true
+ end
+
+ -- Restore tag check (higher priority tags override lower ones)
+ if string.find(tl, "while eating") then
+ props.restoreTag = "eat"
+ elseif string.find(tl, "while drinking") then
+ if props.restoreTag ~= "eat" then
+ props.restoreTag = "drink"
+ end
+ elseif string.find(tl, "use: restores") then
+ if not props.restoreTag then
+ props.restoreTag = "restore"
+ end
+ end
+
+ -- Gray check (header line color fallback)
+ if i == 1 and string.find(text, "|cff9d9d9d") then
+ props.isGray = true
+ end
+ end
end
end
end
end
- return false
+
+ propertyCache[cacheKey] = props
+ return props
+end
+
+-- Check if an item is a quest item by scanning its tooltip
+local function IsQuestItemTooltip(bagID, slotID)
+ if not bagID or not slotID then return false end
+ local link = GetContainerItemLink(bagID, slotID)
+ local props = GetItemProperties(bagID, slotID, link)
+ return props and props.isQuest or false
end
-- Check if a quest item is usable (has 'Use:' or click to text)
local function IsQuestItemUsable(bagID, slotID)
if not bagID or not slotID then return false end
-
- scanTooltip:ClearLines()
- scanTooltip:SetBagItem(bagID, slotID)
-
- for i = 1, scanTooltip:NumLines() do
- local line = getglobal("Guda_SortScanTooltipTextLeft" .. i)
- if line then
- local text = line:GetText()
- if text then
- local tl = string.lower(text)
- if string.find(tl, "use:") or string.find(tl, "right%-click") or string.find(tl, "right click") or string.find(tl, "click to") or string.find(tl, "starts a quest") then
- return true
- end
- end
- end
- end
- return false
+ local link = GetContainerItemLink(bagID, slotID)
+ local props = GetItemProperties(bagID, slotID, link)
+ return props and props.isQuestUsable or false
end
-- Check if item is a quest starter (explicit 'Starts a Quest' or 'This Item Begins a Quest')
local function IsQuestItemStarter(bagID, slotID)
if not bagID or not slotID then return false end
-
- scanTooltip:ClearLines()
- scanTooltip:SetBagItem(bagID, slotID)
-
- for i = 1, scanTooltip:NumLines() do
- local line = getglobal("Guda_SortScanTooltipTextLeft" .. i)
- if line then
- local text = line:GetText()
- if text then
- local tl = string.lower(text)
- if string.find(tl, "quest starter") or string.find(tl, "this item begins a quest") or string.find(tl, "starts a quest") then
- return true
- end
- end
- end
- end
- return false
+ local link = GetContainerItemLink(bagID, slotID)
+ local props = GetItemProperties(bagID, slotID, link)
+ return props and props.isQuestStarter or false
end
-- Check if an item has a gray title in its tooltip or link
local function IsItemGrayTooltip(bagID, slotID, itemLink)
- -- Check link first if provided
- if itemLink and string.find(itemLink, "|cff9d9d9d") then
- return true
- end
+ local props = GetItemProperties(bagID, slotID, itemLink)
+ return props and props.isGray or false
+end
- if not bagID or not slotID then return false end
-
- scanTooltip:ClearLines()
- scanTooltip:SetBagItem(bagID, slotID)
-
- local line = getglobal("Guda_SortScanTooltipTextLeft1")
- if line then
- local text = line:GetText()
- if text then
- -- Poor quality color code is |cff9d9d9d
- if string.find(text, "|cff9d9d9d") then
- return true
- end
- end
- end
- return false
+-- Check if an item has a restore tag (eat, drink, or restore)
+local function GetItemRestoreTagTooltip(bagID, slotID)
+ if not bagID or not slotID then return nil end
+ local link = GetContainerItemLink(bagID, slotID)
+ local props = GetItemProperties(bagID, slotID, link)
+ return props and props.restoreTag or nil
end
-- Extract itemID from item link
@@ -200,6 +237,19 @@ local function GetItemID(link)
return tonumber(itemID) or 0
end
+-- Get item quality from link color code
+local function GetQualityFromLink(link)
+ if not link then return 1 end
+ if string.find(link, "|cff9d9d9d") then return 0 end -- Gray
+ if string.find(link, "|cffffffff") then return 1 end -- White
+ if string.find(link, "|cff1eff00") then return 2 end -- Green
+ if string.find(link, "|cff0070dd") then return 3 end -- Blue
+ if string.find(link, "|cffa335ee") then return 4 end -- Purple
+ if string.find(link, "|cffff8000") then return 5 end -- Orange
+ if string.find(link, "|cffe6cc80") then return 6 end -- Red/Artifact
+ return 1
+end
+
-- Extract texture pattern for grouping similar items
local function GetTexturePattern(textureName)
if not textureName then return "" end
@@ -466,24 +516,30 @@ local function AddSortKeys(items)
for _, item in ipairs(items) do
if item.data and item.data.link then
local itemID = GetItemID(item.data.link)
- local itemName, itemLink, itemRarity, itemLevel, itemCategory, itemType, itemStackCount,
- itemSubType, itemTexture, itemEquipLoc, itemSellPrice = GetItemInfo(itemID)
- item.itemName = itemName
- --addon:Print("itemTexture:%s",itemTexture or 0);
- --addon:Print("itemEquipLoc:%s",itemEquipLoc or 0);
- --addon:Print("itemSubType:%s",itemSubType or 0);
- --addon:Print("itemType:%s",itemType or 0);
- --addon:Print("itemCategory:%s",itemCategory or 0);
- --addon:Print("itemLevel:%s",itemLevel or 0);
+
+ -- Use data collected during the initial scan (item.data) for absolute stability
+ local d = item.data
+ local itemName = d.name
+ local itemLink = d.link
+ local itemRarity = d.quality
+ local itemLevel = d.iLevel
+ local itemCategory = d.class
+ local itemType = d.type
+ local itemSubType = d.subclass
+ local itemTexture = d.texture
+ local itemEquipLoc = d.equipLoc
+ local itemStackSize = d.stackSize or 1
+
+ item.itemName = itemName or ""
+
if not itemName then
- -- Skip items that couldn't be loaded
+ -- Skip items that couldn't be loaded (fallback safety)
item.sortedClass = 999
item.isEquippable = false
item.priority = 1000
item.equipSlotOrder = 999
item.invertedQuality = 0
item.invertedItemLevel = 0
- item.itemName = ""
item.sortedSubclass = 999
item.subclass = ""
item.texturePattern = ""
@@ -519,15 +575,14 @@ local function AddSortKeys(items)
item.equipSlotOrder = EQUIP_SLOT_ORDER[itemSubType] or 999
else
item.sortedClass = CATEGORY_ORDER[itemCategory] or 99
- -- Heuristic: Detect items that should be in the Quest category (priority 11)
+ -- Heuristic: Detect items that should be in the Quest category (priority 7)
-- but aren't categorized as such by the game (e.g. some "Manual" items)
- if item.sortedClass ~= (CATEGORY_ORDER["Quest"] or 99) then
- local nameLower = item.itemName and string.lower(item.itemName) or ""
+ if item.sortedClass ~= (CATEGORY_ORDER["Quest"] or 7) then
+ local nameLower = string.lower(item.itemName)
if string.find(nameLower, "manual") or string.find(nameLower, "quest") then
- item.sortedClass = 11
+ item.sortedClass = CATEGORY_ORDER["Quest"] or 7
elseif IsQuestItemTooltip(item.bagID, item.slot) then
- -- If name-based heuristic fails, check tooltip
- item.sortedClass = 11
+ item.sortedClass = CATEGORY_ORDER["Quest"] or 7
end
end
item.equipSlotOrder = 999
@@ -535,31 +590,24 @@ local function AddSortKeys(items)
-- Subclass ordering
-- Detect consumable restore/eat/drink tag (if available) BEFORE computing subclass priority
- item.restoreTag = nil
- if addon.Modules.Utils and addon.Modules.Utils.GetConsumableRestoreTag then
- if item.bagID and item.slot then
- local tag = addon.Modules.Utils:GetConsumableRestoreTag(item.bagID, item.slot)
- if tag then
- item.restoreTag = tag
- end
+ item.restoreTag = GetItemRestoreTagTooltip(item.bagID, item.slot)
+
+ local baseSub = GetSubclassOrder(itemSubType, item.itemName)
+ local function _cons_prk(t)
+ if t == "eat" then return -300 end
+ if t == "drink" then return -200 end
+ if t == "restore" then return -100 end
+ return 0
end
- end
- local baseSub = GetSubclassOrder(itemSubType, item.name)
- local function _cons_prk(t)
- if t == "eat" then return -300 end
- if t == "drink" then return -200 end
- if t == "restore" then return -100 end
- return 0
- end
- item.sortedSubclass = _cons_prk(item.restoreTag) + baseSub
+ item.sortedSubclass = _cons_prk(item.restoreTag) + baseSub
item.subclass = itemSubType or ""
-- Quest flags: mark quest items and detect starter/usable states
item.isQuest = false
item.isQuestStarter = false
item.isQuestUsable = false
- local nameLower = item.itemName and string.lower(item.itemName) or ""
- if itemCategory == "Quest" or string.find(nameLower, "quest") or (item.data and item.data.class == "Quest") or IsQuestItemTooltip(item.bagID, item.slot) then
+ local nameLower = string.lower(item.itemName)
+ if itemCategory == "Quest" or string.find(nameLower, "quest") or item.data.class == "Quest" or IsQuestItemTooltip(item.bagID, item.slot) then
item.isQuest = true
if IsQuestItemStarter(item.bagID, item.slot) then item.isQuestStarter = true end
if IsQuestItemUsable(item.bagID, item.slot) then item.isQuestUsable = true end
@@ -568,7 +616,6 @@ local function AddSortKeys(items)
-- Texture pattern for grouping similar items (especially trade goods)
item.texturePattern = GetTexturePattern(itemTexture)
-- Group Trade Goods: meats (names ending with 'meat') before eggs
- local nameLower = item.itemName and string.lower(item.itemName) or ""
if itemType == "Trade Goods" then
if string.find(nameLower, "meat$") then
item.texturePattern = "trade_meat"
@@ -584,12 +631,9 @@ local function AddSortKeys(items)
item.invertedItemID = -tonumber(itemID)
-- Stack info for reverse stack sorting
- item.maxStackCount = tonumber(itemStackCount) or 1
+ item.maxStackCount = tonumber(itemStackSize) or 1
item.isStackable = item.maxStackCount > 1
item.stackCount = tonumber(item.data.count) or 1
-
- -- Name for alphabetical sorting
- item.itemName = item.name or ""
end
end
end
@@ -670,6 +714,13 @@ local function SortItems(items)
if ra ~= rb then
return ra > rb
end
+ elseif a.isQuest ~= b.isQuest then
+ -- If only one is a quest item and they are in the same sortedClass,
+ -- put the quest item first within that class (or just let sortedClass handle it)
+ -- Since we moved Quest to its own class 7, this might not be needed unless
+ -- they are in different classes but one is marked isQuest.
+ -- However, if they have same sortedClass, we want quest items first.
+ return a.isQuest
end
if a.sortedSubclass ~= b.sortedSubclass then
return a.sortedSubclass < b.sortedSubclass
@@ -740,6 +791,11 @@ local function CollectItems(bagIDs)
-- Get fresh item info with ALL return values
local itemID = GetItemID(itemLink)
local name, link, quality, iLevel, category, itemType, stackCount, subType, iconTex, equipLoc, sellPrice = GetItemInfo(itemID)
+
+ -- Fallback for quality if GetItemInfo fails (ensures stability)
+ if not quality then
+ quality = GetQualityFromLink(itemLink)
+ end
sequence = sequence + 1
table.insert(items, {
@@ -748,14 +804,16 @@ local function CollectItems(bagIDs)
sequence = sequence, -- Preserve original order
data = {
link = itemLink,
- texture = texture,
+ texture = texture or iconTex,
count = itemCount or 1,
quality = quality or 0,
name = name,
iLevel = iLevel,
type = itemType,
class = category,
- subclass = subType, -- NOW INCLUDED!
+ subclass = subType,
+ equipLoc = equipLoc,
+ stackSize = stackCount or 1,
locked = locked,
},
quality = quality or 0,
@@ -891,8 +949,6 @@ end
-- and spilling into previous bags when needed.
local function BuildGreyTailPositions(bagIDs, greyCount)
local positions = {}
- local index = 1
-
if greyCount <= 0 then return positions end
-- Order bags: lowest priority first (these are considered "last"),
@@ -920,26 +976,42 @@ local function BuildGreyTailPositions(bagIDs, greyCount)
end)
-- Collect tail slots from end to start, spilling to previous bags as needed.
+ local tailSlots = {}
for _, info in ipairs(ordered) do
for slot = info.numSlots, 1, -1 do
- if index <= greyCount then
- positions[index] = { bag = info.bagID, slot = slot }
- index = index + 1
+ if table.getn(tailSlots) < greyCount then
+ table.insert(tailSlots, { bag = info.bagID, slot = slot })
else
break
end
end
- if index > greyCount then break end
+ if table.getn(tailSlots) >= greyCount then break end
end
- return positions
+ -- STABILITY FIX: Sort the collected tail slots to match the ascending scan order.
+ -- This ensures that identical items don't swap places every pass.
+ -- Ascending order: Priority DESC, BagID ASC, Slot ASC (matching BuildTargetPositions)
+ table.sort(tailSlots, function(a, b)
+ local aPrio = tonumber(addon.Modules.Utils:GetContainerPriority(a.bag)) or 0
+ local bPrio = tonumber(addon.Modules.Utils:GetContainerPriority(b.bag)) or 0
+ if aPrio ~= bPrio then
+ return aPrio > bPrio
+ end
+ if a.bag ~= b.bag then
+ return a.bag < b.bag
+ end
+ return a.slot < b.slot
+ end)
+
+ return tailSlots
end
-- Split a list of collected items into non-greys and greys (quality 0)
local function SplitGreyItems(items)
local nonGreys, greys = {}, {}
for _, item in ipairs(items) do
- if tonumber(item.quality or 0) == 0 then
+ -- Use same logic as AddSortKeys for determining Junk/Grey status (stability)
+ if tonumber(item.quality or 0) == 0 or IsItemGrayTooltip(item.bagID, item.slot, item.data.link) then
table.insert(greys, item)
else
table.insert(nonGreys, item)
@@ -1026,20 +1098,8 @@ function SortEngine:AnalyzeContainer(bagIDs, containerType)
for i, item in ipairs(greys) do
local target = tailPositions[i]
- -- FIX: Check if grey item is already in a tail position of a valid regular bag
- local isInTailPosition = false
-
- -- Check if this grey item is already in the end slots of any valid regular bag
- for _, bagID in ipairs(validRegularBags) do
- local numSlots = addon.Modules.Utils:GetBagSlotCount(bagID)
- if item.bagID == bagID and item.slot >= (numSlots - table.getn(greys) + 1) then
- isInTailPosition = true
- break
- end
- end
-
- -- Only count as out of place if it's NOT in a tail position AND doesn't match target
- if target and (item.bagID ~= target.bag or item.slot ~= target.slot) and not isInTailPosition then
+ -- Only count as out of place if it doesn't match the specific stable target
+ if target and (item.bagID ~= target.bag or item.slot ~= target.slot) then
totalOutOfPlace = totalOutOfPlace + 1
end
end
@@ -1326,6 +1386,9 @@ end
--===========================================================================
function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sortType)
+ -- Clear property cache at the start of a sort operation
+ self:ClearCache()
+
-- Analyze to determine how many passes are needed
local analysis = analyzeFunction()
diff --git a/UI/BagFrame.lua b/UI/BagFrame.lua
index 3e30964..6ba7d05 100644
--- a/UI/BagFrame.lua
+++ b/UI/BagFrame.lua
@@ -6,7 +6,19 @@ local addon = Guda
local BagFrame = {}
addon.Modules.BagFrame = BagFrame
+-- Use centralized frame helpers for section headers and bag parents
+function BagFrame:GetSectionHeader(index)
+ return Guda_GetSectionHeader("Guda_BagFrame", "Guda_BagFrame_ItemContainer", index)
+end
+
+function BagFrame:GetBagParent(bagID)
+ return Guda_GetBagParent("Guda_BagFrame", bagParents, bagID, "Guda_BagFrame_ItemContainer")
+end
+
local currentViewChar = nil -- nil = current character
+function BagFrame:GetCurrentViewChar()
+ return currentViewChar
+end
local searchText = ""
local itemButtons = {}
local showKeyring = false -- Toggle for keyring display
@@ -109,6 +121,9 @@ end
-- OnHide
function Guda_BagFrame_OnHide(self)
+ -- Close any open dropdown menus when the bag frame is hidden
+ CloseDropDownMenus()
+
-- Clean up all buttons when frame is hidden (safe since we're not displaying)
for _, bagParent in pairs(bagParents) do
if bagParent then
@@ -146,21 +161,7 @@ end
-- Update lock states of existing buttons (lightweight, used during drag)
function BagFrame:UpdateLockStates()
- for _, bagParent in pairs(bagParents) do
- if bagParent then
- local buttons = { bagParent:GetChildren() }
- for _, button in ipairs(buttons) do
- if button.hasItem ~= nil and button:IsShown() and button.bagID and button.slotID then
- -- Get live lock state
- local _, _, locked = GetContainerItemInfo(button.bagID, button.slotID)
- -- Update desaturation (gray out locked items)
- if not button.otherChar and not button.isReadOnly and SetItemButtonDesaturated then
- SetItemButtonDesaturated(button, locked, 0.5, 0.5, 0.5)
- end
- end
- end
- end
- end
+ Guda_UpdateLockStates(bagParents)
end
-- Update bagline layout (hover option)
@@ -365,44 +366,13 @@ function BagFrame:Update()
end
end
--- Helper to get or create section header
+-- Delegate to centralized helpers
function BagFrame:GetSectionHeader(index)
- local name = "Guda_BagFrame_SectionHeader" .. index
- local header = getglobal(name)
- if not header then
- header = CreateFrame("Frame", name, getglobal("Guda_BagFrame_ItemContainer"))
- header:SetHeight(20)
- header:EnableMouse(true)
- local text = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
- text:SetPoint("LEFT", header, "LEFT", 0, 0)
- header.text = text
-
- header:SetScript("OnEnter", function()
- if this.fullName and this.isShortened then
- GameTooltip:SetOwner(this, "ANCHOR_TOP")
- GameTooltip:SetText(this.fullName)
- GameTooltip:Show()
- end
- end)
- header:SetScript("OnLeave", function()
- GameTooltip:Hide()
- end)
- end
- header.inUse = true
- return header
+ return Guda_GetSectionHeader("Guda_BagFrame", "Guda_BagFrame_ItemContainer", index)
end
--- Helper to get or create bag parent frame
function BagFrame:GetBagParent(bagID)
- local itemContainer = getglobal("Guda_BagFrame_ItemContainer")
- if not bagParents[bagID] then
- bagParents[bagID] = CreateFrame("Frame", "Guda_BagFrame_BagParent"..bagID, itemContainer)
- bagParents[bagID]:SetAllPoints(itemContainer)
- if bagParents[bagID].SetID then
- bagParents[bagID]:SetID(bagID)
- end
- end
- return bagParents[bagID]
+ return Guda_GetBagParent("Guda_BagFrame", bagParents, bagID, "Guda_BagFrame_ItemContainer")
end
-- Display items by category
@@ -953,113 +923,7 @@ end
-- Resize frame based on number of rows and columns
function BagFrame:ResizeFrame(currentRow, currentCol, columns, overrideHeight)
- local buttonSize = addon.Modules.DB:GetSetting("iconSize") or addon.Constants.BUTTON_SIZE
- local spacing = addon.Modules.DB:GetSetting("iconSpacing") or addon.Constants.BUTTON_SPACING
-
- -- Calculate actual number of rows used
- local totalRows = (currentRow or 0) + 1
-
- -- Ensure at least 1 row
- if totalRows < 1 then
- totalRows = 1
- end
-
- -- Calculate required dimensions based on columns
- local containerWidth = (columns * (buttonSize + spacing)) + 20
- local containerHeight = overrideHeight or ((totalRows * (buttonSize + spacing)) + 20)
- local frameWidth = containerWidth + 20
-
- -- Check if search bar is visible
- local showSearchBar = addon.Modules.DB:GetSetting("showSearchBar")
- if showSearchBar == nil then
- showSearchBar = true
- end
-
- -- Adjust frame height based on search bar visibility
- -- Footer height varies: more space needed when search bar is visible
- local titleHeight = 40
- local searchBarHeight = 30
- local footerHeight
- local frameHeight
-
- local hideFooter = addon.Modules.DB:GetSetting("hideFooter")
-
- if hideFooter then
- footerHeight = 10 -- Small padding at bottom
- frameHeight = containerHeight + titleHeight + (showSearchBar and searchBarHeight or 0) + footerHeight
- elseif showSearchBar then
- footerHeight = 55 -- Increased footer height when search bar is visible (toolbar 40px + spacing 15px)
- frameHeight = containerHeight + titleHeight + searchBarHeight + footerHeight -- 125 total
- else
- footerHeight = 45 -- Normal footer height (toolbar 40px + spacing 5px)
- frameHeight = containerHeight + titleHeight + footerHeight -- 85 total
- end
-
- -- Minimum sizes
- if containerWidth < 200 then
- containerWidth = 200
- frameWidth = 220
- end
- if containerHeight < 150 then
- containerHeight = 150
- end
- if frameHeight < 250 then
- frameHeight = 250
- end
-
- -- Maximum sizes
- if containerWidth > 1250 then
- containerWidth = 1250
- frameWidth = 1270
- end
- if containerHeight > 1000 then
- containerHeight = 1000
- end
- if frameHeight > 1200 then
- frameHeight = 1200
- end
-
- -- Resize frames
- local bagFrame = getglobal("Guda_BagFrame")
- local itemContainer = getglobal("Guda_BagFrame_ItemContainer")
-
- if bagFrame then
- bagFrame:SetWidth(frameWidth)
- bagFrame:SetHeight(frameHeight)
-
- -- Always use BOTTOMRIGHT anchor to make frame grow left
- bagFrame:ClearAllPoints()
-
- if addon and addon.Modules and addon.Modules.DB then
- local pos = addon.Modules.DB:GetSetting("bagFramePosition")
- -- Only use saved position if it was saved as BOTTOMRIGHT
- if pos and pos.point == "BOTTOMRIGHT" and pos.x and pos.y then
- bagFrame:SetPoint("BOTTOMRIGHT", "UIParent", "BOTTOMRIGHT", pos.x, pos.y)
- else
- -- Default position: bottom right corner
- bagFrame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -50, 100)
- end
- else
- -- Fallback to default if DB not available
- bagFrame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -50, 100)
- end
- end
-
- if itemContainer then
- itemContainer:SetWidth(containerWidth)
- itemContainer:SetHeight(containerHeight)
- end
-
- -- Resize search bar and toolbar to match container width
- local searchBar = getglobal("Guda_BagFrame_SearchBar")
- if searchBar then
- searchBar:SetWidth(containerWidth)
- end
-
- local toolbar = getglobal("Guda_BagFrame_Toolbar")
- if toolbar then
- toolbar:SetWidth(containerWidth)
- end
+ return Guda_ResizeFrame("Guda_BagFrame", "Guda_BagFrame_ItemContainer", currentRow, currentCol, columns, overrideHeight)
end
-- Check if search is currently active
@@ -1069,74 +933,8 @@ end
-- Check if item passes search filter (pfUI style)
function BagFrame:PassesSearchFilter(itemData)
--- If no search text, everything matches
- if not self:IsSearchActive() then
- return true
- end
-
- -- Empty slots don't match when searching (pfUI style - they get dimmed)
- if not itemData then
- return false
- end
-
- -- Get item name from itemData.name or parse from link
- local itemName = itemData.name
- if not itemName and itemData.link then
- -- Parse name from item link: |cffffffff|Hitem:...|h[Item Name]|h|r
- local _, _, name = string.find(itemData.link, "%[(.+)%]")
- itemName = name
- if not self.warnedAboutParsing then
- addon:Print("DEBUG: Had to parse item name from link: " .. (itemName or "FAILED"))
- self.warnedAboutParsing = true
- end
- end
-
- if not itemName then
- if not self.warnedAboutNoName then
- addon:Print("DEBUG: Item has no name and no link! texture = " .. tostring(itemData.texture))
- self.warnedAboutNoName = true
- end
- return false
- end
-
- -- Case-insensitive search in item name
- local search = string.lower(searchText)
-
- -- Advanced search (pfUI style categories)
- if string.sub(search, 1, 1) == "~" then
- local category = string.sub(search, 2)
- local itemType = itemData.class or ""
- local itemQuality = itemData.quality or -1
-
- if category == "equipment" or category == "armor" or category == "weapon" then
- if itemType == "Armor" or itemType == "Weapon" then return true end
- elseif category == "consumable" then
- if itemType == "Consumable" then return true end
- elseif category == "tradegoods" or category == "trades" then
- if itemType == "Trade Goods" then return true end
- elseif category == "quest" then
- local isQuest, isQuestStarter = Guda_GetQuestInfo(itemData.bagID, itemData.slotID, itemData.isBank)
- if isQuest or isQuestStarter or itemType == "Quest" then return true end
- elseif category == "reagent" then
- if itemType == "Reagent" then return true end
- elseif category == "common" then if itemQuality == 1 then return true end
- elseif category == "uncommon" then if itemQuality == 2 then return true end
- elseif category == "rare" then if itemQuality == 3 then return true end
- elseif category == "epic" then if itemQuality == 4 then return true end
- elseif category == "legendary" then if itemQuality == 5 then return true end
- end
- end
-
- itemName = string.lower(itemName)
- -- Check if item name contains search text
- local matches = string.find(itemName, search, 1, true) ~= nil
-
- -- Debug: print first match found
- if matches and not self.foundFirstMatch then
- self.foundFirstMatch = true
- end
-
- return matches
+ if not self:IsSearchActive() then return true end
+ return Guda_PassesSearchFilter(itemData, searchText)
end
function BagFrame:UpdateMoney()
@@ -1405,119 +1203,15 @@ function Guda_BagFrame_MoneyOnEnter(self)
end
-- Dropdown management
-local characterDropdown = nil
-local bankDropdown = nil
-
--- Toggle character dropdown
-function Guda_BagFrame_ToggleCharacterDropdown(button)
--- Hide bank dropdown if it's shown
- if bankDropdown and bankDropdown:IsShown() then
- bankDropdown:Hide()
- end
-
- if characterDropdown and characterDropdown:IsShown() then
- characterDropdown:Hide()
- return
- end
-
- if not characterDropdown then
- -- Create dropdown frame
- characterDropdown = CreateFrame("Frame", "Guda_CharacterDropdown", UIParent)
- characterDropdown:SetFrameStrata("DIALOG")
- characterDropdown:SetWidth(200)
- characterDropdown:SetBackdrop({
- bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
- edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
- tile = true,
- tileSize = 16,
- edgeSize = 16,
- insets = { left = 4, right = 4, top = 4, bottom = 4 }
- })
- characterDropdown:SetBackdropColor(0, 0, 0, 0.95)
- characterDropdown:EnableMouse(true)
- characterDropdown:Hide()
-
- characterDropdown.buttons = {}
- end
-
- -- Position dropdown below the button
- characterDropdown:ClearAllPoints()
- characterDropdown:SetPoint("TOPLEFT", button, "BOTTOMLEFT", 0, -2)
-
- -- Clear existing buttons
- for _, btn in ipairs(characterDropdown.buttons) do
- btn:Hide()
- end
- characterDropdown.buttons = {}
-
- -- Get all characters on current realm
- local chars = addon.Modules.DB:GetAllCharacters(false, true)
-
- -- Add "Current Character" option at the top
- local yOffset = -8
- local currentCharButton = CreateFrame("Button", nil, characterDropdown)
- currentCharButton:SetWidth(188)
- currentCharButton:SetHeight(20)
- currentCharButton:SetPoint("TOP", characterDropdown, "TOP", 0, yOffset)
-
- -- Button background on hover
- local currentCharBg = currentCharButton:CreateTexture(nil, "BACKGROUND")
- currentCharBg:SetAllPoints()
- currentCharBg:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
- currentCharBg:SetBlendMode("ADD")
- currentCharBg:SetAlpha(0)
-
- -- Button text
- local currentCharText = currentCharButton:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
- currentCharText:SetPoint("LEFT", currentCharButton, "LEFT", 8, 0)
- currentCharText:SetText("Characters")
-
- -- Button scripts
- currentCharButton:SetScript("OnEnter", function()
- currentCharBg:SetAlpha(0.3)
- end)
- currentCharButton:SetScript("OnLeave", function()
- currentCharBg:SetAlpha(0)
- end)
- currentCharButton:SetScript("OnClick", function()
- addon.Modules.BagFrame:ShowCurrentCharacter()
- characterDropdown:Hide()
- end)
-
- table.insert(characterDropdown.buttons, currentCharButton)
- yOffset = yOffset - 20
-
- -- Add separator
- local separator = characterDropdown:CreateTexture(nil, "ARTWORK")
- separator:SetHeight(1)
- separator:SetWidth(180)
- separator:SetPoint("TOP", characterDropdown, "TOP", 0, yOffset)
- separator:SetTexture(1, 1, 1, 0.2)
- yOffset = yOffset - 4
-
- -- Get current player's full name for comparison
+local function Guda_BagCharacterMenu_Initialize()
+ local characters = addon.Modules.DB:GetAllCharacters(false, true)
+ local info
local currentPlayerFullName = addon.Modules.DB:GetPlayerFullName()
+ local currentViewChar = addon.Modules.BagFrame:GetCurrentViewChar()
- -- Add character buttons
- for _, char in ipairs(chars) do
- -- Capture variables in local scope for closure
+ for i, char in ipairs(characters) do
local charFullName = char.fullName
- local charName = char.name
- local charMoney = char.money or 0
local charClassToken = char.classToken
- local isCurrentChar = (charFullName == currentPlayerFullName)
-
- local charButton = CreateFrame("Button", nil, characterDropdown)
- charButton:SetWidth(188)
- charButton:SetHeight(20)
- charButton:SetPoint("TOP", characterDropdown, "TOP", 0, yOffset)
-
- -- Button background on hover
- local charBg = charButton:CreateTexture(nil, "BACKGROUND")
- charBg:SetAllPoints()
- charBg:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
- charBg:SetBlendMode("ADD")
- charBg:SetAlpha(0)
-- Get class color
local classColor = charClassToken and RAID_CLASS_COLORS[charClassToken]
@@ -1526,169 +1220,115 @@ function Guda_BagFrame_ToggleCharacterDropdown(button)
r, g, b = classColor.r, classColor.g, classColor.b
end
- -- Button text
- local charText = charButton:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
- charText:SetPoint("LEFT", charButton, "LEFT", 8, 0)
- charText:SetText(charName)
- charText:SetTextColor(r, g, b)
+ -- Create colored name
+ local coloredName = addon.Modules.Utils:ColorText(char.name, r, g, b)
- -- Money text
- local moneyText = charButton:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
- moneyText:SetPoint("RIGHT", charButton, "RIGHT", -8, 0)
- moneyText:SetText(addon.Modules.Utils:FormatMoney(charMoney))
- moneyText:SetTextColor(0.7, 0.7, 0.7)
-
- -- Button scripts
- charButton:SetScript("OnEnter", function()
- charBg:SetAlpha(0.3)
- end)
- charButton:SetScript("OnLeave", function()
- charBg:SetAlpha(0)
- end)
- charButton:SetScript("OnClick", function()
- if charFullName then
- if isCurrentChar then
- -- Clicking current character - show current live view
- addon.Modules.BagFrame:ShowCurrentCharacter()
- else
- -- Clicking different character - show their stored bags
- addon.Modules.BagFrame:ShowCharacter(charFullName)
- end
- characterDropdown:Hide()
+ info = {}
+ info.text = coloredName
+ info.func = function()
+ if charFullName == currentPlayerFullName then
+ addon.Modules.BagFrame:ShowCurrentCharacter()
else
- addon:Print("Error: Character fullName is nil")
+ addon.Modules.BagFrame:ShowCharacter(charFullName)
end
- end)
-
- table.insert(characterDropdown.buttons, charButton)
- yOffset = yOffset - 20
+ end
+ info.checked = (currentViewChar == charFullName or (not currentViewChar and charFullName == currentPlayerFullName))
+ UIDropDownMenu_AddButton(info)
end
-
- -- Set dropdown height based on content
- characterDropdown:SetHeight(math.abs(yOffset) + 8)
-
- -- Show dropdown
- characterDropdown:Show()
end
--- Hide dropdown when clicking elsewhere
-local function HideCharacterDropdown()
- if characterDropdown then
- characterDropdown:Hide()
+-- Toggle character dropdown
+function Guda_BagFrame_ToggleCharacterDropdown(button)
+ local menuFrame = getglobal("Guda_BagCharacterMenu")
+ if not menuFrame then
+ menuFrame = CreateFrame("Frame", "Guda_BagCharacterMenu", UIParent, "UIDropDownMenuTemplate")
+ end
+ UIDropDownMenu_Initialize(menuFrame, Guda_BagCharacterMenu_Initialize, "MENU")
+ ToggleDropDownMenu(1, nil, menuFrame, "cursor", 0, 0)
+end
+
+-- Mailbox character dropdown
+local function Guda_BagMailboxMenu_Initialize()
+ local characters = addon.Modules.DB:GetAllCharacters(false, true)
+ local info
+
+ for i, char in ipairs(characters) do
+ local charFullName = char.fullName
+ local charClassToken = char.classToken
+
+ -- Get class color
+ local classColor = charClassToken and RAID_CLASS_COLORS[charClassToken]
+ local r, g, b = 1, 1, 1
+ if classColor then
+ r, g, b = classColor.r, classColor.g, classColor.b
+ end
+
+ -- Create colored name
+ local coloredName = addon.Modules.Utils:ColorText(char.name, r, g, b)
+
+ info = {}
+ info.text = coloredName
+ info.func = function()
+ addon.Modules.MailboxFrame:ShowCharacter(charFullName)
+ if not Guda_MailboxFrame:IsShown() then
+ Guda_MailboxFrame:Show()
+ end
+ end
+ local mailboxViewChar = addon.Modules.MailboxFrame:GetCurrentViewChar()
+ info.checked = (mailboxViewChar == charFullName or (not mailboxViewChar and charFullName == addon.Modules.DB:GetPlayerFullName()))
+ UIDropDownMenu_AddButton(info)
+ end
+end
+
+-- Toggle mail dropdown
+function Guda_BagFrame_ToggleMailDropdown(button)
+ local menuFrame = getglobal("Guda_BagMailboxMenu")
+ if not menuFrame then
+ menuFrame = CreateFrame("Frame", "Guda_BagMailboxMenu", UIParent, "UIDropDownMenuTemplate")
+ end
+ UIDropDownMenu_Initialize(menuFrame, Guda_BagMailboxMenu_Initialize, "MENU")
+ ToggleDropDownMenu(1, nil, menuFrame, "cursor", 0, 0)
+end
+
+-- Bank character dropdown
+local function Guda_BagBankMenu_Initialize()
+ local characters = addon.Modules.DB:GetAllCharacters(false, true)
+ local info
+
+ for i, char in ipairs(characters) do
+ local charFullName = char.fullName
+ local charClassToken = char.classToken
+
+ -- Get class color
+ local classColor = charClassToken and RAID_CLASS_COLORS[charClassToken]
+ local r, g, b = 1, 1, 1
+ if classColor then
+ r, g, b = classColor.r, classColor.g, classColor.b
+ end
+
+ -- Create colored name
+ local charName = char.name
+ local coloredName = addon.Modules.Utils:ColorText(charName, r, g, b)
+
+ info = {}
+ info.text = coloredName
+ info.func = function()
+ Guda_BagFrame_ShowCharacterBank(charFullName, charName)
+ end
+ local bankViewChar = addon.Modules.BankFrame:GetCurrentViewChar()
+ info.checked = (bankViewChar == charFullName or (not bankViewChar and charFullName == addon.Modules.DB:GetPlayerFullName()))
+ UIDropDownMenu_AddButton(info)
end
end
-- Toggle bank dropdown
function Guda_BagFrame_ToggleBankDropdown(button)
--- Hide character dropdown if it's shown
- if characterDropdown and characterDropdown:IsShown() then
- characterDropdown:Hide()
+ local menuFrame = getglobal("Guda_BagBankMenu")
+ if not menuFrame then
+ menuFrame = CreateFrame("Frame", "Guda_BagBankMenu", UIParent, "UIDropDownMenuTemplate")
end
-
- if bankDropdown and bankDropdown:IsShown() then
- bankDropdown:Hide()
- return
- end
-
- if not bankDropdown then
- -- Create dropdown frame
- bankDropdown = CreateFrame("Frame", "Guda_BankDropdown", UIParent)
- bankDropdown:SetFrameStrata("DIALOG")
- bankDropdown:SetWidth(200)
- bankDropdown:SetBackdrop({
- bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
- edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
- tile = true,
- tileSize = 16,
- edgeSize = 16,
- insets = { left = 4, right = 4, top = 4, bottom = 4 }
- })
- bankDropdown:SetBackdropColor(0, 0, 0, 0.95)
- bankDropdown:EnableMouse(true)
- bankDropdown:Hide()
-
- bankDropdown.buttons = {}
- end
-
- -- Position dropdown below the button
- bankDropdown:ClearAllPoints()
- bankDropdown:SetPoint("TOPLEFT", button, "BOTTOMLEFT", 0, -2)
- -- Clear existing buttons
- for _, btn in ipairs(bankDropdown.buttons) do
- btn:Hide()
- end
- bankDropdown.buttons = {}
-
- -- Get all characters on current realm
- local chars = addon.Modules.DB:GetAllCharacters(false, true)
-
- local yOffset = -8
-
- -- Add character buttons
- for _, char in ipairs(chars) do
- -- Capture variables in local scope for closure
- local charFullName = char.fullName
- local charName = char.name
- local charMoney = char.money or 0
- local charClassToken = char.classToken
-
- local charButton = CreateFrame("Button", nil, bankDropdown)
- charButton:SetWidth(188)
- charButton:SetHeight(20)
- charButton:SetPoint("TOP", bankDropdown, "TOP", 0, yOffset)
-
- -- Button background on hover
- local charBg = charButton:CreateTexture(nil, "BACKGROUND")
- charBg:SetAllPoints()
- charBg:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
- charBg:SetBlendMode("ADD")
- charBg:SetAlpha(0)
-
- -- Get class color
- local classColor = charClassToken and RAID_CLASS_COLORS[charClassToken]
- local r, g, b = 1, 1, 1
- if classColor then
- r, g, b = classColor.r, classColor.g, classColor.b
- end
-
- -- Button text
- local charText = charButton:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
- charText:SetPoint("LEFT", charButton, "LEFT", 8, 0)
- charText:SetText(charName)
- charText:SetTextColor(r, g, b)
-
- -- Money text
- local moneyText = charButton:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
- moneyText:SetPoint("RIGHT", charButton, "RIGHT", -8, 0)
- moneyText:SetText(addon.Modules.Utils:FormatMoney(charMoney))
- moneyText:SetTextColor(0.7, 0.7, 0.7)
-
- -- Button scripts
- charButton:SetScript("OnEnter", function()
- charBg:SetAlpha(0.3)
- end)
- charButton:SetScript("OnLeave", function()
- charBg:SetAlpha(0)
- end)
- charButton:SetScript("OnClick", function()
- if charFullName then
- -- Show bank for this character
- Guda_BagFrame_ShowCharacterBank(charFullName, charName)
- bankDropdown:Hide()
- else
- addon:Print("Error: Character fullName is nil")
- end
- end)
-
- table.insert(bankDropdown.buttons, charButton)
- yOffset = yOffset - 20
- end
-
- -- Set dropdown height based on content
- bankDropdown:SetHeight(math.abs(yOffset) + 8)
-
- -- Show dropdown
- bankDropdown:Show()
+ UIDropDownMenu_Initialize(menuFrame, Guda_BagBankMenu_Initialize, "MENU")
+ ToggleDropDownMenu(1, nil, menuFrame, "cursor", 0, 0)
end
-- Show character's bank
@@ -2567,7 +2207,6 @@ function BagFrame:Initialize()
if bagFrame then
local originalOnMouseDown = bagFrame:GetScript("OnMouseDown")
bagFrame:SetScript("OnMouseDown", function()
- HideCharacterDropdown()
if originalOnMouseDown then
originalOnMouseDown()
end
diff --git a/UI/BagFrame.xml b/UI/BagFrame.xml
index aa887a5..68f0e85 100644
--- a/UI/BagFrame.xml
+++ b/UI/BagFrame.xml
@@ -248,6 +248,52 @@
+
+
+
@@ -654,15 +700,6 @@
-
-
-
-
-
-
-
-
-
diff --git a/UI/BankFrame.lua b/UI/BankFrame.lua
index 446fa21..9244477 100644
--- a/UI/BankFrame.lua
+++ b/UI/BankFrame.lua
@@ -7,6 +7,9 @@ local BankFrame = {}
addon.Modules.BankFrame = BankFrame
local currentViewChar = nil
+function BankFrame:GetCurrentViewChar()
+ return currentViewChar
+end
local searchText = ""
local isReadOnlyMode = false -- Track if viewing saved bank (read-only) or live bank (interactive)
local hiddenBankBags = {} -- Track which bank bags are hidden (bagID -> true/false)
@@ -78,6 +81,18 @@ function Guda_BankFrame_OnShow(self)
BankFrame:Update()
end
+-- OnHide
+function Guda_BankFrame_OnHide(self)
+ -- Close any open dropdown menus when the bank frame is hidden
+ CloseDropDownMenus()
+
+ -- Close the actual Blizzard bank too
+ local blizzardBankFrame = getglobal("BankFrame")
+ if blizzardBankFrame and blizzardBankFrame:IsShown() then
+ CloseBankFrame()
+ end
+end
+
-- Toggle visibility
function BankFrame:Toggle()
if Guda_BankFrame:IsShown() then
@@ -103,21 +118,7 @@ end
-- Update lock states of existing buttons (lightweight, used during drag)
function BankFrame:UpdateLockStates()
- for _, bankBagParent in pairs(bankBagParents) do
- if bankBagParent then
- local buttons = { bankBagParent:GetChildren() }
- for _, button in ipairs(buttons) do
- if button.hasItem ~= nil and button:IsShown() and button.bagID and button.slotID then
- -- Get live lock state
- local _, _, locked = GetContainerItemInfo(button.bagID, button.slotID)
- -- Update desaturation (gray out locked items)
- if not button.otherChar and not button.isReadOnly and SetItemButtonDesaturated then
- SetItemButtonDesaturated(button, locked, 0.5, 0.5, 0.5)
- end
- end
- end
- end
- end
+ Guda_UpdateLockStates(bankBagParents)
end
-- Update display
@@ -217,44 +218,13 @@ function BankFrame:Update()
end
end
--- Helper to get or create section header
+-- Use centralized frame helpers for section headers and bag parents
function BankFrame:GetSectionHeader(index)
- local name = "Guda_BankFrame_SectionHeader" .. index
- local header = getglobal(name)
- if not header then
- header = CreateFrame("Frame", name, getglobal("Guda_BankFrame_ItemContainer"))
- header:SetHeight(20)
- header:EnableMouse(true)
- local text = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
- text:SetPoint("LEFT", header, "LEFT", 0, 0)
- header.text = text
-
- header:SetScript("OnEnter", function()
- if this.fullName and this.isShortened then
- GameTooltip:SetOwner(this, "ANCHOR_TOP")
- GameTooltip:SetText(this.fullName)
- GameTooltip:Show()
- end
- end)
- header:SetScript("OnLeave", function()
- GameTooltip:Hide()
- end)
- end
- header.inUse = true
- return header
+ return Guda_GetSectionHeader("Guda_BankFrame", "Guda_BankFrame_ItemContainer", index)
end
--- Helper to get or create bank bag parent frame
function BankFrame:GetBagParent(bagID)
- local itemContainer = getglobal("Guda_BankFrame_ItemContainer")
- if not bankBagParents[bagID] then
- bankBagParents[bagID] = CreateFrame("Frame", "Guda_BankFrame_BagParent"..bagID, itemContainer)
- bankBagParents[bagID]:SetAllPoints(itemContainer)
- if bankBagParents[bagID].SetID then
- bankBagParents[bagID]:SetID(bagID)
- end
- end
- return bankBagParents[bagID]
+ return Guda_GetBagParent("Guda_BankFrame", bankBagParents, bagID, "Guda_BankFrame_ItemContainer")
end
-- Display items by category
@@ -1018,59 +988,8 @@ end
-- Check if item passes search filter (pfUI style)
function BankFrame:PassesSearchFilter(itemData)
- -- If no search text, everything matches
- if not self:IsSearchActive() then
- return true
- end
-
- -- Empty slots don't match when searching (pfUI style - they get dimmed)
- if not itemData then
- return false
- end
-
- -- Get item name from itemData.name or parse from link
- local itemName = itemData.name
- if not itemName and itemData.link then
- -- Parse name from item link: |cffffffff|Hitem:...|h[Item Name]|h|r
- local _, _, name = string.find(itemData.link, "%[(.+)%]")
- itemName = name
- end
-
- if not itemName then
- return false
- end
-
- -- Case-insensitive search in item name
- local search = string.lower(searchText)
-
- -- Advanced search (pfUI style categories)
- if string.sub(search, 1, 1) == "~" then
- local category = string.sub(search, 2)
- local itemType = itemData.class or ""
- local itemQuality = itemData.quality or -1
-
- if category == "equipment" or category == "armor" or category == "weapon" then
- if itemType == "Armor" or itemType == "Weapon" then return true end
- elseif category == "consumable" then
- if itemType == "Consumable" then return true end
- elseif category == "tradegoods" or category == "trades" then
- if itemType == "Trade Goods" then return true end
- elseif category == "quest" then
- local isQuest, isQuestStarter = Guda_GetQuestInfo(itemData.bagID, itemData.slotID, itemData.isBank)
- if isQuest or isQuestStarter or itemType == "Quest" then return true end
- elseif category == "reagent" then
- if itemType == "Reagent" then return true end
- elseif category == "common" then if itemQuality == 1 then return true end
- elseif category == "uncommon" then if itemQuality == 2 then return true end
- elseif category == "rare" then if itemQuality == 3 then return true end
- elseif category == "epic" then if itemQuality == 4 then return true end
- elseif category == "legendary" then if itemQuality == 5 then return true end
- end
- end
-
- itemName = string.lower(itemName)
- -- Check if item name contains search text
- return string.find(itemName, search, 1, true) ~= nil
+ if not self:IsSearchActive() then return true end
+ return Guda_PassesSearchFilter(itemData, searchText)
end
-- Search changed handler
@@ -1829,69 +1748,16 @@ function Guda_BankFrame_ClearBagButtonHighlight()
end
-- Bank character dropdown (similar to BagFrame's bank dropdown)
-local bankCharDropdown
+local function Guda_BankCharacterMenu_Initialize()
+ local characters = addon.Modules.DB:GetAllCharacters(false, true)
+ local info
+ local currentPlayerFullName = addon.Modules.DB:GetPlayerFullName()
+ local currentViewChar = addon.Modules.BankFrame:GetCurrentViewChar()
-function Guda_BankFrame_ToggleBankDropdown(button)
- if bankCharDropdown and bankCharDropdown:IsShown() then
- bankCharDropdown:Hide()
- return
- end
-
- if not bankCharDropdown then
- -- Create dropdown frame
- bankCharDropdown = CreateFrame("Frame", "Guda_BankCharDropdown", UIParent)
- bankCharDropdown:SetFrameStrata("DIALOG")
- bankCharDropdown:SetWidth(200)
- bankCharDropdown:SetBackdrop({
- bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
- edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
- tile = true,
- tileSize = 16,
- edgeSize = 16,
- insets = { left = 4, right = 4, top = 4, bottom = 4 }
- })
- bankCharDropdown:SetBackdropColor(0, 0, 0, 0.95)
- bankCharDropdown:EnableMouse(true)
- bankCharDropdown:Hide()
-
- bankCharDropdown.buttons = {}
- end
-
- -- Position dropdown below the button
- bankCharDropdown:ClearAllPoints()
- bankCharDropdown:SetPoint("TOPLEFT", button, "BOTTOMLEFT", 0, -2)
-
- -- Clear existing buttons
- for _, btn in ipairs(bankCharDropdown.buttons) do
- btn:Hide()
- end
- bankCharDropdown.buttons = {}
-
- -- Get all characters on current realm
- local chars = addon.Modules.DB:GetAllCharacters(false, true)
-
- local yOffset = -8
-
- -- Add character buttons
- for _, char in ipairs(chars) do
- -- Capture variables in local scope for closure
+ for i, char in ipairs(characters) do
local charFullName = char.fullName
- local charName = char.name
- local charMoney = char.money or 0
local charClassToken = char.classToken
-
- local charButton = CreateFrame("Button", nil, bankCharDropdown)
- charButton:SetWidth(188)
- charButton:SetHeight(20)
- charButton:SetPoint("TOP", bankCharDropdown, "TOP", 0, yOffset)
-
- -- Button background on hover
- local charBg = charButton:CreateTexture(nil, "BACKGROUND")
- charBg:SetAllPoints()
- charBg:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
- charBg:SetBlendMode("ADD")
- charBg:SetAlpha(0)
-
+
-- Get class color
local classColor = charClassToken and RAID_CLASS_COLORS[charClassToken]
local r, g, b = 1, 1, 1
@@ -1899,44 +1765,30 @@ function Guda_BankFrame_ToggleBankDropdown(button)
r, g, b = classColor.r, classColor.g, classColor.b
end
- -- Button text
- local charText = charButton:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
- charText:SetPoint("LEFT", charButton, "LEFT", 8, 0)
- charText:SetText(charName)
- charText:SetTextColor(r, g, b)
+ -- Create colored name
+ local coloredName = addon.Modules.Utils:ColorText(char.name, r, g, b)
- -- Money text
- local moneyText = charButton:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
- moneyText:SetPoint("RIGHT", charButton, "RIGHT", -8, 0)
- moneyText:SetText(addon.Modules.Utils:FormatMoney(charMoney))
- moneyText:SetTextColor(0.7, 0.7, 0.7)
-
- -- Button scripts
- charButton:SetScript("OnEnter", function()
- charBg:SetAlpha(0.3)
- end)
- charButton:SetScript("OnLeave", function()
- charBg:SetAlpha(0)
- end)
- charButton:SetScript("OnClick", function()
- if charFullName then
- -- Show bank for this character using the BankFrame module
- addon.Modules.BankFrame:ShowCharacter(charFullName)
- bankCharDropdown:Hide()
+ info = {}
+ info.text = coloredName
+ info.func = function()
+ if charFullName == currentPlayerFullName then
+ addon.Modules.BankFrame:ShowCurrentCharacter()
else
- addon:Print("Error: Character fullName is nil")
+ addon.Modules.BankFrame:ShowCharacter(charFullName)
end
- end)
-
- table.insert(bankCharDropdown.buttons, charButton)
- yOffset = yOffset - 20
+ end
+ info.checked = (currentViewChar == charFullName or (not currentViewChar and charFullName == currentPlayerFullName))
+ UIDropDownMenu_AddButton(info)
end
+end
- -- Set dropdown height based on content
- bankCharDropdown:SetHeight(math.abs(yOffset) + 8)
-
- -- Show dropdown
- bankCharDropdown:Show()
+function Guda_BankFrame_ToggleBankDropdown(button)
+ local menuFrame = getglobal("Guda_BankCharacterMenu")
+ if not menuFrame then
+ menuFrame = CreateFrame("Frame", "Guda_BankCharacterMenu", UIParent, "UIDropDownMenuTemplate")
+ end
+ UIDropDownMenu_Initialize(menuFrame, Guda_BankCharacterMenu_Initialize, "MENU")
+ ToggleDropDownMenu(1, nil, menuFrame, "cursor", 0, 0)
end
-- Drag handlers for bank bag slots
diff --git a/UI/BankFrame.xml b/UI/BankFrame.xml
index 0d22445..225f974 100644
--- a/UI/BankFrame.xml
+++ b/UI/BankFrame.xml
@@ -573,15 +573,6 @@
-
-
-
-
-
-
-
-
-
-- Clear search focus when clicking in item area
@@ -602,11 +593,7 @@
Guda_BankFrame_OnShow(this)
- Guda_BankFrame:Hide()
- -- Close the actual Blizzard bank too
- if BankFrame and BankFrame:IsShown() then
- CloseBankFrame()
- end
+ Guda_BankFrame_OnHide(this)
-- Clear search box focus when clicking on bank frame
diff --git a/UI/FrameHelpers.lua b/UI/FrameHelpers.lua
new file mode 100644
index 0000000..91466da
--- /dev/null
+++ b/UI/FrameHelpers.lua
@@ -0,0 +1,209 @@
+-- FrameHelpers: central utilities for frame headers and bag parents
+local addon = Guda
+
+local FrameHelpers = {}
+addon.Modules.FrameHelpers = FrameHelpers
+
+-- Create or return a section header for a given frame prefix and container
+function Guda_GetSectionHeader(framePrefix, containerName, index)
+ local name = framePrefix .. "_SectionHeader" .. index
+ local header = getglobal(name)
+ if not header then
+ local container = getglobal(containerName)
+ header = CreateFrame("Frame", name, container)
+ header:SetHeight(20)
+ header:EnableMouse(true)
+ local text = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
+ text:SetPoint("LEFT", header, "LEFT", 0, 0)
+ header.text = text
+
+ header:SetScript("OnEnter", function()
+ if this.fullName and this.isShortened then
+ GameTooltip:SetOwner(this, "ANCHOR_TOP")
+ GameTooltip:SetText(this.fullName)
+ GameTooltip:Show()
+ end
+ end)
+ header:SetScript("OnLeave", function()
+ GameTooltip:Hide()
+ end)
+ end
+ header.inUse = true
+ return header
+end
+
+-- Create or return a bag parent frame for a frame prefix and bag parents table
+function Guda_GetBagParent(framePrefix, parentsTable, bagID, containerName)
+ local container = getglobal(containerName)
+ if not parentsTable[bagID] then
+ local name = framePrefix .. "_BagParent" .. bagID
+ parentsTable[bagID] = CreateFrame("Frame", name, container)
+ parentsTable[bagID]:SetAllPoints(container)
+ if parentsTable[bagID].SetID then
+ parentsTable[bagID]:SetID(bagID)
+ end
+ end
+ return parentsTable[bagID]
+end
+
+-- Update lock/desaturation states for a table of parent frames
+function Guda_UpdateLockStates(parentsTable)
+ if not parentsTable then return end
+ for _, parent in pairs(parentsTable) do
+ if parent then
+ local buttons = { parent:GetChildren() }
+ for _, button in ipairs(buttons) do
+ if button.hasItem ~= nil and button:IsShown() and button.bagID and button.slotID then
+ local ok, name, texture, count, quality, canUse = pcall(function() return GetContainerItemInfo(button.bagID, button.slotID) end)
+ local _, _, locked = nil, nil, nil
+ if ok then
+ -- older GetContainerItemInfo returns texture, count, locked etc in different orders; try to call a safe wrapper if available
+ local infoOk, iName, iTexture, iCount, iQuality, iCanUse, iLocked = pcall(function() return GetContainerItemInfo(button.bagID, button.slotID) end)
+ if infoOk then
+ -- Try to find 'locked' boolean among returned values (best-effort)
+ for _, val in ipairs({iName, iTexture, iCount, iQuality, iCanUse, iLocked}) do
+ if type(val) == "boolean" then locked = val; break end
+ end
+ end
+ end
+ if not button.otherChar and not button.isReadOnly and SetItemButtonDesaturated and locked ~= nil then
+ SetItemButtonDesaturated(button, locked, 0.5, 0.5, 0.5)
+ end
+ end
+ end
+ end
+ end
+end
+
+-- Shared search filter used by BagFrame and BankFrame
+function Guda_PassesSearchFilter(itemData, searchText)
+ -- If no search text, everything matches
+ if not searchText or searchText == "" then
+ return true
+ end
+
+ -- Ignore common placeholders
+ if searchText == "Search, try ~equipment" or searchText == "Search bank..." then
+ return true
+ end
+
+ -- Empty slots don't match when searching
+ if not itemData then
+ return false
+ end
+
+ local itemName = itemData.name
+ if not itemName and itemData.link then
+ local _, _, name = string.find(itemData.link, "%[(.+)%]")
+ itemName = name
+ end
+
+ if not itemName then return false end
+
+ local search = string.lower(searchText)
+
+ if string.sub(search, 1, 1) == "~" then
+ local category = string.sub(search, 2)
+ local itemType = itemData.class or ""
+ local itemQuality = itemData.quality or -1
+
+ if category == "equipment" or category == "armor" or category == "weapon" then
+ if itemType == "Armor" or itemType == "Weapon" then return true end
+ elseif category == "consumable" then
+ if itemType == "Consumable" then return true end
+ elseif category == "tradegoods" or category == "trades" then
+ if itemType == "Trade Goods" then return true end
+ elseif category == "quest" then
+ local isQuest, isQuestStarter = Guda_GetQuestInfo(itemData.bagID, itemData.slotID, itemData.isBank)
+ if isQuest or isQuestStarter or itemType == "Quest" then return true end
+ elseif category == "reagent" then
+ if itemType == "Reagent" then return true end
+ elseif category == "common" then if itemQuality == 1 then return true end
+ elseif category == "uncommon" then if itemQuality == 2 then return true end
+ elseif category == "rare" then if itemQuality == 3 then return true end
+ elseif category == "epic" then if itemQuality == 4 then return true end
+ elseif category == "legendary" then if itemQuality == 5 then return true end
+ end
+ end
+
+ itemName = string.lower(itemName)
+ return string.find(itemName, string.lower(searchText), 1, true) ~= nil
+end
+
+-- Generic ResizeFrame for Bag/Bank frames
+function Guda_ResizeFrame(frameName, containerName, currentRow, currentCol, columns, overrideHeight)
+ local buttonSize = addon.Modules.DB:GetSetting("iconSize") or addon.Constants.BUTTON_SIZE
+ local spacing = addon.Modules.DB:GetSetting("iconSpacing") or addon.Constants.BUTTON_SPACING
+
+ local totalRows = (currentRow or 0) + 1
+ if totalRows < 1 then totalRows = 1 end
+
+ local containerWidth = (columns * (buttonSize + spacing)) + 20
+ local containerHeight = overrideHeight or ((totalRows * (buttonSize + spacing)) + 20)
+ local frameWidth = containerWidth + 20
+
+ local showSearchBar = addon.Modules.DB:GetSetting("showSearchBar")
+ if showSearchBar == nil then showSearchBar = true end
+
+ local titleHeight = 40
+ local searchBarHeight = 30
+ local footerHeight = 45
+ local frameHeight
+
+ local hideFooter = addon.Modules.DB:GetSetting("hideFooter")
+ if hideFooter then
+ footerHeight = 10
+ frameHeight = containerHeight + titleHeight + (showSearchBar and searchBarHeight or 0) + footerHeight
+ elseif showSearchBar then
+ frameHeight = containerHeight + titleHeight + searchBarHeight + footerHeight
+ else
+ frameHeight = containerHeight + titleHeight + footerHeight
+ end
+
+ if containerWidth < 200 then
+ containerWidth = 200
+ frameWidth = 220
+ end
+ if containerHeight < 150 then containerHeight = 150 end
+ if frameHeight < 250 then frameHeight = 250 end
+
+ if containerWidth > 1250 then containerWidth = 1250; frameWidth = 1270 end
+ if containerHeight > 1000 then containerHeight = 1000 end
+ if frameHeight > 1200 then frameHeight = 1200 end
+
+ local frame = getglobal(frameName)
+ local itemContainer = getglobal(containerName)
+
+ if frame then
+ frame:SetWidth(frameWidth)
+ frame:SetHeight(frameHeight)
+ frame:ClearAllPoints()
+ -- Try to preserve saved position if present (saved only for Bag frame)
+ if addon and addon.Modules and addon.Modules.DB then
+ local settingName = (frameName == "Guda_BagFrame") and "bagFramePosition" or nil
+ if settingName then
+ local pos = addon.Modules.DB:GetSetting(settingName)
+ if pos and pos.point == "BOTTOMRIGHT" and pos.x and pos.y then
+ frame:SetPoint("BOTTOMRIGHT", "UIParent", "BOTTOMRIGHT", pos.x, pos.y)
+ else
+ frame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -50, 100)
+ end
+ else
+ frame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -50, 100)
+ end
+ else
+ frame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -50, 100)
+ end
+ end
+
+ if itemContainer then
+ itemContainer:SetWidth(containerWidth)
+ itemContainer:SetHeight(containerHeight)
+ end
+
+ -- Resize search bar and toolbar to match container width
+ local searchBar = getglobal(frameName .. "_SearchBar")
+ if searchBar then searchBar:SetWidth(containerWidth) end
+ local toolbar = getglobal(frameName .. "_Toolbar")
+ if toolbar then toolbar:SetWidth(containerWidth) end
+end
diff --git a/UI/ItemButton.lua b/UI/ItemButton.lua
index db454a7..96e1b0e 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
@@ -377,6 +381,15 @@ function Guda_ItemButton_OnLoad(self)
-- Default behavior
if ContainerFrameItemButton_OnClick then
+ -- Mailbox clicks should be ignored except for Ctrl+Click (preview)
+ -- OR if it's a live mail item for the current player and Shift+Click (loot)
+ if this.isMail then
+ if IsControlKeyDown() then
+ ContainerFrameItemButton_OnClick(arg1)
+ end
+ return
+ end
+
ContainerFrameItemButton_OnClick(arg1)
end
end)
@@ -425,6 +438,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
@@ -447,13 +464,20 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
self.bagID = bagID
self.slotID = slotID
-- Also set the Blizzard slot ID for compatibility with ContainerFrameItemButtonTemplate behavior
- if self.SetID and slotID then
- self:SetID(slotID)
+ -- ALWAYS set ID to something (0 if nil) to avoid leaking old IDs when button is reused
+ if self.SetID then
+ self:SetID(slotID or 0)
end
+ -- Explicitly set bag index to avoid Blizzard's ContainerFrameItemButton_OnEnter logic
+ -- from picking up this button as part of a real bag.
+ self.bagIndex = bagID or -100 -- Use an invalid bag index for non-bag buttons
self.itemData = itemData
self.isBank = isBank or false
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
@@ -481,6 +505,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")
@@ -538,7 +564,17 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
-- Apply the determined texture and count
if self.hasItem then
- if SetItemButtonTexture then SetItemButtonTexture(self, displayTexture) end
+ if SetItemButtonTexture then
+ SetItemButtonTexture(self, displayTexture)
+ end
+
+ -- Explicitly set and show icon texture as SetItemButtonTexture can be unreliable for custom paths in 1.12
+ local iconTexture = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon
+ if iconTexture and displayTexture then
+ iconTexture:SetTexture(displayTexture)
+ iconTexture:Show()
+ end
+
if SetItemButtonCount then SetItemButtonCount(self, displayCount or 1) end
if emptySlotBg then emptySlotBg:Hide() end
-- Update cooldown overlay for live items
@@ -625,6 +661,9 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
-- For 1.12.1 compatibility, access textures using both methods and proper naming
-- We'll set the NormalTexture later based on whether slot is empty or filled
+ -- ICON TEXTURE: Ensure we use the correct name ($parentIconTexture is standard)
+ local iconTexture = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon
+
-- Pushed texture follows the button size/position
local pushedTexture = getglobal(self:GetName().."PushedTexture")
if not pushedTexture and self.GetPushedTexture then
@@ -744,7 +783,7 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
-- Special border for keyring items (cyan/blue)
self.qualityBorder:SetBackdropBorderColor(0.2, 0.8, 1.0, 1)
self.qualityBorder:Show()
- elseif itemQuality and itemLink then
+ elseif itemQuality then
-- Check settings to determine if we should show borders (nil-safe)
local showEquipmentBorder, showOtherBorder
if addon and addon.Modules and addon.Modules.DB and addon.Modules.DB.GetSetting then
@@ -765,10 +804,12 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
-- Check if item is equipment (nil-safe)
local isEquipment = false
- if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.IsEquipment then
- isEquipment = addon.Modules.Utils:IsEquipment(itemLink)
- elseif Guda and Guda.Modules and Guda.Modules.Utils and Guda.Modules.Utils.IsEquipment then
- isEquipment = Guda.Modules.Utils:IsEquipment(itemLink)
+ if itemLink then
+ if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.IsEquipment then
+ isEquipment = addon.Modules.Utils:IsEquipment(itemLink)
+ elseif Guda and Guda.Modules and Guda.Modules.Utils and Guda.Modules.Utils.IsEquipment then
+ isEquipment = Guda.Modules.Utils:IsEquipment(itemLink)
+ end
end
-- Determine if we should show the border based on item type and settings
@@ -920,8 +961,8 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
self.questIcon:ClearAllPoints()
self.questIcon:SetPoint("TOPRIGHT", self, "TOPRIGHT", 1, 0)
end
- else
- -- Hide icon for empty slots
+ elseif not self.isMail then
+ -- Hide icon for empty slots, but keep it for mailbox custom icons
iconTexture:Hide()
end
end
@@ -929,7 +970,7 @@ end
-- OnEnter handler (show tooltip)
function Guda_ItemButton_OnEnter(self)
--- 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
@@ -941,7 +982,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
@@ -949,7 +990,71 @@ 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()
+ local isMailboxOpen = addon.Modules.MailboxScanner and addon.Modules.MailboxScanner:IsMailboxOpen()
+
+ if (not self.otherChar or self.otherChar == currentPlayerName) and self.mailIndex and isMailboxOpen then
+ -- Live mailbox for current character (only when mailbox is actually open)
+ GameTooltip:SetInboxItem(self.mailIndex, self.mailItemIndex or 1)
+ elseif self.itemData and (self.itemData.link or self.itemData.itemID) then
+ -- Read-only / other character mailbox OR current character mailbox when closed
+ GameTooltip.GudaViewedCharacter = self.otherChar or currentPlayerName
+ if self.itemData.link then
+ GameTooltip:SetHyperlink(self.itemData.link)
+ else
+ GameTooltip:SetHyperlink("item:" .. self.itemData.itemID .. ":0:0:0")
+ end
+ elseif self.itemData and self.itemData.name then
+ -- Money or generic mail
+ GameTooltip:AddLine(self.itemData.name, 1, 1, 1)
+ elseif self.mailData and self.mailData.money and self.mailData.money > 0 then
+ -- Fallback for money only mail
+ GameTooltip:AddLine("Money", 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
+
+ -- Add Inventory counts for mailbox items at the very bottom
+ if addon.Modules.Tooltip and addon.Modules.Tooltip.AddInventoryInfo then
+ local link = self.itemData and (self.itemData.link or (self.itemData.itemID and ("item:" .. self.itemData.itemID .. ":0:0:0")))
+ if not link and self.mailIndex and isMailboxOpen then
+ link = addon.Modules.Utils:GetInboxItemLink(self.mailIndex, self.mailItemIndex or 1)
+ end
+ if link then
+ addon.Modules.Tooltip:AddInventoryInfo(GameTooltip, link)
+ 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)
@@ -1017,8 +1122,9 @@ end
-- OnLeave handler
function Guda_ItemButton_OnLeave(self)
-- Clear any viewed character hint on the tooltip when leaving
- if GameTooltip and GameTooltip.GudaViewedCharacter then
+ if GameTooltip then
GameTooltip.GudaViewedCharacter = nil
+ GameTooltip.GudaInventoryAdded = nil
end
GameTooltip:Hide()
ResetCursor()
diff --git a/UI/MailboxFrame.lua b/UI/MailboxFrame.lua
new file mode 100644
index 0000000..9738ed3
--- /dev/null
+++ b/UI/MailboxFrame.lua
@@ -0,0 +1,376 @@
+-- Mailbox Frame
+-- Mailbox viewing UI
+
+local addon = Guda
+
+local MailboxFrame = {}
+addon.Modules.MailboxFrame = MailboxFrame
+
+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
+function Guda_MailboxFrame_OnLoad(self)
+ -- Apply border visibility setting
+ if MailboxFrame.UpdateBorderVisibility then
+ MailboxFrame:UpdateBorderVisibility()
+ end
+
+ -- Set up initial backdrop
+ -- (No longer needed here as UpdateBorderVisibility handles it)
+
+ -- Set up search box placeholder
+ local searchBox = getglobal(self:GetName().."_SearchBar_SearchBox")
+ if searchBox then
+ searchBox:SetText("Search mailbox...")
+ searchBox:SetTextColor(0.5, 0.5, 0.5, 1)
+ end
+
+ -- Create invisible full-screen frame to catch clicks outside the mailbox frame while typing in search
+ if not mailboxClickCatcher then
+ mailboxClickCatcher = CreateFrame("Frame", "Guda_MailboxClickCatcher", UIParent)
+ mailboxClickCatcher:SetFrameStrata("BACKGROUND")
+ mailboxClickCatcher:SetAllPoints(UIParent)
+ mailboxClickCatcher:EnableMouse(true)
+ mailboxClickCatcher:Hide()
+
+ mailboxClickCatcher:SetScript("OnMouseDown", function()
+ if Guda_MailboxFrame_ClearSearch then
+ Guda_MailboxFrame_ClearSearch()
+ end
+ end)
+ end
+end
+
+-- Clear search focus
+function Guda_MailboxFrame_ClearSearch()
+ local searchBox = getglobal("Guda_MailboxFrame_SearchBar_SearchBox")
+ if searchBox then
+ searchBox:ClearFocus()
+ end
+end
+
+-- OnShow
+function Guda_MailboxFrame_OnShow(self)
+ -- Apply border visibility setting
+ if MailboxFrame.UpdateBorderVisibility then
+ MailboxFrame:UpdateBorderVisibility()
+ end
+
+ -- Apply frame transparency
+ if Guda_ApplyBackgroundTransparency then
+ Guda_ApplyBackgroundTransparency()
+ end
+
+ MailboxFrame:Update()
+end
+
+-- Update border visibility
+function MailboxFrame:UpdateBorderVisibility()
+ local hideBorders = addon.Modules.DB:GetSetting("hideBorders")
+ if hideBorders then
+ Guda:ApplyBackdrop(Guda_MailboxFrame, "MINIMALIST_BORDER", "DEFAULT")
+ else
+ Guda:ApplyBackdrop(Guda_MailboxFrame, "DEFAULT_FRAME", "DEFAULT")
+ end
+end
+
+-- OnHide
+function Guda_MailboxFrame_OnHide(self)
+ -- Close any open dropdown menus when the mailbox frame is hidden
+ CloseDropDownMenus()
+end
+
+-- Toggle visibility
+function MailboxFrame:Toggle()
+ if Guda_MailboxFrame:IsShown() then
+ Guda_MailboxFrame:Hide()
+ else
+ Guda_MailboxFrame:Show()
+ end
+end
+
+-- Show specific character's mailbox
+function MailboxFrame:GetCurrentViewChar()
+ return currentViewChar
+end
+
+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
+end
+
+-- Update the mailbox frame
+function MailboxFrame:Update()
+ if not Guda_MailboxFrame:IsShown() then return end
+
+ -- Determine which character to show
+ local charFullName = currentViewChar or addon.Modules.DB:GetPlayerFullName()
+ local mailboxData = addon.Modules.DB:GetCharacterMailbox(charFullName)
+
+ -- Extract character name from fullName
+ local charName = charFullName
+ local dashPos = string.find(charFullName, "-")
+ if dashPos then
+ charName = string.sub(charFullName, 1, dashPos - 1)
+ end
+
+ getglobal("Guda_MailboxFrame_Title"):SetText(charName .. "'s Mailbox")
+
+ -- Filter items based on search text
+ local filteredItems = {}
+ local totalItems = table.getn(mailboxData)
+ for i, mail in ipairs(mailboxData) do
+ local matchesSearch = true
+ if searchText ~= "" then
+ matchesSearch = false
+ if mail.sender and string.find(string.lower(mail.sender), searchText) then
+ matchesSearch = true
+ elseif mail.subject and string.find(string.lower(mail.subject), searchText) then
+ matchesSearch = true
+ 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)
+ end
+ end
+
+ -- Display items
+ local emptyMessage = getglobal("Guda_MailboxFrame_EmptyMessage")
+ if emptyMessage then
+ if totalItems == 0 then
+ emptyMessage:SetText("No mailbox data found for this character.\n\nVisit a mailbox in-game to save your mail data.")
+ emptyMessage:Show()
+ else
+ emptyMessage:Hide()
+ end
+ end
+
+ self:DisplayItems(filteredItems, charFullName, totalItems)
+end
+
+-- Display mailbox items in a list
+function MailboxFrame:DisplayItems(items, charFullName, totalMails)
+ local container = getglobal("Guda_MailboxFrame_ItemContainer")
+
+ -- Hide all existing rows first
+ for _, row in pairs(mailboxRows) do
+ row:Hide()
+ end
+
+ local totalItems = table.getn(items)
+ local totalPages = math.max(1, math.ceil(totalItems / ITEMS_PER_PAGE))
+
+ 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
+
+ row:ClearAllPoints()
+ row:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -(rowIndex - 1) * 55)
+
+ -- Row Money Frame
+ local moneyFrame = getglobal(row:GetName() .. "_MoneyFrame")
+ if moneyFrame then
+ -- Ensure row money frame doesn't auto-update to character money
+ moneyFrame.moneyType = "STATIC"
+ moneyFrame:UnregisterAllEvents()
+ moneyFrame:SetScript("OnShow", nil) -- Disable Blizzard's auto-update OnShow
+ moneyFrame:SetScript("OnEvent", nil)
+
+ -- Restore right alignment for the money frame to avoid overlap with icon
+ moneyFrame:ClearAllPoints()
+ moneyFrame:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", -10, 8)
+ end
+
+ if (mail.money or 0) > 0 then
+ moneyFrame:Show()
+ MoneyFrame_Update(moneyFrame:GetName(), mail.money)
+ else
+ if moneyFrame then moneyFrame:Hide() end
+ end
+
+ -- Fill row data
+ local sender = mail.sender or "Unknown"
+ getglobal(row:GetName() .. "_Sender"):SetText(sender)
+
+ local subject = mail.subject or "No Subject"
+ if mail.item and mail.item.quality then
+ local r, g, b = addon.Modules.Utils:GetQualityColor(mail.item.quality)
+ subject = addon.Modules.Utils:ColorText(subject, r, g, b)
+ end
+ getglobal(row:GetName() .. "_Subject"):SetText(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
+
+ -- Center vertically in the row
+ if itemButton then
+ itemButton:ClearAllPoints()
+ itemButton:SetPoint("LEFT", row, "LEFT", 10, 0)
+ end
+
+ if mail.item and (mail.item.texture or mail.item.link) then
+ 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
+ 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")
+ elseif mail.packageIcon then
+ icon:SetTexture(mail.packageIcon)
+ else
+ icon:SetTexture("Interface\\Icons\\INV_Letter_15")
+ end
+ icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
+ icon:Show()
+ end
+ if itemButton.qualityBorder then itemButton.qualityBorder:Hide() end
+ end
+
+ row:Show()
+ rowIndex = rowIndex + 1
+ end
+
+ -- 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 pagination text
+ local paginationText = getglobal("Guda_MailboxFrame_Pagination_Text")
+ if paginationText then
+ if totalMails == 0 then
+ paginationText:SetText("No Mail")
+ else
+ paginationText:SetText(string.format("%d/%d (items: %d)", currentPage, totalPages, totalMails or 0))
+ end
+ end
+end
+
+-- Search text changed
+function Guda_MailboxFrame_OnSearchTextChanged()
+ local searchBox = getglobal("Guda_MailboxFrame_SearchBar_SearchBox")
+ local text = searchBox:GetText()
+ if text == "Search mailbox..." then
+ searchText = ""
+ else
+ searchText = string.lower(text)
+ end
+ currentPage = 1
+ MailboxFrame:Update()
+end
+
+-- Show character selection menu
+local function Guda_MailboxCharacterMenu_Initialize()
+ local characters = addon.Modules.DB:GetAllCharacters(true, true)
+ local info
+
+ for i, char in ipairs(characters) do
+ local charFullName = char.fullName
+ local charClassToken = char.classToken
+
+ -- Get class color
+ local classColor = charClassToken and RAID_CLASS_COLORS[charClassToken]
+ local r, g, b = 1, 1, 1
+ if classColor then
+ r, g, b = classColor.r, classColor.g, classColor.b
+ end
+
+ -- Create colored name
+ local coloredName = addon.Modules.Utils:ColorText(char.name, r, g, b)
+
+ info = {}
+ info.text = coloredName
+ info.func = function() MailboxFrame:ShowCharacter(charFullName) end
+ info.checked = (currentViewChar == char.fullName or (not currentViewChar and char.fullName == addon.Modules.DB:GetPlayerFullName()))
+ UIDropDownMenu_AddButton(info)
+ end
+end
+
+function Guda_MailboxFrame_ShowCharacterMenu()
+ local menuFrame = getglobal("Guda_MailboxCharacterMenu")
+ if not menuFrame then
+ menuFrame = CreateFrame("Frame", "Guda_MailboxCharacterMenu", UIParent, "UIDropDownMenuTemplate")
+ end
+ UIDropDownMenu_Initialize(menuFrame, Guda_MailboxCharacterMenu_Initialize, "MENU")
+ ToggleDropDownMenu(1, nil, menuFrame, "cursor", 0, 0)
+end
diff --git a/UI/MailboxFrame.xml b/UI/MailboxFrame.xml
new file mode 100644
index 0000000..94d1590
--- /dev/null
+++ b/UI/MailboxFrame.xml
@@ -0,0 +1,355 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Guda_MailboxFrame_OnSearchTextChanged()
+
+
+ this:ClearFocus()
+
+
+ this:ClearFocus()
+
+
+ if this:GetText() == "Search mailbox..." then
+ this:SetText("")
+ this:SetTextColor(1, 1, 1, 1)
+ end
+ if Guda_MailboxClickCatcher then
+ Guda_MailboxClickCatcher:Show()
+ end
+
+
+ if this:GetText() == "" then
+ this:SetText("Search mailbox...")
+ this:SetTextColor(0.5, 0.5, 0.5, 1)
+ end
+ if Guda_MailboxClickCatcher then
+ Guda_MailboxClickCatcher:Hide()
+ end
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Guda_MailboxFrame_OnLoad(this)
+
+
+ Guda_MailboxFrame_OnShow(this)
+
+
+ Guda_MailboxFrame_OnHide(this)
+
+
+ this:StartMoving()
+
+
+ this:StopMovingOrSizing()
+
+
+
+
diff --git a/UI/SettingsPopup.lua b/UI/SettingsPopup.lua
index 56f4030..a57b499 100644
--- a/UI/SettingsPopup.lua
+++ b/UI/SettingsPopup.lua
@@ -368,7 +368,7 @@ function Guda_ApplyBackgroundTransparency()
local transparency = Guda.Modules.DB:GetSetting("bgTransparency") or 0.15
local alpha = 1.0 - transparency
- local frames = { "Guda_BagFrame", "Guda_BankFrame", "Guda_SettingsPopup", "Guda_QuestItemBar" }
+ local frames = { "Guda_BagFrame", "Guda_BankFrame", "Guda_MailboxFrame", "Guda_SettingsPopup", "Guda_QuestItemBar" }
for _, frameName in ipairs(frames) do
local frame = getglobal(frameName)
if frame then
@@ -603,6 +603,15 @@ function Guda_SettingsPopup_HideBordersCheckbox_OnClick(self)
end
end
+ local mailboxFrame = getglobal("Guda_MailboxFrame")
+ if mailboxFrame then
+ if isChecked then
+ Guda:ApplyBackdrop(mailboxFrame, "MINIMALIST_BORDER", "DEFAULT")
+ else
+ Guda:ApplyBackdrop(mailboxFrame, "DEFAULT_FRAME", "DEFAULT")
+ end
+ end
+
-- Update border visibility on settings frame
if SettingsPopup.UpdateBorderVisibility then
SettingsPopup:UpdateBorderVisibility()