Merge pull request #18 from vatichild/feat/mailbox-ui

feat: mailboxframe
This commit is contained in:
Vati
2025-12-28 04:37:10 +04:00
committed by GitHub
19 changed files with 2126 additions and 869 deletions
Binary file not shown.
+116
View File
@@ -131,6 +131,7 @@ function DB:Initialize()
money = 0, money = 0,
bags = {}, bags = {},
bank = {}, bank = {},
mailbox = {}, -- Add mailbox storage
equipped = {}, -- Add equipped items storage equipped = {}, -- Add equipped items storage
character = {}, -- Add character info storage character = {}, -- Add character info storage
lastUpdate = time(), lastUpdate = time(),
@@ -153,6 +154,10 @@ function DB:Initialize()
char.character = {} char.character = {}
addon:Debug("Added character field to existing character") addon:Debug("Added character field to existing character")
end end
if not char.mailbox then
char.mailbox = {}
addon:Debug("Added mailbox field to existing character")
end
end end
addon:Debug("Database initialized for %s", fullName) addon:Debug("Database initialized for %s", fullName)
@@ -215,6 +220,58 @@ function DB:SaveMoney(copper)
end end
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) -- Get all characters (optionally filter by faction and/or realm)
function DB:GetAllCharacters(sameFactionOnly, currentRealmOnly) function DB:GetAllCharacters(sameFactionOnly, currentRealmOnly)
local chars = {} local chars = {}
@@ -256,6 +313,65 @@ function DB:GetCharacterBank(fullName)
return char and char.bank or {} return char and char.bank or {}
end 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 -- Get character's equipped items
function DB:GetCharacterEquipped(fullName) function DB:GetCharacterEquipped(fullName)
local char = Guda_DB.characters[fullName] local char = Guda_DB.characters[fullName]
+8
View File
@@ -79,3 +79,11 @@ end
function Events:OnPlayerLogout(callback, owner) function Events:OnPlayerLogout(callback, owner)
self:Register("PLAYER_LOGOUT", callback, owner) self:Register("PLAYER_LOGOUT", callback, owner)
end 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
+2
View File
@@ -102,11 +102,13 @@ addon.Modules = {
Tooltip = {}, Tooltip = {},
BagScanner = {}, BagScanner = {},
BankScanner = {}, BankScanner = {},
MailboxScanner = {},
MoneyTracker = {}, MoneyTracker = {},
EquipmentScanner = {}, EquipmentScanner = {},
SortEngine = {}, SortEngine = {},
BagFrame = {}, BagFrame = {},
BankFrame = {}, BankFrame = {},
MailboxFrame = {},
QuestItemBar = {}, QuestItemBar = {},
TrackedItemBar = {}, TrackedItemBar = {},
SettingsPopup = {}, SettingsPopup = {},
+7
View File
@@ -18,6 +18,7 @@ function Main:Initialize()
-- Initialize scanners -- Initialize scanners
addon.Modules.BagScanner:Initialize() addon.Modules.BagScanner:Initialize()
addon.Modules.BankScanner:Initialize() addon.Modules.BankScanner:Initialize()
addon.Modules.MailboxScanner:Initialize()
addon.Modules.MoneyTracker:Initialize() addon.Modules.MoneyTracker:Initialize()
addon.Modules.EquipmentScanner:Initialize() addon.Modules.EquipmentScanner:Initialize()
@@ -25,6 +26,7 @@ function Main:Initialize()
addon:Print("Initializing UI...") addon:Print("Initializing UI...")
addon.Modules.BagFrame:Initialize() addon.Modules.BagFrame:Initialize()
addon.Modules.BankFrame:Initialize() addon.Modules.BankFrame:Initialize()
addon.Modules.MailboxFrame:Initialize()
addon:Debug("Checking QuestItemBar module...") addon:Debug("Checking QuestItemBar module...")
if addon.Modules.QuestItemBar and addon.Modules.QuestItemBar.isLoaded then if addon.Modules.QuestItemBar and addon.Modules.QuestItemBar.isLoaded then
@@ -103,6 +105,10 @@ function Main:SetupSlashCommands()
-- Toggle bank -- Toggle bank
addon.Modules.BankFrame:Toggle() addon.Modules.BankFrame:Toggle()
elseif msg == "mail" or msg == "mailbox" then
-- Toggle mailbox
addon.Modules.MailboxFrame:Toggle()
elseif msg == "sort" then elseif msg == "sort" then
-- Sort bags -- Sort bags
addon.Modules.SortEngine:SortBags() addon.Modules.SortEngine:SortBags()
@@ -143,6 +149,7 @@ function Main:SetupSlashCommands()
addon:Print("Commands:") addon:Print("Commands:")
addon:Print("/guda - Toggle bags") addon:Print("/guda - Toggle bags")
addon:Print("/guda bank - Toggle bank") addon:Print("/guda bank - Toggle bank")
addon:Print("/guda mail - Toggle mailbox")
addon:Print("/guda sort - Sort bags") addon:Print("/guda sort - Sort bags")
addon:Print("/guda sortbank - Sort bank") addon:Print("/guda sortbank - Sort bank")
addon:Print("/guda track - Toggle item tracking") addon:Print("/guda track - Toggle item tracking")
+146 -7
View File
@@ -7,12 +7,16 @@ addon.Modules.Tooltip = Tooltip
-- Helper function to get item ID from link (Lua 5.0 compatible) -- Helper function to get item ID from link (Lua 5.0 compatible)
local function GetItemIDFromLink(link) local function GetItemIDFromLink(link)
if not link then return nil end 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 return itemID and tonumber(itemID) or nil
end end
local function CountCurrentCharacterItems(itemID) local function CountCurrentCharacterItems(itemID)
local bagCount = 0 local bagCount = 0
local bankCount = 0 local bankCount = 0
local mailCount = 0
local equippedCount = 0 local equippedCount = 0
-- Count current character's bags in real-time -- Count current character's bags in real-time
@@ -83,6 +87,74 @@ local function CountCurrentCharacterItems(itemID)
end end
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 -- Count equipped items in real-time
for slotID = 1, 19 do -- All equipment slots for slotID = 1, 19 do -- All equipment slots
local link = GetInventoryItemLink("player", slotID) local link = GetInventoryItemLink("player", slotID)
@@ -94,7 +166,7 @@ local function CountCurrentCharacterItems(itemID)
end end
end end
return bagCount, bankCount, equippedCount return bagCount, bankCount, equippedCount, mailCount
end end
-- Count items for a specific character with real-time data for current character -- 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 -- For other characters, use saved data
local bagCount = 0 local bagCount = 0
local bankCount = 0 local bankCount = 0
local mailCount = 0
local equippedCount = 0 local equippedCount = 0
-- Count bags from saved data -- Count bags from saved data
@@ -141,6 +214,38 @@ local function CountItemsForCharacter(itemID, characterData, isCurrentChar)
end end
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 -- Count equipped items from saved data
if characterData.equipped and type(characterData.equipped) == "table" then if characterData.equipped and type(characterData.equipped) == "table" then
for slotName, itemData in pairs(characterData.equipped) do for slotName, itemData in pairs(characterData.equipped) do
@@ -153,7 +258,7 @@ local function CountItemsForCharacter(itemID, characterData, isCurrentChar)
end end
end end
return bagCount, bankCount, equippedCount return bagCount, bankCount, equippedCount, mailCount
end end
@@ -187,8 +292,15 @@ function Tooltip:AddInventoryInfo(tooltip, link)
return return
end 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 totalBags = 0
local totalBank = 0 local totalBank = 0
local totalMail = 0
local totalEquipped = 0 local totalEquipped = 0
local characterCounts = {} local characterCounts = {}
local hasAnyItems = false local hasAnyItems = false
@@ -201,18 +313,20 @@ function Tooltip:AddInventoryInfo(tooltip, link)
-- Ensure charData is actually a table and on current realm -- Ensure charData is actually a table and on current realm
if type(charData) == "table" and charData.realm == currentRealm then if type(charData) == "table" and charData.realm == currentRealm then
local isCurrentChar = (charName == currentPlayerName) 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 hasAnyItems = true
totalBags = totalBags + bagCount totalBags = totalBags + bagCount
totalBank = totalBank + bankCount totalBank = totalBank + bankCount
totalMail = totalMail + mailCount
totalEquipped = totalEquipped + equippedCount totalEquipped = totalEquipped + equippedCount
table.insert(characterCounts, { table.insert(characterCounts, {
name = charData.name or charName, name = charData.name or charName,
classToken = charData.classToken, classToken = charData.classToken,
bagCount = bagCount, bagCount = bagCount,
bankCount = bankCount, bankCount = bankCount,
mailCount = mailCount,
equippedCount = equippedCount, equippedCount = equippedCount,
isCurrent = isCurrentChar isCurrent = isCurrentChar
}) })
@@ -221,7 +335,7 @@ function Tooltip:AddInventoryInfo(tooltip, link)
-- If charData is not a table (string, number, etc.), just skip it -- If charData is not a table (string, number, etc.), just skip it
end end
local totalCount = totalBags + totalBank + totalEquipped local totalCount = totalBags + totalBank + totalMail + totalEquipped
if hasAnyItems then if hasAnyItems then
@@ -233,7 +347,16 @@ function Tooltip:AddInventoryInfo(tooltip, link)
-- Total line with cyan label and white count -- Total line with cyan label and white count
local totalText = "|cFF00FFFFTotal|r: |cFFFFFFFF" .. totalCount .. "|r" 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) tooltip:AddDoubleLine(totalText, breakdownText, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)
-- Sort with current character first -- Sort with current character first
@@ -254,6 +377,9 @@ function Tooltip:AddInventoryInfo(tooltip, link)
if charInfo.bankCount > 0 then if charInfo.bankCount > 0 then
table.insert(parts, "|cFF00FFFFBank|r: |cFFFFFFFF" .. charInfo.bankCount .. "|r") table.insert(parts, "|cFF00FFFFBank|r: |cFFFFFFFF" .. charInfo.bankCount .. "|r")
end end
if charInfo.mailCount > 0 then
table.insert(parts, "|cFF00FFFFMail|r: |cFFFFFFFF" .. charInfo.mailCount .. "|r")
end
if charInfo.equippedCount > 0 then if charInfo.equippedCount > 0 then
table.insert(parts, "|cFF00FFFFEquipped|r: |cFFFFFFFF" .. charInfo.equippedCount .. "|r") table.insert(parts, "|cFF00FFFFEquipped|r: |cFFFFFFFF" .. charInfo.equippedCount .. "|r")
end end
@@ -453,6 +579,19 @@ function Tooltip:Initialize()
end) end)
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 -- Hook SetTradeSkillItem for profession reagents and items
local oldSetTradeSkillItem = GameTooltip.SetTradeSkillItem local oldSetTradeSkillItem = GameTooltip.SetTradeSkillItem
function GameTooltip:SetTradeSkillItem(skillIndex, reagentIndex) function GameTooltip:SetTradeSkillItem(skillIndex, reagentIndex)
+30 -5
View File
@@ -95,6 +95,36 @@ function Utils:GetItemInfo(itemLink)
return nil return nil
end 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 -- Get quality color
function Utils:GetQualityColor(quality) function Utils:GetQualityColor(quality)
local color = addon.Constants.QUALITY_COLORS[quality] or addon.Constants.QUALITY_COLORS[1] local color = addon.Constants.QUALITY_COLORS[quality] or addon.Constants.QUALITY_COLORS[1]
@@ -156,12 +186,7 @@ function Utils:FormatTimeAgo(timestamp)
end end
-- Create hidden tooltip for scanning (only once) -- Create hidden tooltip for scanning (only once)
local scanTooltip = nil
local function GetScanTooltip() local function GetScanTooltip()
if not scanTooltip then
scanTooltip = CreateFrame("GameTooltip", "GudaBagScanTooltip", nil, "GameTooltipTemplate")
scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
end
return scanTooltip return scanTooltip
end end
+323
View File
@@ -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
+5 -1
View File
@@ -2,7 +2,7 @@
## Title: Guda ## Title: Guda
## Notes: All-in-one bag and bank addon for Turtle WoW ## Notes: All-in-one bag and bank addon for Turtle WoW
## Author: Vati ## Author: Vati
## Version: 1.4.8 ## Version: 1.5.0
## SavedVariables: Guda_DB ## SavedVariables: Guda_DB
## SavedVariablesPerCharacter: Guda_CharDB ## SavedVariablesPerCharacter: Guda_CharDB
@@ -16,6 +16,7 @@ Core\Tooltip.lua
Data\BagScanner.lua Data\BagScanner.lua
Data\BankScanner.lua Data\BankScanner.lua
Data\MailboxScanner.lua
Data\MoneyTracker.lua Data\MoneyTracker.lua
Data\EquipmentScanner.lua Data\EquipmentScanner.lua
@@ -23,14 +24,17 @@ Sorting\SortEngine.lua
# UI Lua must load before XML so OnLoad handlers exist # UI Lua must load before XML so OnLoad handlers exist
UI\ItemButton.lua UI\ItemButton.lua
UI\FrameHelpers.lua
UI\BagFrame.lua UI\BagFrame.lua
UI\BankFrame.lua UI\BankFrame.lua
UI\MailboxFrame.lua
UI\QuestItemBar.lua UI\QuestItemBar.lua
UI\TrackedItemBar.lua UI\TrackedItemBar.lua
UI\SettingsPopup.lua UI\SettingsPopup.lua
UI\ItemButton.xml UI\ItemButton.xml
UI\BagFrame.xml UI\BagFrame.xml
UI\BankFrame.xml UI\BankFrame.xml
UI\MailboxFrame.xml
UI\SettingsPopup.xml UI\SettingsPopup.xml
Core\Main.lua Core\Main.lua
+201 -138
View File
@@ -57,6 +57,7 @@ local CATEGORY_ORDER = {
["Weapon"] = 4, -- Non-equippable weapons ["Weapon"] = 4, -- Non-equippable weapons
["Armor"] = 5, -- Non-equippable armor ["Armor"] = 5, -- Non-equippable armor
["Tools"] = 6, ["Tools"] = 6,
["Quest"] = 7,
["Quiver"] = 8, ["Quiver"] = 8,
["Reagent"] = 9, ["Reagent"] = 9,
["Trade Goods"] = 10, ["Trade Goods"] = 10,
@@ -64,7 +65,6 @@ local CATEGORY_ORDER = {
["Container"] = 12, ["Container"] = 12,
["Key"] = 14, ["Key"] = 14,
["Miscellaneous"] = 15, ["Miscellaneous"] = 15,
["Quest"] = 16,
["Junk"] = 17, ["Junk"] = 17,
["Class Items"] = 18, ["Class Items"] = 18,
} }
@@ -97,100 +97,137 @@ local GEM_PATTERNS = {
local scanTooltip = CreateFrame("GameTooltip", "Guda_SortScanTooltip", nil, "GameTooltipTemplate") local scanTooltip = CreateFrame("GameTooltip", "Guda_SortScanTooltip", nil, "GameTooltipTemplate")
scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
-- Check if an item is a quest item by scanning its tooltip -- Property cache to prevent race conditions during rapid moves
local function IsQuestItemTooltip(bagID, slotID) local propertyCache = {}
if not bagID or not slotID then return false end
scanTooltip:ClearLines() function SortEngine:ClearCache()
scanTooltip:SetBagItem(bagID, slotID) propertyCache = {}
end
-- Check all tooltip lines for quest-related text (case-insensitive) local function GetItemProperties(bagID, slotID, itemLink)
for i = 1, scanTooltip:NumLines() do if not itemLink then return nil end
local line = getglobal("Guda_SortScanTooltipTextLeft" .. i)
if line then -- Extract base item link for stable caching (item:ID:Enchant:...)
local text = line:GetText() local _, _, baseLink = string.find(itemLink, "(item:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+)")
if text then local cacheKey = baseLink or itemLink
local tl = string.lower(text)
if string.find(tl, "quest starter") or if propertyCache[cacheKey] then
string.find(tl, "this item begins a quest") or return propertyCache[cacheKey]
string.find(tl, "starts a quest") or end
string.find(tl, "quest item") or
string.find(tl, "manual") then local props = {
return true 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
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 end
-- Check if a quest item is usable (has 'Use:' or click to text) -- Check if a quest item is usable (has 'Use:' or click to text)
local function IsQuestItemUsable(bagID, slotID) local function IsQuestItemUsable(bagID, slotID)
if not bagID or not slotID then return false end if not bagID or not slotID then return false end
local link = GetContainerItemLink(bagID, slotID)
scanTooltip:ClearLines() local props = GetItemProperties(bagID, slotID, link)
scanTooltip:SetBagItem(bagID, slotID) return props and props.isQuestUsable or false
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
end end
-- Check if item is a quest starter (explicit 'Starts a Quest' or 'This Item Begins a Quest') -- Check if item is a quest starter (explicit 'Starts a Quest' or 'This Item Begins a Quest')
local function IsQuestItemStarter(bagID, slotID) local function IsQuestItemStarter(bagID, slotID)
if not bagID or not slotID then return false end if not bagID or not slotID then return false end
local link = GetContainerItemLink(bagID, slotID)
scanTooltip:ClearLines() local props = GetItemProperties(bagID, slotID, link)
scanTooltip:SetBagItem(bagID, slotID) return props and props.isQuestStarter or false
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
end end
-- Check if an item has a gray title in its tooltip or link -- Check if an item has a gray title in its tooltip or link
local function IsItemGrayTooltip(bagID, slotID, itemLink) local function IsItemGrayTooltip(bagID, slotID, itemLink)
-- Check link first if provided local props = GetItemProperties(bagID, slotID, itemLink)
if itemLink and string.find(itemLink, "|cff9d9d9d") then return props and props.isGray or false
return true end
end
if not bagID or not slotID then return false end -- Check if an item has a restore tag (eat, drink, or restore)
local function GetItemRestoreTagTooltip(bagID, slotID)
scanTooltip:ClearLines() if not bagID or not slotID then return nil end
scanTooltip:SetBagItem(bagID, slotID) local link = GetContainerItemLink(bagID, slotID)
local props = GetItemProperties(bagID, slotID, link)
local line = getglobal("Guda_SortScanTooltipTextLeft1") return props and props.restoreTag or nil
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
end end
-- Extract itemID from item link -- Extract itemID from item link
@@ -200,6 +237,19 @@ local function GetItemID(link)
return tonumber(itemID) or 0 return tonumber(itemID) or 0
end 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 -- Extract texture pattern for grouping similar items
local function GetTexturePattern(textureName) local function GetTexturePattern(textureName)
if not textureName then return "" end if not textureName then return "" end
@@ -466,24 +516,30 @@ local function AddSortKeys(items)
for _, item in ipairs(items) do for _, item in ipairs(items) do
if item.data and item.data.link then if item.data and item.data.link then
local itemID = GetItemID(item.data.link) local itemID = GetItemID(item.data.link)
local itemName, itemLink, itemRarity, itemLevel, itemCategory, itemType, itemStackCount,
itemSubType, itemTexture, itemEquipLoc, itemSellPrice = GetItemInfo(itemID) -- Use data collected during the initial scan (item.data) for absolute stability
item.itemName = itemName local d = item.data
--addon:Print("itemTexture:%s",itemTexture or 0); local itemName = d.name
--addon:Print("itemEquipLoc:%s",itemEquipLoc or 0); local itemLink = d.link
--addon:Print("itemSubType:%s",itemSubType or 0); local itemRarity = d.quality
--addon:Print("itemType:%s",itemType or 0); local itemLevel = d.iLevel
--addon:Print("itemCategory:%s",itemCategory or 0); local itemCategory = d.class
--addon:Print("itemLevel:%s",itemLevel or 0); 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 if not itemName then
-- Skip items that couldn't be loaded -- Skip items that couldn't be loaded (fallback safety)
item.sortedClass = 999 item.sortedClass = 999
item.isEquippable = false item.isEquippable = false
item.priority = 1000 item.priority = 1000
item.equipSlotOrder = 999 item.equipSlotOrder = 999
item.invertedQuality = 0 item.invertedQuality = 0
item.invertedItemLevel = 0 item.invertedItemLevel = 0
item.itemName = ""
item.sortedSubclass = 999 item.sortedSubclass = 999
item.subclass = "" item.subclass = ""
item.texturePattern = "" item.texturePattern = ""
@@ -519,15 +575,14 @@ local function AddSortKeys(items)
item.equipSlotOrder = EQUIP_SLOT_ORDER[itemSubType] or 999 item.equipSlotOrder = EQUIP_SLOT_ORDER[itemSubType] or 999
else else
item.sortedClass = CATEGORY_ORDER[itemCategory] or 99 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) -- but aren't categorized as such by the game (e.g. some "Manual" items)
if item.sortedClass ~= (CATEGORY_ORDER["Quest"] or 99) then if item.sortedClass ~= (CATEGORY_ORDER["Quest"] or 7) then
local nameLower = item.itemName and string.lower(item.itemName) or "" local nameLower = string.lower(item.itemName)
if string.find(nameLower, "manual") or string.find(nameLower, "quest") then 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 elseif IsQuestItemTooltip(item.bagID, item.slot) then
-- If name-based heuristic fails, check tooltip item.sortedClass = CATEGORY_ORDER["Quest"] or 7
item.sortedClass = 11
end end
end end
item.equipSlotOrder = 999 item.equipSlotOrder = 999
@@ -535,31 +590,24 @@ local function AddSortKeys(items)
-- Subclass ordering -- Subclass ordering
-- Detect consumable restore/eat/drink tag (if available) BEFORE computing subclass priority -- Detect consumable restore/eat/drink tag (if available) BEFORE computing subclass priority
item.restoreTag = nil item.restoreTag = GetItemRestoreTagTooltip(item.bagID, item.slot)
if addon.Modules.Utils and addon.Modules.Utils.GetConsumableRestoreTag then
if item.bagID and item.slot then local baseSub = GetSubclassOrder(itemSubType, item.itemName)
local tag = addon.Modules.Utils:GetConsumableRestoreTag(item.bagID, item.slot) local function _cons_prk(t)
if tag then if t == "eat" then return -300 end
item.restoreTag = tag if t == "drink" then return -200 end
end if t == "restore" then return -100 end
return 0
end end
end item.sortedSubclass = _cons_prk(item.restoreTag) + baseSub
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.subclass = itemSubType or "" item.subclass = itemSubType or ""
-- Quest flags: mark quest items and detect starter/usable states -- Quest flags: mark quest items and detect starter/usable states
item.isQuest = false item.isQuest = false
item.isQuestStarter = false item.isQuestStarter = false
item.isQuestUsable = false item.isQuestUsable = false
local nameLower = item.itemName and string.lower(item.itemName) or "" local nameLower = string.lower(item.itemName)
if itemCategory == "Quest" or string.find(nameLower, "quest") or (item.data and item.data.class == "Quest") or IsQuestItemTooltip(item.bagID, item.slot) then if itemCategory == "Quest" or string.find(nameLower, "quest") or item.data.class == "Quest" or IsQuestItemTooltip(item.bagID, item.slot) then
item.isQuest = true item.isQuest = true
if IsQuestItemStarter(item.bagID, item.slot) then item.isQuestStarter = true end if IsQuestItemStarter(item.bagID, item.slot) then item.isQuestStarter = true end
if IsQuestItemUsable(item.bagID, item.slot) then item.isQuestUsable = 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) -- Texture pattern for grouping similar items (especially trade goods)
item.texturePattern = GetTexturePattern(itemTexture) item.texturePattern = GetTexturePattern(itemTexture)
-- Group Trade Goods: meats (names ending with 'meat') before eggs -- 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 itemType == "Trade Goods" then
if string.find(nameLower, "meat$") then if string.find(nameLower, "meat$") then
item.texturePattern = "trade_meat" item.texturePattern = "trade_meat"
@@ -584,12 +631,9 @@ local function AddSortKeys(items)
item.invertedItemID = -tonumber(itemID) item.invertedItemID = -tonumber(itemID)
-- Stack info for reverse stack sorting -- Stack info for reverse stack sorting
item.maxStackCount = tonumber(itemStackCount) or 1 item.maxStackCount = tonumber(itemStackSize) or 1
item.isStackable = item.maxStackCount > 1 item.isStackable = item.maxStackCount > 1
item.stackCount = tonumber(item.data.count) or 1 item.stackCount = tonumber(item.data.count) or 1
-- Name for alphabetical sorting
item.itemName = item.name or ""
end end
end end
end end
@@ -670,6 +714,13 @@ local function SortItems(items)
if ra ~= rb then if ra ~= rb then
return ra > rb return ra > rb
end 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 end
if a.sortedSubclass ~= b.sortedSubclass then if a.sortedSubclass ~= b.sortedSubclass then
return a.sortedSubclass < b.sortedSubclass return a.sortedSubclass < b.sortedSubclass
@@ -740,6 +791,11 @@ local function CollectItems(bagIDs)
-- Get fresh item info with ALL return values -- Get fresh item info with ALL return values
local itemID = GetItemID(itemLink) local itemID = GetItemID(itemLink)
local name, link, quality, iLevel, category, itemType, stackCount, subType, iconTex, equipLoc, sellPrice = GetItemInfo(itemID) 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 sequence = sequence + 1
table.insert(items, { table.insert(items, {
@@ -748,14 +804,16 @@ local function CollectItems(bagIDs)
sequence = sequence, -- Preserve original order sequence = sequence, -- Preserve original order
data = { data = {
link = itemLink, link = itemLink,
texture = texture, texture = texture or iconTex,
count = itemCount or 1, count = itemCount or 1,
quality = quality or 0, quality = quality or 0,
name = name, name = name,
iLevel = iLevel, iLevel = iLevel,
type = itemType, type = itemType,
class = category, class = category,
subclass = subType, -- NOW INCLUDED! subclass = subType,
equipLoc = equipLoc,
stackSize = stackCount or 1,
locked = locked, locked = locked,
}, },
quality = quality or 0, quality = quality or 0,
@@ -891,8 +949,6 @@ end
-- and spilling into previous bags when needed. -- and spilling into previous bags when needed.
local function BuildGreyTailPositions(bagIDs, greyCount) local function BuildGreyTailPositions(bagIDs, greyCount)
local positions = {} local positions = {}
local index = 1
if greyCount <= 0 then return positions end if greyCount <= 0 then return positions end
-- Order bags: lowest priority first (these are considered "last"), -- Order bags: lowest priority first (these are considered "last"),
@@ -920,26 +976,42 @@ local function BuildGreyTailPositions(bagIDs, greyCount)
end) end)
-- Collect tail slots from end to start, spilling to previous bags as needed. -- Collect tail slots from end to start, spilling to previous bags as needed.
local tailSlots = {}
for _, info in ipairs(ordered) do for _, info in ipairs(ordered) do
for slot = info.numSlots, 1, -1 do for slot = info.numSlots, 1, -1 do
if index <= greyCount then if table.getn(tailSlots) < greyCount then
positions[index] = { bag = info.bagID, slot = slot } table.insert(tailSlots, { bag = info.bagID, slot = slot })
index = index + 1
else else
break break
end end
end end
if index > greyCount then break end if table.getn(tailSlots) >= greyCount then break end
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 end
-- Split a list of collected items into non-greys and greys (quality 0) -- Split a list of collected items into non-greys and greys (quality 0)
local function SplitGreyItems(items) local function SplitGreyItems(items)
local nonGreys, greys = {}, {} local nonGreys, greys = {}, {}
for _, item in ipairs(items) do 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) table.insert(greys, item)
else else
table.insert(nonGreys, item) table.insert(nonGreys, item)
@@ -1026,20 +1098,8 @@ function SortEngine:AnalyzeContainer(bagIDs, containerType)
for i, item in ipairs(greys) do for i, item in ipairs(greys) do
local target = tailPositions[i] local target = tailPositions[i]
-- FIX: Check if grey item is already in a tail position of a valid regular bag -- Only count as out of place if it doesn't match the specific stable target
local isInTailPosition = false if target and (item.bagID ~= target.bag or item.slot ~= target.slot) then
-- 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
totalOutOfPlace = totalOutOfPlace + 1 totalOutOfPlace = totalOutOfPlace + 1
end end
end end
@@ -1326,6 +1386,9 @@ end
--=========================================================================== --===========================================================================
function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sortType) 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 -- Analyze to determine how many passes are needed
local analysis = analyzeFunction() local analysis = analyzeFunction()
+124 -485
View File
@@ -6,7 +6,19 @@ local addon = Guda
local BagFrame = {} local BagFrame = {}
addon.Modules.BagFrame = 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 local currentViewChar = nil -- nil = current character
function BagFrame:GetCurrentViewChar()
return currentViewChar
end
local searchText = "" local searchText = ""
local itemButtons = {} local itemButtons = {}
local showKeyring = false -- Toggle for keyring display local showKeyring = false -- Toggle for keyring display
@@ -109,6 +121,9 @@ end
-- OnHide -- OnHide
function Guda_BagFrame_OnHide(self) 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) -- Clean up all buttons when frame is hidden (safe since we're not displaying)
for _, bagParent in pairs(bagParents) do for _, bagParent in pairs(bagParents) do
if bagParent then if bagParent then
@@ -146,21 +161,7 @@ end
-- Update lock states of existing buttons (lightweight, used during drag) -- Update lock states of existing buttons (lightweight, used during drag)
function BagFrame:UpdateLockStates() function BagFrame:UpdateLockStates()
for _, bagParent in pairs(bagParents) do Guda_UpdateLockStates(bagParents)
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
end end
-- Update bagline layout (hover option) -- Update bagline layout (hover option)
@@ -365,44 +366,13 @@ function BagFrame:Update()
end end
end end
-- Helper to get or create section header -- Delegate to centralized helpers
function BagFrame:GetSectionHeader(index) function BagFrame:GetSectionHeader(index)
local name = "Guda_BagFrame_SectionHeader" .. index return Guda_GetSectionHeader("Guda_BagFrame", "Guda_BagFrame_ItemContainer", 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
end end
-- Helper to get or create bag parent frame
function BagFrame:GetBagParent(bagID) function BagFrame:GetBagParent(bagID)
local itemContainer = getglobal("Guda_BagFrame_ItemContainer") return Guda_GetBagParent("Guda_BagFrame", bagParents, bagID, "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]
end end
-- Display items by category -- Display items by category
@@ -953,113 +923,7 @@ end
-- Resize frame based on number of rows and columns -- Resize frame based on number of rows and columns
function BagFrame:ResizeFrame(currentRow, currentCol, columns, overrideHeight) function BagFrame:ResizeFrame(currentRow, currentCol, columns, overrideHeight)
local buttonSize = addon.Modules.DB:GetSetting("iconSize") or addon.Constants.BUTTON_SIZE return Guda_ResizeFrame("Guda_BagFrame", "Guda_BagFrame_ItemContainer", currentRow, currentCol, columns, overrideHeight)
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
end end
-- Check if search is currently active -- Check if search is currently active
@@ -1069,74 +933,8 @@ end
-- Check if item passes search filter (pfUI style) -- Check if item passes search filter (pfUI style)
function BagFrame:PassesSearchFilter(itemData) function BagFrame:PassesSearchFilter(itemData)
-- If no search text, everything matches if not self:IsSearchActive() then return true end
if not self:IsSearchActive() then return Guda_PassesSearchFilter(itemData, searchText)
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
end end
function BagFrame:UpdateMoney() function BagFrame:UpdateMoney()
@@ -1405,119 +1203,15 @@ function Guda_BagFrame_MoneyOnEnter(self)
end end
-- Dropdown management -- Dropdown management
local characterDropdown = nil local function Guda_BagCharacterMenu_Initialize()
local bankDropdown = nil local characters = addon.Modules.DB:GetAllCharacters(false, true)
local info
-- 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 currentPlayerFullName = addon.Modules.DB:GetPlayerFullName() local currentPlayerFullName = addon.Modules.DB:GetPlayerFullName()
local currentViewChar = addon.Modules.BagFrame:GetCurrentViewChar()
-- Add character buttons for i, char in ipairs(characters) do
for _, char in ipairs(chars) do
-- Capture variables in local scope for closure
local charFullName = char.fullName local charFullName = char.fullName
local charName = char.name
local charMoney = char.money or 0
local charClassToken = char.classToken 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 -- Get class color
local classColor = charClassToken and RAID_CLASS_COLORS[charClassToken] 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 r, g, b = classColor.r, classColor.g, classColor.b
end end
-- Button text -- Create colored name
local charText = charButton:CreateFontString(nil, "ARTWORK", "GameFontHighlight") local coloredName = addon.Modules.Utils:ColorText(char.name, r, g, b)
charText:SetPoint("LEFT", charButton, "LEFT", 8, 0)
charText:SetText(charName)
charText:SetTextColor(r, g, b)
-- Money text info = {}
local moneyText = charButton:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") info.text = coloredName
moneyText:SetPoint("RIGHT", charButton, "RIGHT", -8, 0) info.func = function()
moneyText:SetText(addon.Modules.Utils:FormatMoney(charMoney)) if charFullName == currentPlayerFullName then
moneyText:SetTextColor(0.7, 0.7, 0.7) addon.Modules.BagFrame:ShowCurrentCharacter()
-- 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()
else else
addon:Print("Error: Character fullName is nil") addon.Modules.BagFrame:ShowCharacter(charFullName)
end end
end) end
info.checked = (currentViewChar == charFullName or (not currentViewChar and charFullName == currentPlayerFullName))
table.insert(characterDropdown.buttons, charButton) UIDropDownMenu_AddButton(info)
yOffset = yOffset - 20
end end
-- Set dropdown height based on content
characterDropdown:SetHeight(math.abs(yOffset) + 8)
-- Show dropdown
characterDropdown:Show()
end end
-- Hide dropdown when clicking elsewhere -- Toggle character dropdown
local function HideCharacterDropdown() function Guda_BagFrame_ToggleCharacterDropdown(button)
if characterDropdown then local menuFrame = getglobal("Guda_BagCharacterMenu")
characterDropdown:Hide() 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
end end
-- Toggle bank dropdown -- Toggle bank dropdown
function Guda_BagFrame_ToggleBankDropdown(button) function Guda_BagFrame_ToggleBankDropdown(button)
-- Hide character dropdown if it's shown local menuFrame = getglobal("Guda_BagBankMenu")
if characterDropdown and characterDropdown:IsShown() then if not menuFrame then
characterDropdown:Hide() menuFrame = CreateFrame("Frame", "Guda_BagBankMenu", UIParent, "UIDropDownMenuTemplate")
end end
UIDropDownMenu_Initialize(menuFrame, Guda_BagBankMenu_Initialize, "MENU")
if bankDropdown and bankDropdown:IsShown() then ToggleDropDownMenu(1, nil, menuFrame, "cursor", 0, 0)
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()
end end
-- Show character's bank -- Show character's bank
@@ -2567,7 +2207,6 @@ function BagFrame:Initialize()
if bagFrame then if bagFrame then
local originalOnMouseDown = bagFrame:GetScript("OnMouseDown") local originalOnMouseDown = bagFrame:GetScript("OnMouseDown")
bagFrame:SetScript("OnMouseDown", function() bagFrame:SetScript("OnMouseDown", function()
HideCharacterDropdown()
if originalOnMouseDown then if originalOnMouseDown then
originalOnMouseDown() originalOnMouseDown()
end end
+46 -9
View File
@@ -248,6 +248,52 @@
</Scripts> </Scripts>
</Button> </Button>
<!-- Mail Button (header left, next to bank) -->
<Button name="$parent_MailButton">
<Size>
<AbsDimension x="24" y="24"/>
</Size>
<Anchors>
<Anchor point="LEFT" relativePoint="RIGHT" relativeTo="$parent_BankButton">
<Offset>
<AbsDimension x="2" y="0"/>
</Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parent_Icon">
<Size>
<AbsDimension x="20" y="20"/>
</Size>
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
<Color r="0.8" g="0.8" b="0.8"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnLoad>
local icon = getglobal(this:GetName().."_Icon")
if icon then
icon:SetTexture("Interface\\AddOns\\Guda\\Assets\\Envelope")
end
</OnLoad>
<OnClick>
Guda_BagFrame_ToggleMailDropdown(this)
</OnClick>
<OnEnter>
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:SetText("View Mailbox")
GameTooltip:Show()
</OnEnter>
<OnLeave>
GameTooltip:Hide()
</OnLeave>
</Scripts>
</Button>
<!-- Toolbar Container (for money tooltip and bag slots) --> <!-- Toolbar Container (for money tooltip and bag slots) -->
<Frame name="$parent_Toolbar" enableMouse="true"> <Frame name="$parent_Toolbar" enableMouse="true">
<Size> <Size>
@@ -654,15 +700,6 @@
<Anchors> <Anchors>
<Anchor point="TOP" relativePoint="BOTTOM" relativeTo="$parent_SearchBar" x="0" y="-5"/> <Anchor point="TOP" relativePoint="BOTTOM" relativeTo="$parent_SearchBar" x="0" y="-5"/>
</Anchors> </Anchors>
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" tile="true">
<BackgroundInsets>
<AbsInset left="0" right="0" top="0" bottom="0"/>
</BackgroundInsets>
<TileSize>
<AbsValue val="16"/>
</TileSize>
<Color r="0" g="0" b="0" a="0.5"/>
</Backdrop>
<!-- Note: with enableMouse=false, OnMouseDown/OnReceiveDrag here would not fire; <!-- Note: with enableMouse=false, OnMouseDown/OnReceiveDrag here would not fire;
child item buttons will handle mouse events directly. --> child item buttons will handle mouse events directly. -->
</Frame> </Frame>
+47 -195
View File
@@ -7,6 +7,9 @@ local BankFrame = {}
addon.Modules.BankFrame = BankFrame addon.Modules.BankFrame = BankFrame
local currentViewChar = nil local currentViewChar = nil
function BankFrame:GetCurrentViewChar()
return currentViewChar
end
local searchText = "" local searchText = ""
local isReadOnlyMode = false -- Track if viewing saved bank (read-only) or live bank (interactive) 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) local hiddenBankBags = {} -- Track which bank bags are hidden (bagID -> true/false)
@@ -78,6 +81,18 @@ function Guda_BankFrame_OnShow(self)
BankFrame:Update() BankFrame:Update()
end 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 -- Toggle visibility
function BankFrame:Toggle() function BankFrame:Toggle()
if Guda_BankFrame:IsShown() then if Guda_BankFrame:IsShown() then
@@ -103,21 +118,7 @@ end
-- Update lock states of existing buttons (lightweight, used during drag) -- Update lock states of existing buttons (lightweight, used during drag)
function BankFrame:UpdateLockStates() function BankFrame:UpdateLockStates()
for _, bankBagParent in pairs(bankBagParents) do Guda_UpdateLockStates(bankBagParents)
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
end end
-- Update display -- Update display
@@ -217,44 +218,13 @@ function BankFrame:Update()
end end
end end
-- Helper to get or create section header -- Use centralized frame helpers for section headers and bag parents
function BankFrame:GetSectionHeader(index) function BankFrame:GetSectionHeader(index)
local name = "Guda_BankFrame_SectionHeader" .. index return Guda_GetSectionHeader("Guda_BankFrame", "Guda_BankFrame_ItemContainer", 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
end end
-- Helper to get or create bank bag parent frame
function BankFrame:GetBagParent(bagID) function BankFrame:GetBagParent(bagID)
local itemContainer = getglobal("Guda_BankFrame_ItemContainer") return Guda_GetBagParent("Guda_BankFrame", bankBagParents, bagID, "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]
end end
-- Display items by category -- Display items by category
@@ -1018,59 +988,8 @@ end
-- Check if item passes search filter (pfUI style) -- Check if item passes search filter (pfUI style)
function BankFrame:PassesSearchFilter(itemData) function BankFrame:PassesSearchFilter(itemData)
-- If no search text, everything matches if not self:IsSearchActive() then return true end
if not self:IsSearchActive() then return Guda_PassesSearchFilter(itemData, searchText)
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
end end
-- Search changed handler -- Search changed handler
@@ -1829,69 +1748,16 @@ function Guda_BankFrame_ClearBagButtonHighlight()
end end
-- Bank character dropdown (similar to BagFrame's bank dropdown) -- 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) for i, char in ipairs(characters) do
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
local charFullName = char.fullName local charFullName = char.fullName
local charName = char.name
local charMoney = char.money or 0
local charClassToken = char.classToken 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 -- Get class color
local classColor = charClassToken and RAID_CLASS_COLORS[charClassToken] local classColor = charClassToken and RAID_CLASS_COLORS[charClassToken]
local r, g, b = 1, 1, 1 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 r, g, b = classColor.r, classColor.g, classColor.b
end end
-- Button text -- Create colored name
local charText = charButton:CreateFontString(nil, "ARTWORK", "GameFontHighlight") local coloredName = addon.Modules.Utils:ColorText(char.name, r, g, b)
charText:SetPoint("LEFT", charButton, "LEFT", 8, 0)
charText:SetText(charName)
charText:SetTextColor(r, g, b)
-- Money text info = {}
local moneyText = charButton:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") info.text = coloredName
moneyText:SetPoint("RIGHT", charButton, "RIGHT", -8, 0) info.func = function()
moneyText:SetText(addon.Modules.Utils:FormatMoney(charMoney)) if charFullName == currentPlayerFullName then
moneyText:SetTextColor(0.7, 0.7, 0.7) addon.Modules.BankFrame:ShowCurrentCharacter()
-- 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()
else else
addon:Print("Error: Character fullName is nil") addon.Modules.BankFrame:ShowCharacter(charFullName)
end end
end) end
info.checked = (currentViewChar == charFullName or (not currentViewChar and charFullName == currentPlayerFullName))
table.insert(bankCharDropdown.buttons, charButton) UIDropDownMenu_AddButton(info)
yOffset = yOffset - 20
end end
end
-- Set dropdown height based on content function Guda_BankFrame_ToggleBankDropdown(button)
bankCharDropdown:SetHeight(math.abs(yOffset) + 8) local menuFrame = getglobal("Guda_BankCharacterMenu")
if not menuFrame then
-- Show dropdown menuFrame = CreateFrame("Frame", "Guda_BankCharacterMenu", UIParent, "UIDropDownMenuTemplate")
bankCharDropdown:Show() end
UIDropDownMenu_Initialize(menuFrame, Guda_BankCharacterMenu_Initialize, "MENU")
ToggleDropDownMenu(1, nil, menuFrame, "cursor", 0, 0)
end end
-- Drag handlers for bank bag slots -- Drag handlers for bank bag slots
+1 -14
View File
@@ -573,15 +573,6 @@
<Anchors> <Anchors>
<Anchor point="TOP" relativePoint="BOTTOM" relativeTo="$parent_SearchBar" x="0" y="-5"/> <Anchor point="TOP" relativePoint="BOTTOM" relativeTo="$parent_SearchBar" x="0" y="-5"/>
</Anchors> </Anchors>
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" tile="true">
<BackgroundInsets>
<AbsInset left="0" right="0" top="0" bottom="10"/>
</BackgroundInsets>
<TileSize>
<AbsValue val="16"/>
</TileSize>
<Color r="0" g="0" b="0" a="0.5"/>
</Backdrop>
<Scripts> <Scripts>
<OnMouseDown> <OnMouseDown>
-- Clear search focus when clicking in item area -- Clear search focus when clicking in item area
@@ -602,11 +593,7 @@
Guda_BankFrame_OnShow(this) Guda_BankFrame_OnShow(this)
</OnShow> </OnShow>
<OnHide> <OnHide>
Guda_BankFrame:Hide() Guda_BankFrame_OnHide(this)
-- Close the actual Blizzard bank too
if BankFrame and BankFrame:IsShown() then
CloseBankFrame()
end
</OnHide> </OnHide>
<OnMouseDown> <OnMouseDown>
-- Clear search box focus when clicking on bank frame -- Clear search box focus when clicking on bank frame
+209
View File
@@ -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
+120 -14
View File
@@ -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
-- Check if an item is a quest item by scanning its tooltip and determine type -- Check if an item is a quest item by scanning its tooltip and determine type
local function IsQuestItem(bagID, slotID, isBank) local function IsQuestItem(bagID, slotID, isBank)
bagID = tonumber(bagID)
slotID = tonumber(slotID)
if not bagID or not slotID then return false end if not bagID or not slotID then return false end
scanTooltip:ClearLines() scanTooltip:ClearLines()
@@ -117,6 +119,8 @@ end
-- Scan tooltip for red text that is NOT a durability line -- Scan tooltip for red text that is NOT a durability line
local function IsItemUnusable(bagID, slotID, isBank) local function IsItemUnusable(bagID, slotID, isBank)
bagID = tonumber(bagID)
slotID = tonumber(slotID)
if not bagID or not slotID then return false end if not bagID or not slotID then return false end
-- Some clients require SetOwner before every SetBagItem/SetInventoryItem to populate lines -- Some clients require SetOwner before every SetBagItem/SetInventoryItem to populate lines
@@ -377,6 +381,15 @@ function Guda_ItemButton_OnLoad(self)
-- Default behavior -- Default behavior
if ContainerFrameItemButton_OnClick then 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) ContainerFrameItemButton_OnClick(arg1)
end end
end) end)
@@ -425,6 +438,10 @@ end
-- Set item data -- Set item data
function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCharName, matchesFilter, isReadOnly) 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 -- Proactively clear any previous cooldown overlay state before reassigning this pooled button
do do
local cd = getglobal(self:GetName().."Cooldown") or self.cooldown 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.bagID = bagID
self.slotID = slotID self.slotID = slotID
-- Also set the Blizzard slot ID for compatibility with ContainerFrameItemButtonTemplate behavior -- Also set the Blizzard slot ID for compatibility with ContainerFrameItemButtonTemplate behavior
if self.SetID and slotID then -- ALWAYS set ID to something (0 if nil) to avoid leaking old IDs when button is reused
self:SetID(slotID) if self.SetID then
self:SetID(slotID or 0)
end 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.itemData = itemData
self.isBank = isBank or false self.isBank = isBank or false
self.otherChar = otherCharName self.otherChar = otherCharName
self.isReadOnly = isReadOnly or false -- Track if this is read-only mode 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) -- Re-register for drag/drop every time (crucial for button reuse in Classic/Vanilla)
if not self.isReadOnly and not self.otherChar then 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 matchesFilter = true
end end
self.mailData = nil -- Clear mail metadata by default
-- Use Blizzard's default count fontstring (ContainerFrameItemButtonTemplate creates $parentCount) -- Use Blizzard's default count fontstring (ContainerFrameItemButtonTemplate creates $parentCount)
local countText = getglobal(self:GetName().."Count") local countText = getglobal(self:GetName().."Count")
local emptySlotBg = getglobal(self:GetName().."_EmptySlotBg") 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 -- Apply the determined texture and count
if self.hasItem then 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 SetItemButtonCount then SetItemButtonCount(self, displayCount or 1) end
if emptySlotBg then emptySlotBg:Hide() end if emptySlotBg then emptySlotBg:Hide() end
-- Update cooldown overlay for live items -- 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 -- 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 -- 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 -- Pushed texture follows the button size/position
local pushedTexture = getglobal(self:GetName().."PushedTexture") local pushedTexture = getglobal(self:GetName().."PushedTexture")
if not pushedTexture and self.GetPushedTexture then 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) -- Special border for keyring items (cyan/blue)
self.qualityBorder:SetBackdropBorderColor(0.2, 0.8, 1.0, 1) self.qualityBorder:SetBackdropBorderColor(0.2, 0.8, 1.0, 1)
self.qualityBorder:Show() self.qualityBorder:Show()
elseif itemQuality and itemLink then elseif itemQuality then
-- Check settings to determine if we should show borders (nil-safe) -- Check settings to determine if we should show borders (nil-safe)
local showEquipmentBorder, showOtherBorder local showEquipmentBorder, showOtherBorder
if addon and addon.Modules and addon.Modules.DB and addon.Modules.DB.GetSetting then 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) -- Check if item is equipment (nil-safe)
local isEquipment = false local isEquipment = false
if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.IsEquipment then if itemLink then
isEquipment = addon.Modules.Utils:IsEquipment(itemLink) if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.IsEquipment then
elseif Guda and Guda.Modules and Guda.Modules.Utils and Guda.Modules.Utils.IsEquipment then isEquipment = addon.Modules.Utils:IsEquipment(itemLink)
isEquipment = Guda.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 end
-- Determine if we should show the border based on item type and settings -- 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:ClearAllPoints()
self.questIcon:SetPoint("TOPRIGHT", self, "TOPRIGHT", 1, 0) self.questIcon:SetPoint("TOPRIGHT", self, "TOPRIGHT", 1, 0)
end end
else elseif not self.isMail then
-- Hide icon for empty slots -- Hide icon for empty slots, but keep it for mailbox custom icons
iconTexture:Hide() iconTexture:Hide()
end end
end end
@@ -929,7 +970,7 @@ end
-- OnEnter handler (show tooltip) -- OnEnter handler (show tooltip)
function Guda_ItemButton_OnEnter(self) 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 not self.otherChar and self.bagID then
if self.isBank then if self.isBank then
-- Bank item - highlight bank bag button -- Bank item - highlight bank bag button
@@ -941,7 +982,7 @@ function Guda_ItemButton_OnEnter(self)
end end
-- Early return for empty slots (no tooltip needed) -- Early return for empty slots (no tooltip needed)
if not self.hasItem then if not self.hasItem and not self.isMail then
return return
end end
@@ -949,7 +990,71 @@ function Guda_ItemButton_OnEnter(self)
-- important position of tooltip -- important position of tooltip
GameTooltip:SetPoint("BOTTOMRIGHT", self, "TOPLEFT", 10, 0) 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 GameTooltip.GudaViewedCharacter = self.otherChar
if self.itemData and self.itemData.link then if self.itemData and self.itemData.link then
GameTooltip:SetHyperlink(self.itemData.link) GameTooltip:SetHyperlink(self.itemData.link)
@@ -1017,8 +1122,9 @@ end
-- OnLeave handler -- OnLeave handler
function Guda_ItemButton_OnLeave(self) function Guda_ItemButton_OnLeave(self)
-- Clear any viewed character hint on the tooltip when leaving -- Clear any viewed character hint on the tooltip when leaving
if GameTooltip and GameTooltip.GudaViewedCharacter then if GameTooltip then
GameTooltip.GudaViewedCharacter = nil GameTooltip.GudaViewedCharacter = nil
GameTooltip.GudaInventoryAdded = nil
end end
GameTooltip:Hide() GameTooltip:Hide()
ResetCursor() ResetCursor()
+376
View File
@@ -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
+355
View File
@@ -0,0 +1,355 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/">
<!-- Mailbox Row Template -->
<Frame name="Guda_MailboxRowTemplate" virtual="true">
<Size>
<AbsDimension x="380" y="50"/>
</Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parent_Background">
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT"/>
</Anchors>
<Color r="1" g="1" b="1" a="0.05"/>
</Texture>
</Layer>
<Layer level="ARTWORK">
<FontString name="$parent_Sender" inherits="GameFontNormal" justifyH="LEFT">
<Size>
<AbsDimension x="140" y="16"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="60" y="-8"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
<FontString name="$parent_Subject" inherits="GameFontHighlightSmall" justifyH="LEFT">
<Size>
<AbsDimension x="280" y="14"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parent_Sender" relativePoint="BOTTOMLEFT">
<Offset>
<AbsDimension x="0" y="-2"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
<FontString name="$parent_ExpireTime" inherits="GameFontDisableSmall" justifyH="RIGHT">
<Size>
<AbsDimension x="100" y="12"/>
</Size>
<Anchors>
<Anchor point="TOPRIGHT">
<Offset>
<AbsDimension x="-10" y="-8"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Button name="$parent_ItemButton" inherits="Guda_ItemButtonTemplate">
<Anchors>
<Anchor point="LEFT" relativeTo="$parent" relativePoint="LEFT">
<Offset>
<AbsDimension x="10" y="0"/>
</Offset>
</Anchor>
</Anchors>
</Button>
<Frame name="$parent_MoneyFrame" inherits="SmallMoneyFrameTemplate">
<Anchors>
<Anchor point="BOTTOMRIGHT">
<Offset>
<AbsDimension x="-10" y="8"/>
</Offset>
</Anchor>
</Anchors>
</Frame>
</Frames>
</Frame>
<!-- Mailbox Frame -->
<Frame name="Guda_MailboxFrame" toplevel="true" movable="true" enableMouse="true" hidden="true" parent="UIParent">
<Size>
<AbsDimension x="420" y="540"/>
</Size>
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER" relativeTo="UIParent">
<Offset>
<AbsDimension x="0" y="0"/>
</Offset>
</Anchor>
</Anchors>
<Backdrop bgFile="Interface\ChatFrame\ChatFrameBackground" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<BackgroundInsets>
<AbsInset left="11" right="12" top="12" bottom="11"/>
</BackgroundInsets>
<TileSize>
<AbsValue val="16"/>
</TileSize>
<EdgeSize>
<AbsValue val="32"/>
</EdgeSize>
</Backdrop>
<Layers>
<!-- Title at top -->
<Layer level="ARTWORK">
<FontString name="$parent_Title" inherits="GameFontNormalLarge">
<Anchors>
<Anchor point="TOP">
<Offset>
<AbsDimension x="0" y="-12"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- Close Button -->
<Button name="$parent_CloseButton" inherits="UIPanelCloseButton">
<Anchors>
<Anchor point="TOPRIGHT">
<Offset>
<AbsDimension x="-16" y="-10"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
Guda_MailboxFrame:Hide()
</OnClick>
</Scripts>
</Button>
<!-- Search Bar -->
<Frame name="$parent_SearchBar">
<Size>
<AbsDimension x="350" y="30"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="20" y="-40"/>
</Offset>
</Anchor>
</Anchors>
<Frames>
<EditBox name="$parent_SearchBox" autoFocus="false" inherits="InputBoxTemplate">
<Size>
<AbsDimension x="0" y="20"/>
</Size>
<Anchors>
<Anchor point="LEFT">
<Offset>
<AbsDimension x="16" y="0"/>
</Offset>
</Anchor>
<Anchor point="RIGHT">
<Offset>
<AbsDimension x="-10" y="0"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnTextChanged>
Guda_MailboxFrame_OnSearchTextChanged()
</OnTextChanged>
<OnEscapePressed>
this:ClearFocus()
</OnEscapePressed>
<OnEnterPressed>
this:ClearFocus()
</OnEnterPressed>
<OnEditFocusGained>
if this:GetText() == "Search mailbox..." then
this:SetText("")
this:SetTextColor(1, 1, 1, 1)
end
if Guda_MailboxClickCatcher then
Guda_MailboxClickCatcher:Show()
end
</OnEditFocusGained>
<OnEditFocusLost>
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
</OnEditFocusLost>
</Scripts>
</EditBox>
</Frames>
</Frame>
<!-- Character Selection Button -->
<Button name="$parent_CharacterButton">
<Size>
<AbsDimension x="24" y="24"/>
</Size>
<Anchors>
<Anchor point="TOPRIGHT">
<Offset>
<AbsDimension x="-20" y="-43"/>
</Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parent_Icon">
<Size>
<AbsDimension x="20" y="20"/>
</Size>
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
<Color r="0.8" g="0.8" b="0.8"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnLoad>
local icon = getglobal(this:GetName().."_Icon")
if icon then
icon:SetTexture("Interface\\AddOns\\Guda\\Assets\\Envelope")
end
</OnLoad>
<OnClick>
Guda_MailboxFrame_ShowCharacterMenu()
</OnClick>
<OnEnter>
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:SetText("Switch Character")
GameTooltip:Show()
</OnEnter>
<OnLeave>
GameTooltip:Hide()
</OnLeave>
</Scripts>
</Button>
<!-- Item Container -->
<Frame name="Guda_MailboxFrame_ItemContainer">
<Size>
<AbsDimension x="360" y="420"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="20" y="-75"/>
</Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<FontString name="Guda_MailboxFrame_EmptyMessage" inherits="GameFontNormal" justifyH="CENTER" hidden="true">
<Size>
<AbsDimension x="300" y="0"/>
</Size>
<Anchors>
<Anchor point="CENTER">
<Offset>
<AbsDimension x="0" y="0"/>
</Offset>
</Anchor>
</Anchors>
<Color r="1" g="0.82" b="0"/>
</FontString>
</Layer>
</Layers>
</Frame>
<!-- Pagination -->
<Button name="$parent_PrevPageButton" inherits="UIPanelButtonTemplate" text="Prev">
<Size>
<AbsDimension x="60" y="22"/>
</Size>
<Anchors>
<Anchor point="BOTTOM">
<Offset>
<AbsDimension x="-100" y="30"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
Guda.Modules.MailboxFrame:PrevPage()
</OnClick>
</Scripts>
</Button>
<Button name="$parent_NextPageButton" inherits="UIPanelButtonTemplate" text="Next">
<Size>
<AbsDimension x="60" y="22"/>
</Size>
<Anchors>
<Anchor point="BOTTOM">
<Offset>
<AbsDimension x="100" y="30"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
Guda.Modules.MailboxFrame:NextPage()
</OnClick>
</Scripts>
</Button>
<Frame name="$parent_Pagination">
<Size>
<AbsDimension x="130" y="22"/>
</Size>
<Anchors>
<Anchor point="BOTTOM">
<Offset>
<AbsDimension x="0" y="30"/>
</Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parent_Text" inherits="GameFontHighlightSmall" justifyH="CENTER">
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
</Frames>
<Scripts>
<OnLoad>
Guda_MailboxFrame_OnLoad(this)
</OnLoad>
<OnShow>
Guda_MailboxFrame_OnShow(this)
</OnShow>
<OnHide>
Guda_MailboxFrame_OnHide(this)
</OnHide>
<OnMouseDown>
this:StartMoving()
</OnMouseDown>
<OnMouseUp>
this:StopMovingOrSizing()
</OnMouseUp>
</Scripts>
</Frame>
</Ui>
+10 -1
View File
@@ -368,7 +368,7 @@ function Guda_ApplyBackgroundTransparency()
local transparency = Guda.Modules.DB:GetSetting("bgTransparency") or 0.15 local transparency = Guda.Modules.DB:GetSetting("bgTransparency") or 0.15
local alpha = 1.0 - transparency 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 for _, frameName in ipairs(frames) do
local frame = getglobal(frameName) local frame = getglobal(frameName)
if frame then if frame then
@@ -603,6 +603,15 @@ function Guda_SettingsPopup_HideBordersCheckbox_OnClick(self)
end end
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 -- Update border visibility on settings frame
if SettingsPopup.UpdateBorderVisibility then if SettingsPopup.UpdateBorderVisibility then
SettingsPopup:UpdateBorderVisibility() SettingsPopup:UpdateBorderVisibility()