Files
Nanami-UI/Bags/Container.lua
2026-03-31 18:03:23 +08:00

1627 lines
62 KiB
Lua

--------------------------------------------------------------------------------
-- S-Frames: Bag Container UI (Bags/Container.lua)
--------------------------------------------------------------------------------
SFrames.Bags.Container = {}
local SLOT_SIZE = 36
local SPACING = 6
local MARGIN = 10
local TOP_OFFSET = 52 -- Space for title + search bar + gold
local TEXT_EMPTY = "\231\169\186"
local TEXT_ITEM = "\231\137\169\229\147\129"
local TEXT_BACKPACK = "\232\131\140\229\140\133"
local TEXT_BAGS_TITLE = "\232\131\140\229\140\133"
local TEXT_BANK_TITLE = "\233\147\182\232\161\140"
local TEXT_SORT = "\230\149\180\231\144\134"
local TEXT_SETTINGS = "\232\174\190\231\189\174"
local TEXT_BAG_SLOT = "\232\131\140\229\140\133\230\167\189"
local TEXT_BAG_EMPTY = "\231\169\186"
local TEXT_BAG_SLOTS = "\230\160\188"
local TEXT_EQUIPPED_BAG = "\229\183\178\232\163\133\229\164\135\232\131\140\229\140\133"
local TEXT_HS_USE = "\231\130\185\229\135\187\228\189\191\231\148\168\231\130\137\231\159\179"
local TEXT_HS_NOT_FOUND = "\232\131\140\229\140\133\228\184\173\230\156\170\230\137\190\229\136\176\231\130\137\231\159\179\227\128\130"
local TEXT_CHARACTER = "\232\167\146\232\137\178"
local TEXT_ONLINE = "\229\156\168\231\186\191"
local TEXT_OFFLINE = "\231\166\187\231\186\191"
local PANEL_BG_ALPHA = 0.55
local SLOT_BG_ALPHA = 0.22
local CHARACTER_SELECTOR_ICON = "Interface\\CHARACTERFRAME\\TemporaryPortrait-Female-Human"
local _A = SFrames.ActiveTheme
local BagFrame = nil
local ItemSlots = {}
local playerBagInvSlots = { [1] = 20, [2] = 21, [3] = 22, [4] = 23 }
local function IsLiveBankOpen()
local bankFrame = _G["SFramesBankFrame"]
if not bankFrame or not bankFrame:IsVisible() then return false end
if not SFrames.Bags.Bank then return false end
return not SFrames.Bags.Bank.isOffline
end
local function SetTooltipFromContainerItem(bagID, slotID)
if not GameTooltip then return false end
GameTooltip:ClearLines()
if GameTooltip.SetBagItem then
local ok = pcall(function() GameTooltip:SetBagItem(bagID, slotID) end)
if ok then
local left1 = _G["GameTooltipTextLeft1"]
if left1 and left1:GetText() and left1:GetText() ~= "" then
return true
end
end
end
local link = GetContainerItemLink(bagID, slotID)
if link then
local ok = pcall(function() GameTooltip:SetHyperlink(link) end)
if ok then
local left1 = _G["GameTooltipTextLeft1"]
if left1 and left1:GetText() and left1:GetText() ~= "" then
return true
end
end
local name = GetItemInfo(link)
if name and name ~= "" then
GameTooltip:SetText(name, 1, 1, 1)
return true
end
end
return false
end
local function HasTooltipText()
local left1 = _G["GameTooltipTextLeft1"]
return left1 and left1:GetText() and left1:GetText() ~= ""
end
local function GetItemNameFromLink(link)
if type(link) ~= "string" or link == "" then
return nil
end
local name = GetItemInfo(link)
if name and name ~= "" then
return name
end
local _, _, parsed = string.find(link, "%[(.+)%]")
if parsed and parsed ~= "" then
return parsed
end
return nil
end
local function ShowMerchantCursorForSlot(button)
if not button then return end
if button.bagID == nil or button.slotID == nil then return end
if SFrames.Bags.Container and SFrames.Bags.Container.isOffline then return end
if not ((MerchantFrame and MerchantFrame:IsVisible()) or (SFramesMerchantFrame and SFramesMerchantFrame:IsVisible())) then return end
local texture, _, locked = GetContainerItemInfo(button.bagID, button.slotID)
if not texture or locked then return end
-- Prefer container-aware API when available.
if ShowContainerSellCursor then
local ok = pcall(function()
ShowContainerSellCursor(button.bagID, button.slotID)
end)
if ok then return end
end
-- Fallback to generic merchant sell cursor.
if ShowMerchantSellCursor then
local ok = pcall(function()
ShowMerchantSellCursor()
end)
if ok then return end
end
-- Final fallback for clients lacking both APIs.
if SetCursor then
pcall(function()
SetCursor("BUY_CURSOR")
end)
end
end
local function ResolvePlayerBagInvSlots()
local fallback = { [1] = 20, [2] = 21, [3] = 22, [4] = 23 }
for bagIndex = 1, 4 do
local invSlot = nil
local liveBtn = _G["CharacterBag" .. (bagIndex - 1) .. "Slot"]
-- Most reliable when Blizzard character UI is loaded.
if liveBtn and liveBtn.GetID then
local id = liveBtn:GetID()
if type(id) == "number" and id > 0 then
invSlot = id
end
end
if (not invSlot) and ContainerIDToInventoryID then
local ok, slotID = pcall(function() return ContainerIDToInventoryID(bagIndex) end)
if ok and type(slotID) == "number" and slotID > 0 then
invSlot = slotID
end
end
if (not invSlot) and GetInventorySlotInfo then
local slotNames = {
"Bag" .. (bagIndex - 1) .. "Slot",
"Bag" .. bagIndex .. "Slot",
"CharacterBag" .. (bagIndex - 1) .. "Slot",
}
for _, slotName in ipairs(slotNames) do
local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end)
if ok and type(slotID) == "number" and slotID > 0 then
invSlot = slotID
break
end
end
end
playerBagInvSlots[bagIndex] = invSlot or fallback[bagIndex]
end
end
local function GetLivePlayerBagIconTexture(bagIndex)
local btn = _G["CharacterBag" .. (bagIndex - 1) .. "Slot"]
if not btn then return nil end
local icon = _G[btn:GetName() .. "IconTexture"] or _G[btn:GetName() .. "Icon"]
if icon and icon.GetTexture then
local tex = icon:GetTexture()
if tex and tex ~= "" then
return tex
end
end
return nil
end
local function IsDisabledIconTexture(tex)
if type(tex) ~= "string" then return false end
local lower = string.lower(tex)
lower = string.gsub(lower, "\\", "/")
return string.find(lower, "disabled", 1, true) ~= nil
end
local function IsPaperdollBagPlaceholder(tex)
if type(tex) ~= "string" then return false end
local lower = string.lower(tex)
lower = string.gsub(lower, "\\", "/")
if string.find(lower, "ui-paperdoll-slot-bag", 1, true) then
return true
end
-- Some clients may return slightly different placeholder paths.
if string.find(lower, "paperdoll", 1, true) and
string.find(lower, "slot", 1, true) and
string.find(lower, "bag", 1, true) then
return true
end
return false
end
local function IsUsableBagIconTexture(tex)
if type(tex) ~= "string" or tex == "" then
return false
end
if IsPaperdollBagPlaceholder(tex) then
return false
end
if IsDisabledIconTexture(tex) then
return false
end
return true
end
local function GetIconFromItemLink(link)
if not link then return nil end
local _, _, _, _, _, _, _, _, tex = GetItemInfo(link)
if tex then return tex end
local _, _, itemID = string.find(link, "item:(%d+)")
if itemID then
local _, _, _, _, _, _, _, _, tex2 = GetItemInfo("item:" .. itemID)
if tex2 then return tex2 end
end
return nil
end
local function BuildPlayerTradeLinkSet()
local links = {}
if not (TradeFrame and TradeFrame:IsVisible()) then return links end
if not GetTradePlayerItemLink then return links end
for i = 1, 6 do
local link = GetTradePlayerItemLink(i)
if link and link ~= "" then
links[link] = true
end
end
return links
end
local function CreateCoinDisplay(parent, frameName)
local frame = CreateFrame("Frame", frameName, parent, "SmallMoneyFrameTemplate")
frame:SetFrameStrata(parent:GetFrameStrata())
frame:SetFrameLevel(parent:GetFrameLevel() + 30)
frame:SetWidth(140)
frame:SetHeight(16)
return frame
end
local function SetCoinDisplayMoney(display, copper)
if not display then return end
local value = tonumber(copper) or 0
if value < 0 then value = 0 end
if SmallMoneyFrame_SetAmount then
local ok = pcall(function() SmallMoneyFrame_SetAmount(display, value) end)
if ok then return end
end
if MoneyFrame_Update and display.GetName then
local frameName = display:GetName()
if frameName and frameName ~= "" then
pcall(function() MoneyFrame_Update(frameName, value) end)
end
end
end
-- Create a single item slot button
local function CreateSlot(parent, id)
local button = CreateFrame("Button", "SFramesBagSlot" .. id, parent, "ItemButtonTemplate")
button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
button:RegisterForDrag("LeftButton")
-- Rounded backdrop style (matching CharacterPanel equipment slots)
local DEFAULT_BORDER = (_A and _A.slotBorder) or { 0.25, 0.25, 0.3, 0.8 }
button:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 18,
insets = { left = 2, right = 2, top = 2, bottom = 2 }
})
button:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.9)
button:SetBackdropBorderColor(DEFAULT_BORDER[1], DEFAULT_BORDER[2], DEFAULT_BORDER[3], DEFAULT_BORDER[4])
-- Inset icon within the rounded border
local icon = _G[button:GetName() .. "IconTexture"]
if icon then
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
icon:ClearAllPoints()
icon:SetPoint("TOPLEFT", button, "TOPLEFT", 4, -4)
icon:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -4, 4)
end
local qualGlow = button:CreateTexture(nil, "OVERLAY")
qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
qualGlow:SetBlendMode("ADD")
qualGlow:SetAlpha(0.8)
qualGlow:SetWidth(SLOT_SIZE * 1.9)
qualGlow:SetHeight(SLOT_SIZE * 1.9)
qualGlow:SetPoint("CENTER", button, "CENTER", 0, 0)
qualGlow:Hide()
button.qualGlow = qualGlow
function button:SetBorderColor(r, g, b, a)
self.qualGlow:SetVertexColor(r, g, b)
self.qualGlow:Show()
self._qualityBorder = true
end
function button:ShowBorder()
self._qualityBorder = true
end
function button:HideBorder()
self.qualGlow:Hide()
self._qualityBorder = false
end
-- Hide the ugly default rounded Blizzard border
local nt = _G[button:GetName() .. "NormalTexture"]
if nt then nt:SetTexture(nil) nt:Hide() end
-- Grey item marker (a small coin/junk icon in the corner)
local junkIcon = button:CreateTexture(nil, "OVERLAY")
junkIcon:SetTexture("Interface\\Buttons\\UI-GroupLoot-Coin-Up")
junkIcon:SetWidth(14)
junkIcon:SetHeight(14)
-- Place it on top of everything
junkIcon:SetPoint("TOPLEFT", button, "TOPLEFT", 1, -1)
junkIcon:Hide()
button.junkIcon = junkIcon
local previewGlow = button:CreateTexture(nil, "OVERLAY")
previewGlow:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
previewGlow:SetBlendMode("ADD")
previewGlow:SetAllPoints(button)
previewGlow:Hide()
button.previewGlow = previewGlow
local tradeText = button:CreateFontString(nil, "OVERLAY")
tradeText:SetFont("Fonts\\Zekton.ttf", 10, "OUTLINE")
if setglobal then -- check Vanilla or use default font
tradeText:SetFontObject(GameFontNormal)
end
tradeText:SetPoint("CENTER", button, "CENTER", 0, 0)
tradeText:SetText("交易")
tradeText:SetTextColor(0, 1, 0, 1)
tradeText:Hide()
button.tradeText = tradeText
button:SetScript("OnEnter", function()
if this.bagID == nil or this.slotID == nil then return end
SFrames.Bags._hoveredSlot = this
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:ClearLines()
if SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar then
local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Container.offlineChar)
if data and data.bags[this.bagID] and data.bags[this.bagID].items[this.slotID] then
local link = data.bags[this.bagID].items[this.slotID].link
local shown = false
if link then
local _, _, itemStr = string.find(link, "(item:[%-?%d:]+)")
local ok = false
if itemStr then
ok = pcall(function() GameTooltip:SetHyperlink(itemStr) end)
else
ok = pcall(function() GameTooltip:SetHyperlink(link) end)
end
shown = ok and HasTooltipText()
if not shown then
local name = GetItemNameFromLink(link)
if name and name ~= "" then
GameTooltip:SetText(name, 1, 1, 1)
shown = true
end
end
if not shown then
GameTooltip:SetText(TEXT_ITEM, 1, 1, 1)
end
else
GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65)
end
else
GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65)
end
else
local shown = SetTooltipFromContainerItem(this.bagID, this.slotID)
if not shown then
GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65)
end
end
if IsControlKeyDown() then
ShowInspectCursor()
else
ShowMerchantCursorForSlot(this)
end
GameTooltip:Show()
end)
button:SetScript("OnLeave", function()
SFrames.Bags._hoveredSlot = nil
this.controlDownLast = nil
GameTooltip:Hide()
if HideContainerSellCursor and this.bagID and this.slotID then
pcall(function()
HideContainerSellCursor(this.bagID, this.slotID)
end)
end
ResetCursor()
end)
local cooldown = CreateFrame("Model", button:GetName().."Cooldown", button, "CooldownFrameTemplate")
cooldown:SetAllPoints(button)
function button:SplitStack(split)
if not split or split < 1 then return end
if self.bagID == nil or self.slotID == nil then return end
SplitContainerItem(self.bagID, self.slotID, split)
end
button:SetScript("OnClick", function()
local bagID = this.bagID
local slotID = this.slotID
local isOffline = SFrames.Bags.Container.isOffline
-- Helper: get item link for this slot (works both online and offline)
local function GetSlotLink()
if isOffline and SFrames.Bags.Container.offlineChar then
local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Container.offlineChar)
if data and data.bags[bagID] and data.bags[bagID].items[slotID] then
return data.bags[bagID].items[slotID].link
end
return nil
end
return GetContainerItemLink(bagID, slotID)
end
if IsControlKeyDown() and arg1 == "LeftButton" then
local link = GetSlotLink()
if link and DressUpItemLink then
DressUpItemLink(link)
return
end
end
if IsShiftKeyDown() then
local eb = ChatFrameEditBox
if eb and eb.IsVisible and eb:IsVisible() then
local link = GetSlotLink()
if link then eb:Insert(link) end
return
end
if not isOffline and arg1 == "LeftButton" and (not CursorHasItem()) and OpenStackSplitFrame then
local _, itemCount = GetContainerItemInfo(bagID, slotID)
if itemCount and itemCount > 1 then
OpenStackSplitFrame(itemCount, this, "BOTTOMLEFT", "TOPLEFT")
return
end
end
end
-- Block all other actions in offline mode
if isOffline then return end
if arg1 == "RightButton" then
if SFrames.Mail and SFrames.Mail.TryAddItemFromBag then
if SFrames.Mail.TryAddItemFromBag(bagID, slotID) then return end
end
if this.bagID >= 0 and IsLiveBankOpen() and AutoStoreBagItem then
local ok = pcall(function() AutoStoreBagItem(this.bagID, this.slotID) end)
if ok then return end
end
if TradeFrame and TradeFrame:IsVisible() and not IsShiftKeyDown() then
local _, _, locked = GetContainerItemInfo(this.bagID, this.slotID)
if locked then
local link = GetSlotLink()
if link and GetTradePlayerItemLink then
local inTradeSlot = nil
for i = 1, 6 do
if GetTradePlayerItemLink(i) == link then
inTradeSlot = i
break
end
end
if inTradeSlot then
ClearCursor()
ClickTradeButton(inTradeSlot)
ClearCursor()
return
end
if BagFrame and BagFrame:IsVisible() then
SFrames.Bags.Container:UpdateLayout()
end
end
else
PickupContainerItem(this.bagID, this.slotID)
local tradeSlot = TradeFrame_GetAvailableSlot and TradeFrame_GetAvailableSlot()
if tradeSlot then ClickTradeButton(tradeSlot) end
if CursorHasItem() then ClearCursor() end
return
end
end
if AuctionFrame and AuctionFrame:IsShown() and not IsShiftKeyDown() then
if AuctionFrameBrowse and AuctionFrameBrowse:IsShown() then
local link = GetContainerItemLink(this.bagID, this.slotID)
if link then
local _, _, itemName = string.find(link, "%[(.+)%]")
if itemName and BrowseName then
BrowseName:SetText(itemName)
if AuctionFrameBrowse_Search then AuctionFrameBrowse_Search() end
end
end
return
elseif AuctionFrameAuctions and AuctionFrameAuctions:IsShown() then
PickupContainerItem(this.bagID, this.slotID)
if AuctionsItemButton then AuctionsItemButton:Click() end
if CursorHasItem() then ClearCursor() end
return
end
end
UseContainerItem(this.bagID, this.slotID)
else
PickupContainerItem(this.bagID, this.slotID)
end
end)
button:SetScript("OnDragStart", function()
if SFrames.Bags.Container.isOffline then return end
if CursorHasItem() then return end
PickupContainerItem(this.bagID, this.slotID)
end)
button:SetScript("OnReceiveDrag", function()
if SFrames.Bags.Container.isOffline then return end
if CursorHasItem() then
PickupContainerItem(this.bagID, this.slotID)
end
end)
return button
end
local function SaveBagFramePosition()
if not (BagFrame and SFramesDB and SFramesDB.Bags) then return end
local point, _, relPoint, x, y = BagFrame:GetPoint()
if not point or not relPoint then return end
SFramesDB.Bags.bagPosition = {
point = point,
relPoint = relPoint,
x = x or 0,
y = y or 0,
}
end
local function ApplyBagFramePosition()
if not BagFrame then return end
BagFrame:ClearAllPoints()
local pos = SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bagPosition
if pos and pos.point and pos.relPoint and type(pos.x) == "number" and type(pos.y) == "number" then
BagFrame:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y)
else
BagFrame:SetPoint("RIGHT", UIParent, "RIGHT", -20, 0)
end
end
function SFrames.Bags.Container:PreviewBagSlots(targetBagID)
for _, btn in ipairs(ItemSlots) do
if btn and btn:IsShown() then
local icon = _G[btn:GetName() .. "IconTexture"]
local isMatch = (btn.bagID == targetBagID)
if icon then
icon:SetVertexColor(1, 1, 1)
end
if btn.previewGlow then
if isMatch then
btn.previewGlow:Show()
else
btn.previewGlow:Hide()
end
end
end
end
end
function SFrames.Bags.Container:ClearBagPreview()
for _, btn in ipairs(ItemSlots) do
if btn and btn:IsShown() then
local icon = _G[btn:GetName() .. "IconTexture"]
if icon then icon:SetVertexColor(1, 1, 1) end
if btn.previewGlow then btn.previewGlow:Hide() end
end
end
-- Restore search dimming state after preview.
local query = ""
if SFramesBagSearchBox and SFramesBagSearchBox.GetText then
query = SFramesBagSearchBox:GetText() or ""
end
if SFrames.Bags.Features and SFrames.Bags.Features.ApplySearch then
SFrames.Bags.Features:ApplySearch(query)
end
end
-- Build/Update the item slot grid
function SFrames.Bags.Container:UpdateLayout()
if not BagFrame then return end
local cols = (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.columns) or 10
local spacing = (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bagSpacing) or SPACING
spacing = tonumber(spacing) or SPACING
if spacing < 0 then spacing = 0 end
local slots = {}
local isOffline = self.isOffline
local charName = self.offlineChar
local offlineDB = nil
if isOffline and charName then
offlineDB = SFrames.Bags.Offline:GetCharacterData(charName)
if not offlineDB then
isOffline = false
self.isOffline = false
self.offlineChar = nil
if BagFrame and BagFrame.RefreshCharacterSelectorText then
BagFrame.RefreshCharacterSelectorText()
end
end
end
local tradeLinks = nil
if not isOffline and TradeFrame and TradeFrame:IsVisible() then
tradeLinks = BuildPlayerTradeLinkSet()
end
-- Collect all slots (bags 0-4)
for bag = 0, 4 do
local size = 0
if isOffline and offlineDB then
if offlineDB.bags[bag] then size = offlineDB.bags[bag].size end
else
size = GetContainerNumSlots(bag)
end
for slot = 1, size do
table.insert(slots, { bag = bag, slot = slot })
end
end
local numSlots = table.getn(slots)
if numSlots == 0 then numSlots = 1 end
local rows = math.ceil(numSlots / cols)
-- Resize frame
local width = MARGIN * 2 + (cols * SLOT_SIZE) + math.max(0, (cols - 1)) * spacing
local height = MARGIN * 2 + TOP_OFFSET + (rows * SLOT_SIZE) + math.max(0, (rows - 1)) * spacing + 24
BagFrame:SetWidth(math.max(width, 160))
BagFrame:SetHeight(height)
-- Position & update slots
for i, meta in ipairs(slots) do
local btn = ItemSlots[i]
if not btn then
btn = CreateSlot(BagFrame, i)
ItemSlots[i] = btn
end
btn.bagID = meta.bag
btn.slotID = meta.slot
local row = math.floor((i - 1) / cols)
local col = math.mod((i - 1), cols)
btn:ClearAllPoints()
btn:SetPoint("TOPLEFT", BagFrame, "TOPLEFT",
MARGIN + col * (SLOT_SIZE + spacing),
-(MARGIN + TOP_OFFSET + row * (SLOT_SIZE + spacing)))
-- Fetch item info
local texture, count, quality, link, locked
if isOffline and offlineDB then
if offlineDB.bags[meta.bag] and offlineDB.bags[meta.bag].items[meta.slot] then
local item = offlineDB.bags[meta.bag].items[meta.slot]
texture = item.texture
count = item.count
quality = item.quality
link = item.link
locked = false
end
else
local t, c, l, q = GetContainerItemInfo(meta.bag, meta.slot)
texture = t; count = c; locked = l; quality = q
link = GetContainerItemLink(meta.bag, meta.slot)
end
SetItemButtonTexture(btn, texture)
SetItemButtonCount(btn, count)
local iconTex = _G[btn:GetName() .. "IconTexture"]
local isDesaturated = locked and (not tradeLinks)
if btn.tradeText then btn.tradeText:Hide() end
if tradeLinks and link then
local isLockedByTrade = locked and (tradeLinks[link] and true or false)
if isLockedByTrade then
if btn.tradeText then btn.tradeText:Show() end
isDesaturated = true
else
-- Ignore stale locked flags for items no longer in trade.
isDesaturated = false
SFramesBagsTooltipScanner = SFramesBagsTooltipScanner or CreateFrame("GameTooltip", "SFramesBagsTooltipScanner", nil, "GameTooltipTemplate")
SFramesBagsTooltipScanner:SetOwner(UIParent, "ANCHOR_NONE")
SFramesBagsTooltipScanner:ClearLines()
local hasItem = SFramesBagsTooltipScanner:SetBagItem(meta.bag, meta.slot)
if hasItem then
for i = 1, 10 do
local line = _G["SFramesBagsTooltipScannerTextLeft" .. i]
if line then
local text = line:GetText()
if text and (text == ITEM_SOULBOUND or text == ITEM_BIND_QUEST or text == "Quest Item") then
isDesaturated = true
break
end
else
break
end
end
end
end
elseif not isOffline and TradeFrame and TradeFrame:IsVisible() and link then
SFramesBagsTooltipScanner = SFramesBagsTooltipScanner or CreateFrame("GameTooltip", "SFramesBagsTooltipScanner", nil, "GameTooltipTemplate")
SFramesBagsTooltipScanner:SetOwner(UIParent, "ANCHOR_NONE")
SFramesBagsTooltipScanner:ClearLines()
local hasItem = SFramesBagsTooltipScanner:SetBagItem(meta.bag, meta.slot)
if hasItem then
for i = 1, 10 do
local line = _G["SFramesBagsTooltipScannerTextLeft" .. i]
if line then
local text = line:GetText()
if text and (text == ITEM_SOULBOUND or text == ITEM_BIND_QUEST or text == "Quest Item") then
isDesaturated = true
break
end
else
break
end
end
end
end
if iconTex then
if isDesaturated then
iconTex:SetVertexColor(0.5, 0.5, 0.5)
else
iconTex:SetVertexColor(1, 1, 1)
end
end
if btn.previewGlow then btn.previewGlow:Hide() end
-- Quality border & Grey marker
btn:HideBorder()
btn.junkIcon:Hide()
if link then
-- 1st attempt: Safest 1.12 generic approach: Read the color straight out of the hyperlink!
-- Standard link format: |cffAABBCC|Hitem:...
local _, _, hex = string.find(link, "|c(%x+)|H")
local parsedColor = false
if hex and string.len(hex) == 8 then
local hexLower = string.lower(hex)
if hexLower == "ff9d9d9d" then
-- Poor / Grey Item
btn:SetBorderColor(0.5, 0.5, 0.5, 1)
btn:ShowBorder()
btn.junkIcon:Show()
parsedColor = true
elseif hexLower == "ffffffff" then
-- Common / White Item (No special border)
parsedColor = true
else
-- Green, Blue, Purple, Orange
local r = tonumber(string.sub(hex, 3, 4), 16) / 255
local g = tonumber(string.sub(hex, 5, 6), 16) / 255
local b = tonumber(string.sub(hex, 7, 8), 16) / 255
btn:SetBorderColor(r, g, b, 1)
btn:ShowBorder()
parsedColor = true
end
end
-- 2nd attempt fallback: Use the API quality return values if regex fails
if not parsedColor then
local q = quality
if not q then
local _, _, itemString = string.find(link, "item:(%d+)")
if itemString then
local _, _, scanRarity = GetItemInfo("item:" .. itemString)
q = scanRarity
end
end
if q then
if q == 0 then
btn:SetBorderColor(0.5, 0.5, 0.5, 1)
btn:ShowBorder()
btn.junkIcon:Show()
elseif q > 1 then
local r, g, b = GetItemQualityColor(q)
btn:SetBorderColor(r, g, b, 1)
btn:ShowBorder()
end
end
end
end
-- Cooldowns
local cooldown = _G[btn:GetName() .. "Cooldown"]
if cooldown then
if isOffline then
cooldown:Hide()
else
local start, duration, enable = GetContainerItemCooldown(meta.bag, meta.slot)
if start and duration and start > 0 and duration > 0 then
CooldownFrame_SetTimer(cooldown, start, duration, enable)
cooldown:Show()
else
cooldown:Hide()
end
end
end
btn:Show()
end
-- Hide excess buttons
for i = numSlots + 1, table.getn(ItemSlots) do
if ItemSlots[i] then ItemSlots[i]:Hide() end
end
if BagFrame.UpdateBagSlotIcons then
BagFrame.UpdateBagSlotIcons()
end
-- Update money display
if BagFrame.moneyFrame then
local copper = 0
if isOffline and offlineDB then
copper = offlineDB.money or 0
else
copper = GetMoney()
end
SetCoinDisplayMoney(BagFrame.moneyFrame, copper)
end
end
-- Main frame initialization (called once after PLAYER_LOGIN)
function SFrames.Bags.Container:Initialize()
if BagFrame then return end
BagFrame = CreateFrame("Frame", "SFramesBagFrame", UIParent)
BagFrame:SetWidth(420)
BagFrame:SetHeight(200)
BagFrame:SetFrameStrata("HIGH")
BagFrame:SetToplevel(true)
BagFrame:EnableMouse(true)
BagFrame:SetMovable(true)
BagFrame:SetClampedToScreen(true)
BagFrame:RegisterForDrag("LeftButton")
BagFrame:SetScript("OnDragStart", function() this:StartMoving() end)
BagFrame:SetScript("OnDragStop", function()
this:StopMovingOrSizing()
SaveBagFramePosition()
end)
ApplyBagFramePosition()
tinsert(UISpecialFrames, "SFramesBagFrame")
BagFrame:SetScript("OnUpdate", function()
local btn = SFrames.Bags._hoveredSlot
if not btn then return end
if not GameTooltip:IsOwned(btn) then
SFrames.Bags._hoveredSlot = nil
return
end
if IsControlKeyDown() then
if not btn.controlDownLast then
btn.controlDownLast = true
ShowInspectCursor()
end
else
if btn.controlDownLast then
btn.controlDownLast = false
ResetCursor()
ShowMerchantCursorForSlot(btn)
end
end
end)
-- ESC menu style rounded backdrop
BagFrame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
local _A = SFrames.ActiveTheme
local _bagBgA = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.bgAlpha) == "number") and SFramesDB.Bags.bgAlpha or 0.95
if _A and _A.panelBg then
BagFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], _bagBgA)
BagFrame:SetBackdropBorderColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], _A.panelBorder[4] or 0.9)
else
BagFrame:SetBackdropColor(0.12, 0.06, 0.10, _bagBgA)
BagFrame:SetBackdropBorderColor(0.55, 0.30, 0.42, 0.9)
end
local bagShadow = CreateFrame("Frame", nil, BagFrame)
bagShadow:SetPoint("TOPLEFT", BagFrame, "TOPLEFT", -5, 5)
bagShadow:SetPoint("BOTTOMRIGHT", BagFrame, "BOTTOMRIGHT", 5, -5)
bagShadow:SetFrameLevel(math.max(BagFrame:GetFrameLevel() - 1, 0))
bagShadow:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
bagShadow:SetBackdropColor(0, 0, 0, 0.55)
bagShadow:SetBackdropBorderColor(0, 0, 0, 0.4)
local scale = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.scale) == "number" and SFramesDB.Bags.scale) or 0.85
BagFrame:SetScale(scale)
local bagAlpha = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.alpha) == "number" and SFramesDB.Bags.alpha) or 1
BagFrame:SetAlpha(bagAlpha)
local titleIco = SFrames:CreateIcon(BagFrame, "backpack", 14)
titleIco:SetDrawLayer("OVERLAY")
titleIco:SetPoint("TOPLEFT", BagFrame, "TOPLEFT", 10, -7)
titleIco:SetVertexColor(_A.title[1], _A.title[2], _A.title[3])
local titleFS = SFrames:CreateFontString(BagFrame, 12, "LEFT")
titleFS:SetPoint("LEFT", titleIco, "RIGHT", 4, 0)
titleFS:SetText(TEXT_BAGS_TITLE)
titleFS:SetTextColor(_A.title[1], _A.title[2], _A.title[3])
BagFrame.title = titleFS
-- Close button
local closeBtn = CreateFrame("Button", "SFramesBagClose", BagFrame, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", BagFrame, "TOPRIGHT", 0, 0)
closeBtn:SetScript("OnClick", function() SFrames.Bags.Container:Close() end)
-- Money display
BagFrame.moneyFrame = CreateCoinDisplay(BagFrame, "SFramesBagMoneyFrame")
BagFrame.moneyFrame:SetPoint("RIGHT", BagFrame, "BOTTOMRIGHT", -8, 17)
SetCoinDisplayMoney(BagFrame.moneyFrame, 0)
-- Bag slot management buttons (5 slots: 0=backpack, 1-4 = equipped bags)
BagFrame.bagSlotBtns = {}
local BAG_BTN_SIZE = 22
ResolvePlayerBagInvSlots()
local bagInvSlots = playerBagInvSlots
local bagBar = CreateFrame("Frame", "SFramesBagBar", BagFrame)
bagBar:SetWidth((BAG_BTN_SIZE * 5) + (3 * 4))
bagBar:SetHeight(BAG_BTN_SIZE)
bagBar:SetPoint("BOTTOMLEFT", BagFrame, "BOTTOMLEFT", 8, 6)
bagBar:SetFrameStrata(BagFrame:GetFrameStrata())
bagBar:SetFrameLevel(BagFrame:GetFrameLevel() + 40)
bagBar:EnableMouse(false)
BagFrame.bagBar = bagBar
local function PlaceCursorItemInBagSlot(invSlot)
if not invSlot or not CursorHasItem() then return false end
local ok = false
if EquipCursorItem then
ok = pcall(function() EquipCursorItem(invSlot) end)
if ok and (not CursorHasItem()) then
return true
end
end
-- In vanilla this can both place and swap bag items in bag slots.
ok = pcall(function() PickupBagFromSlot(invSlot) end)
if ok and (not CursorHasItem()) then
return true
end
if PutItemInBag then
ok = pcall(function() PutItemInBag(invSlot) end)
if ok and (not CursorHasItem()) then
return true
end
end
-- Fallback path if PutItemInBag is unavailable.
if CursorHasItem() then
local fallbackOk = pcall(function() PickupInventoryItem(invSlot) end)
ok = fallbackOk or ok
end
return ok and (not CursorHasItem())
end
for bagIndex = 0, 4 do
local bsBtn = CreateFrame("Button", "SFramesBagMgrBtn"..bagIndex, bagBar)
bsBtn:SetWidth(BAG_BTN_SIZE)
bsBtn:SetHeight(BAG_BTN_SIZE)
bsBtn:EnableMouse(true)
bsBtn:SetFrameStrata(bagBar:GetFrameStrata())
bsBtn:SetFrameLevel(bagBar:GetFrameLevel() + 1)
-- Anchor: first one at BOTTOMLEFT, rest chained RIGHT
if bagIndex == 0 then
bsBtn:SetPoint("TOPLEFT", bagBar, "TOPLEFT", 0, 0)
else
bsBtn:SetPoint("LEFT", _G["SFramesBagMgrBtn" .. (bagIndex-1)], "RIGHT", 3, 0)
end
bsBtn.bagID = bagIndex
-- Rounded backdrop (matching item slot style)
bsBtn:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 12,
insets = { left = 2, right = 2, top = 2, bottom = 2 }
})
bsBtn:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.9)
bsBtn:SetBackdropBorderColor(_A.slotBorder[1], _A.slotBorder[2], _A.slotBorder[3], _A.slotBorder[4] or 0.8)
-- Icon texture inset within border
local bsIcon = bsBtn:CreateTexture(nil, "OVERLAY")
bsIcon:SetPoint("TOPLEFT", bsBtn, "TOPLEFT", 2, -2)
bsIcon:SetPoint("BOTTOMRIGHT", bsBtn, "BOTTOMRIGHT", -2, 2)
bsIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
bsIcon:SetBlendMode("BLEND")
bsIcon:SetVertexColor(1, 1, 1, 1)
bsBtn.icon = bsIcon
local bsHighlight = bsBtn:CreateTexture(nil, "HIGHLIGHT")
bsHighlight:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
bsHighlight:SetBlendMode("ADD")
bsHighlight:SetAllPoints(bsBtn)
bsBtn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:ClearLines()
if SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar then
local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Container.offlineChar)
if this.bagID == 0 then
local slots = data and data.bags and data.bags[0] and data.bags[0].size or 0
if slots and slots > 0 then
GameTooltip:SetText(string.format("%s (%d%s)", TEXT_BACKPACK, slots, TEXT_BAG_SLOTS), 1, 1, 1)
else
GameTooltip:SetText(TEXT_BACKPACK, 1, 1, 1)
end
else
local shown = false
local slots = data and data.bags and data.bags[this.bagID] and data.bags[this.bagID].size or 0
local bagMeta = data and data.equippedBags and data.equippedBags[this.bagID]
if bagMeta and bagMeta.link then
local ok = pcall(function() GameTooltip:SetHyperlink(bagMeta.link) end)
shown = ok and HasTooltipText()
if not shown then
local name = GetItemNameFromLink(bagMeta.link)
if name and name ~= "" then
GameTooltip:SetText(name, 1, 1, 1)
shown = true
end
end
end
if not shown then
if slots and slots > 0 then
GameTooltip:SetText(string.format("%s %d (%d%s)", TEXT_BAG_SLOT, this.bagID, slots, TEXT_BAG_SLOTS), 1, 1, 1)
else
GameTooltip:SetText(string.format("%s %d (%s)", TEXT_BAG_SLOT, this.bagID, TEXT_BAG_EMPTY), 0.6, 0.6, 0.6)
end
end
end
GameTooltip:AddLine(TEXT_OFFLINE, 0.75, 0.75, 0.75)
else
if this.bagID == 0 then
GameTooltip:SetText(TEXT_BACKPACK, 1, 1, 1)
else
local invSlot = playerBagInvSlots[this.bagID]
local shown = false
if invSlot then
if GameTooltip.SetInventoryItem then
local ok = pcall(function() GameTooltip:SetInventoryItem("player", invSlot) end)
if ok then
local left1 = _G["GameTooltipTextLeft1"]
if left1 and left1:GetText() and left1:GetText() ~= "" then
shown = true
end
end
end
if not shown then
local bagLink = GetInventoryItemLink("player", invSlot)
if bagLink then
local ok = pcall(function() GameTooltip:SetHyperlink(bagLink) end)
if ok then
shown = true
else
local name = GetItemInfo(bagLink) or (TEXT_EQUIPPED_BAG.." "..this.bagID)
GameTooltip:SetText(name, 1, 1, 1)
shown = true
end
end
end
if not shown then
local slots = GetContainerNumSlots(this.bagID) or 0
if slots > 0 then
GameTooltip:SetText(string.format("%s %d (%d%s)", TEXT_BAG_SLOT, this.bagID, slots, TEXT_BAG_SLOTS), 1, 1, 1)
else
GameTooltip:SetText(string.format("%s %d (%s)", TEXT_BAG_SLOT, this.bagID, TEXT_BAG_EMPTY), 0.6, 0.6, 0.6)
end
end
else
GameTooltip:SetText(string.format("%s %d", TEXT_BAG_SLOT, this.bagID), 0.8, 0.8, 0.8)
end
end
end
if BagFrame:IsVisible() then
SFrames.Bags.Container:PreviewBagSlots(this.bagID)
end
GameTooltip:Show()
end)
bsBtn:SetScript("OnLeave", function()
GameTooltip:Hide()
if BagFrame:IsVisible() then
SFrames.Bags.Container:ClearBagPreview()
end
end)
bsBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
bsBtn:RegisterForDrag("LeftButton")
bsBtn:SetScript("OnDragStart", function()
if this.bagID <= 0 then return end
local invSlot = playerBagInvSlots[this.bagID]
if invSlot then
pcall(function() PickupBagFromSlot(invSlot) end)
end
end)
bsBtn:SetScript("OnReceiveDrag", function()
if not CursorHasItem() then return end
if this.bagID == 0 then
if PutItemInBackpack then
pcall(function() PutItemInBackpack() end)
end
else
local invSlot = playerBagInvSlots[this.bagID]
PlaceCursorItemInBagSlot(invSlot)
end
if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end
if BagFrame:IsVisible() then SFrames.Bags.Container:UpdateLayout() end
end)
bsBtn:SetScript("OnClick", function()
if CursorHasItem() then
if this.bagID == 0 then
if PutItemInBackpack then
pcall(function() PutItemInBackpack() end)
end
else
local invSlot = playerBagInvSlots[this.bagID]
PlaceCursorItemInBagSlot(invSlot)
end
if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end
if BagFrame:IsVisible() then SFrames.Bags.Container:UpdateLayout() end
return
end
if this.bagID > 0 then
local invSlot = playerBagInvSlots[this.bagID]
if invSlot then
pcall(function() PickupBagFromSlot(invSlot) end)
end
elseif arg1 == "RightButton" then
-- Backpack slot has no equip slot to pick up from.
else
SFrames.Bags.Container:Toggle()
end
end)
BagFrame.bagSlotBtns[bagIndex] = bsBtn
end
-- Update bag slot textures whenever bag contents change
BagFrame.UpdateBagSlotIcons = function()
local isOffline = SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar
local offlineDB = nil
if isOffline then
offlineDB = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Container.offlineChar)
end
ResolvePlayerBagInvSlots()
for bagIndex = 0, 4 do
local btn = BagFrame.bagSlotBtns[bagIndex]
if btn then
if bagIndex == 0 then
btn.icon:SetTexture("Interface\\Buttons\\Button-Backpack-Up")
elseif isOffline and offlineDB then
local slots = offlineDB.bags and offlineDB.bags[bagIndex] and offlineDB.bags[bagIndex].size or 0
local bagMeta = offlineDB.equippedBags and offlineDB.equippedBags[bagIndex]
local tex = nil
if bagMeta and bagMeta.link then
tex = GetIconFromItemLink(bagMeta.link)
end
if (not tex) and bagMeta and IsUsableBagIconTexture(bagMeta.texture) then
tex = bagMeta.texture
end
if (not tex) and slots and slots > 0 then
tex = "Interface\\Icons\\INV_Misc_Bag_08"
end
if not tex then
tex = "Interface\\Icons\\INV_Misc_Bag_08"
end
btn.icon:SetTexture(tex)
else
local invSlot = playerBagInvSlots[bagIndex]
local tex = nil
local slots = GetContainerNumSlots(bagIndex) or 0
if invSlot then
local link = GetInventoryItemLink("player", invSlot)
tex = GetIconFromItemLink(link)
if (not tex) and slots > 0 then
local rawTex = GetInventoryItemTexture("player", invSlot)
if type(rawTex) ~= "string" then
tex = rawTex
elseif IsUsableBagIconTexture(rawTex) then
tex = rawTex
end
end
end
if (not tex) and slots > 0 then
local liveTex = GetLivePlayerBagIconTexture(bagIndex)
if IsUsableBagIconTexture(liveTex) then
tex = liveTex
end
end
if tex then
btn.icon:SetTexture(tex)
else
btn.icon:SetTexture("Interface\\Icons\\INV_Misc_Bag_08")
end
end
btn:Enable()
if btn.icon.SetDesaturated then
pcall(function() btn.icon:SetDesaturated(false) end)
end
btn.icon:SetVertexColor(1, 1, 1, 1)
btn.icon:SetAlpha(1)
end
end
end
local function QueueBagSlotIconRefreshes()
if not BagFrame or not BagFrame.UpdateBagSlotIcons then return end
local elapsed = 0
local nextRefresh = 1
local refreshPoints = { 0.08, 0.25, 0.55 }
local refreshTimer = CreateFrame("Frame")
refreshTimer:SetScript("OnUpdate", function()
elapsed = elapsed + arg1
if nextRefresh <= table.getn(refreshPoints) and elapsed >= refreshPoints[nextRefresh] then
if BagFrame.UpdateBagSlotIcons then
BagFrame.UpdateBagSlotIcons()
end
if BagFrame:IsVisible() then
SFrames.Bags.Container:UpdateLayout()
end
nextRefresh = nextRefresh + 1
end
if nextRefresh > table.getn(refreshPoints) then
this:SetScript("OnUpdate", nil)
end
end)
end
BagFrame.UpdateBagSlotIcons()
QueueBagSlotIconRefreshes()
-- Search bar
local eb = CreateFrame("EditBox", "SFramesBagSearchBox", BagFrame, "InputBoxTemplate")
eb:SetWidth(120)
eb:SetHeight(18)
eb:SetPoint("TOPLEFT", BagFrame, "TOPLEFT", 10, -26)
eb:SetAutoFocus(false)
eb:SetScript("OnEnterPressed", function() this:ClearFocus() end)
eb:SetScript("OnEscapePressed", function() this:ClearFocus(); this:SetText("") end)
eb:SetScript("OnTextChanged", function()
SFrames.Bags.Features:ApplySearch(this:GetText())
end)
local function CreateHeaderIconButton(name, parent, iconPath)
local btn = CreateFrame("Button", name, parent)
btn:SetWidth(18)
btn:SetHeight(18)
btn:SetFrameStrata(parent:GetFrameStrata())
btn:SetFrameLevel(parent:GetFrameLevel() + 45)
btn:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 12,
insets = { left = 2, right = 2, top = 2, bottom = 2 }
})
btn:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.86)
btn:SetBackdropBorderColor(_A.slotBorder[1], _A.slotBorder[2], _A.slotBorder[3], _A.slotBorder[4] or 0.8)
local icon = btn:CreateTexture(nil, "ARTWORK")
icon:SetTexture(iconPath or "Interface\\Icons\\INV_Misc_QuestionMark")
icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2)
icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2)
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
btn.icon = icon
local hl = btn:CreateTexture(nil, "HIGHLIGHT")
hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
hl:SetBlendMode("ADD")
hl:SetAllPoints(btn)
return btn
end
-- Character selector: icon button-triggered dropdown.
local function GetCurrentCharacterName()
local live = UnitName("player")
if type(live) == "string" and live ~= "" then
return live
end
local cached = SFrames.Bags.Offline:GetCurrentPlayerName()
if type(cached) == "string" and cached ~= "" then
return cached
end
return TEXT_CHARACTER
end
local function GetOfflineBankTargetCharacter()
if SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar then
return SFrames.Bags.Container.offlineChar
end
return GetCurrentCharacterName()
end
local charBtn = CreateHeaderIconButton("SFramesBagCharBtn", BagFrame, CHARACTER_SELECTOR_ICON)
charBtn:SetPoint("TOPRIGHT", BagFrame, "TOPRIGHT", -8, -26)
BagFrame.charSelectBtn = charBtn
local cfgBtn = CreateHeaderIconButton("SFramesBagConfigBtn", BagFrame, "Interface\\Icons\\INV_Misc_Gear_01")
cfgBtn:SetPoint("RIGHT", charBtn, "LEFT", -4, 0)
SFrames:SetIcon(cfgBtn.icon, "settings")
cfgBtn:SetScript("OnClick", function()
if SFrames.ConfigUI and SFrames.ConfigUI.Build then
SFrames.ConfigUI:Build("bags")
end
end)
cfgBtn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(TEXT_SETTINGS, 1, 1, 1)
GameTooltip:Show()
end)
cfgBtn:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
local bankBtn = CreateHeaderIconButton("SFramesBagOfflineBankBtn", BagFrame, "Interface\\Icons\\INV_Misc_Key_05")
bankBtn:SetPoint("RIGHT", cfgBtn, "LEFT", -4, 0)
SFrames:SetIcon(bankBtn.icon, "gold")
bankBtn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(TEXT_OFFLINE .. TEXT_BANK_TITLE, 1, 1, 1)
local target = GetOfflineBankTargetCharacter()
if target and target ~= "" and target ~= TEXT_CHARACTER then
GameTooltip:AddLine(target, 0.8, 0.8, 0.8)
end
GameTooltip:Show()
end)
bankBtn:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
bankBtn:SetScript("OnClick", function()
if not (SFrames.Bags and SFrames.Bags.Bank) then return end
local target = GetOfflineBankTargetCharacter()
if target == TEXT_CHARACTER then
target = nil
end
if SFrames.Bags.Bank.OpenOffline then
SFrames.Bags.Bank:OpenOffline(target)
else
SFrames.Bags.Bank:Open()
end
end)
-- Sort button (icon)
local sortBtn = CreateHeaderIconButton("SFramesBagSortBtn", BagFrame, "Interface\\Icons\\INV_Misc_Note_05")
sortBtn:SetPoint("LEFT", eb, "RIGHT", 6, 0)
SFrames:SetIcon(sortBtn.icon, "backpack")
sortBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
sortBtn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(TEXT_SORT, 1, 1, 1)
GameTooltip:AddLine("\229\183\166\233\148\174\230\149\180\231\144\134 | \229\143\179\233\148\174\229\143\141\229\186\143\230\149\180\231\144\134", 0.7, 0.7, 0.7)
GameTooltip:Show()
end)
sortBtn:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
sortBtn:SetScript("OnClick", function()
if SFrames.Bags.Sort then
local reverse = (arg1 == "RightButton")
SFrames.Bags.Sort:Start(reverse)
end
end)
local hsBtn = CreateHeaderIconButton("SFramesBagHSBtn", BagFrame, "Interface\\Icons\\INV_Misc_Rune_01")
hsBtn:SetPoint("LEFT", sortBtn, "RIGHT", 4, 0)
SFrames:SetIcon(hsBtn.icon, "hearthstone")
hsBtn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(TEXT_HS_USE, 1, 1, 1)
GameTooltip:Show()
end)
hsBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
hsBtn:SetScript("OnClick", function()
for bag = 0, 4 do
for slot = 1, GetContainerNumSlots(bag) do
local link = GetContainerItemLink(bag, slot)
if link and (string.find(link, "6948") or string.find(link, "Hearthstone")) then
UseContainerItem(bag, slot)
return
end
end
end
SFrames:Print(TEXT_HS_NOT_FOUND)
end)
local keyBtn = CreateHeaderIconButton("SFramesBagKeyBtn", BagFrame, "Interface\\Icons\\INV_Misc_Key_04")
keyBtn:SetPoint("LEFT", hsBtn, "RIGHT", 4, 0)
SFrames:SetIcon(keyBtn.icon, "key")
keyBtn:RegisterForClicks("LeftButtonUp")
keyBtn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText("\233\146\165\229\140\153\233\147\190", 1, 0.82, 0)
GameTooltip:Show()
end)
keyBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
keyBtn:SetScript("OnClick", function()
if SFrames.Bags._origToggleKeyRing then
SFrames.Bags._origToggleKeyRing()
elseif ToggleKeyRing then
ToggleKeyRing()
end
end)
local dbMenu = CreateFrame("Frame", "SFramesBagDropdown", BagFrame, "UIDropDownMenuTemplate")
dbMenu:Hide()
dbMenu:SetPoint("TOPRIGHT", charBtn, "BOTTOMRIGHT", 0, 0)
local function RefreshCharacterSelectorText()
if not BagFrame or not BagFrame.charSelectBtn then return end
if SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar then
BagFrame.charSelectorLabel = SFrames.Bags.Container.offlineChar .. " (" .. TEXT_OFFLINE .. ")"
else
BagFrame.charSelectorLabel = GetCurrentCharacterName() .. " (" .. TEXT_ONLINE .. ")"
end
end
BagFrame.RefreshCharacterSelectorText = RefreshCharacterSelectorText
local function OnDropdownClick()
local char = this.value
if char == "ONLINE" then
SFrames.Bags.Container.isOffline = false
SFrames.Bags.Container.offlineChar = nil
else
SFrames.Bags.Container.isOffline = true
SFrames.Bags.Container.offlineChar = char
end
RefreshCharacterSelectorText()
SFrames.Bags.Container:UpdateLayout()
end
UIDropDownMenu_Initialize(dbMenu, function()
local info = UIDropDownMenu_CreateInfo and UIDropDownMenu_CreateInfo() or {}
local currentName = GetCurrentCharacterName()
info.text = currentName .. " (" .. TEXT_ONLINE .. ")"
info.value = "ONLINE"
info.func = OnDropdownClick
info.checked = (not SFrames.Bags.Container.isOffline)
UIDropDownMenu_AddButton(info)
local chars = SFrames.Bags.Offline:GetCharacterList()
table.sort(chars)
for _, char in ipairs(chars) do
if char ~= currentName then
info = UIDropDownMenu_CreateInfo and UIDropDownMenu_CreateInfo() or {}
info.text = char .. " (" .. TEXT_OFFLINE .. ")"
info.value = char
info.func = OnDropdownClick
info.checked = SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar == char
UIDropDownMenu_AddButton(info)
end
end
end)
charBtn:SetScript("OnClick", function()
ToggleDropDownMenu(1, nil, dbMenu, this, 0, 0)
end)
charBtn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(BagFrame.charSelectorLabel or TEXT_CHARACTER, 1, 1, 1)
GameTooltip:Show()
end)
charBtn:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
RefreshCharacterSelectorText()
-- React to bag updates while open
BagFrame:RegisterEvent("BAG_UPDATE")
BagFrame:RegisterEvent("PLAYER_MONEY")
BagFrame:RegisterEvent("BAG_UPDATE_COOLDOWN")
BagFrame:RegisterEvent("UNIT_INVENTORY_CHANGED")
BagFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
BagFrame:RegisterEvent("TRADE_SHOW")
BagFrame:RegisterEvent("TRADE_CLOSED")
BagFrame:RegisterEvent("TRADE_UPDATE")
BagFrame:RegisterEvent("TRADE_PLAYER_ITEM_CHANGED")
BagFrame:RegisterEvent("TRADE_TARGET_ITEM_CHANGED")
BagFrame:RegisterEvent("TRADE_ACCEPT_UPDATE")
BagFrame:SetScript("OnEvent", function()
if BagFrame:IsVisible() then
SFrames.Bags.Container:UpdateLayout()
end
if event == "PLAYER_ENTERING_WORLD" then
if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end
QueueBagSlotIconRefreshes()
elseif event == "BAG_UPDATE" or event == "UNIT_INVENTORY_CHANGED" then
if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end
elseif event == "TRADE_SHOW" or event == "TRADE_CLOSED" or event == "TRADE_UPDATE"
or event == "TRADE_PLAYER_ITEM_CHANGED" or event == "TRADE_TARGET_ITEM_CHANGED"
or event == "TRADE_ACCEPT_UPDATE" then
if event == "TRADE_CLOSED" then
for _, btn in ipairs(ItemSlots) do
if btn.tradeText then btn.tradeText:Hide() end
local iconTex = _G[btn:GetName() .. "IconTexture"]
if iconTex and iconTex.SetDesaturated then
pcall(function() iconTex:SetDesaturated(false) end)
end
if iconTex then iconTex:SetVertexColor(1, 1, 1, 1) end
end
end
if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end
QueueBagSlotIconRefreshes()
end
end)
BagFrame:Hide()
end
function SFrames.Bags.Container:Toggle()
if not BagFrame then return end
if BagFrame:IsVisible() then self:Close() else self:Open() end
end
function SFrames.Bags.Container:Open()
if not BagFrame then return end
self.isOffline = false
self.offlineChar = nil
if BagFrame.RefreshCharacterSelectorText then BagFrame.RefreshCharacterSelectorText() end
-- Clear search
if SFramesBagSearchBox then SFramesBagSearchBox:SetText("") end
self:UpdateLayout()
BagFrame:Show()
local elapsed = 0
local nextRefresh = 1
local refreshPoints = { 0.05, 0.18, 0.40 }
local refreshTimer = CreateFrame("Frame")
refreshTimer:SetScript("OnUpdate", function()
elapsed = elapsed + arg1
if nextRefresh <= table.getn(refreshPoints) and elapsed >= refreshPoints[nextRefresh] then
if BagFrame:IsVisible() then
if BagFrame.UpdateBagSlotIcons then
BagFrame.UpdateBagSlotIcons()
end
SFrames.Bags.Container:UpdateLayout()
end
nextRefresh = nextRefresh + 1
end
if nextRefresh > table.getn(refreshPoints) then
this:SetScript("OnUpdate", nil)
end
end)
PlaySound("igBackPackOpen")
end
function SFrames.Bags.Container:Close()
if not BagFrame then return end
BagFrame:Hide()
PlaySound("igBackPackClose")
end