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

2093 lines
73 KiB
Lua

--------------------------------------------------------------------------------
-- S-Frames: Bank Module GUI (Bags/Bank.lua)
-- Unified custom interface for the player's Bank and extended Bank Bags
--------------------------------------------------------------------------------
SFrames.Bags.Bank = {}
local SFBankFrame = nil
local ItemSlots = {}
local isClosing = false -- Guard to prevent close闂傚倸鍊烽悞锕傚礈濮樿泛纾婚柛娑卞枟閸欏繘鏌嶈閹叉矠nt闂傚倸鍊烽悞锕傚礈濮樿泛纾婚柛娑卞枙缁诲棛绱掑顔界厪se recursion
local SLOT_SIZE = 34
local SPACING = 6
local MARGIN = 10
local TOP_OFFSET = 52 -- Space for title + search bar row
local BOTTOM_OFFSET = 34 -- Space for bank bag slot controls
local TEXT_EMPTY = "\231\169\186"
local TEXT_ITEM = "\231\137\169\229\147\129"
local TEXT_BANK_TITLE = "\233\147\182\232\161\140"
local TEXT_SORT = "\230\149\180\231\144\134"
local TEXT_BUY_SLOT = "\232\180\173\228\185\176\230\160\143\228\189\141"
local TEXT_UNAVAILABLE_OFFLINE = "\231\166\187\231\186\191\230\168\161\229\188\143\228\184\139\228\184\141\229\143\175\231\148\168"
local TEXT_BANK_BAG_SLOT = "\233\147\182\232\161\140\232\131\140\229\140\133\230\167\189"
local TEXT_BANK_BAG = "\233\147\182\232\161\140\232\131\140\229\140\133"
local TEXT_LOCKED_BANK_SLOT = "\229\183\178\233\148\129\229\174\154\231\154\132\233\147\182\232\161\140\232\131\140\229\140\133\230\167\189"
local TEXT_CLICK_BUY = "\231\130\185\229\135\187\232\180\173\228\185\176\232\175\165\230\160\143\228\189\141"
local TEXT_BUY_PREV_FIRST = "\232\175\183\229\133\136\232\180\173\228\185\176\229\137\141\228\184\128\228\184\170\230\160\143\228\189\141"
local TEXT_DRAG_EQUIP = "\230\139\150\229\133\165\232\131\140\229\140\133\229\143\175\232\163\133\229\164\135\229\136\176\230\173\164\230\160\143\228\189\141"
local TEXT_RIGHT_PICKUP = "\229\143\179\233\148\174\229\143\150\228\184\139\229\183\178\232\163\133\229\164\135\232\131\140\229\140\133"
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 TEXT_LAYOUT_ERR = "\233\147\182\232\161\140\229\184\131\229\177\128\230\155\180\230\150\176\229\164\177\232\180\165\239\188\154"
local TEXT_SLOT_UNIT = "\230\160\188"
local DEFAULT_BAG_ICON = "Interface\\Buttons\\Button-Backpack-Up"
local EMPTY_BAG_ICON = "Interface\\Icons\\INV_Misc_Bag_08"
local CHARACTER_SELECTOR_ICON = "Interface\\CHARACTERFRAME\\TemporaryPortrait-Female-Human"
local PANEL_BG_ALPHA = 0.55
local SLOT_BG_ALPHA = 0.22
local _A = SFrames.ActiveTheme
local bankSearchText = "" -- Current search filter text
local BankBagButtons = {}
local bankLayoutErrorStamp = nil
local BANK_BAG_SIZE = 22
local BANK_BAG_SPACING = 3
local BANK_BAG_COUNT = 6
local BANK_BAG_FIRST_ID = 5
local BANK_BAG_LAST_ID = BANK_BAG_FIRST_ID + BANK_BAG_COUNT - 1
local bankBagInvSlotCache = {}
local function SafeBankUpdateLayout()
local ok, err = pcall(function() SFrames.Bags.Bank:UpdateLayout() end)
if not ok and err then
if bankLayoutErrorStamp ~= err then
bankLayoutErrorStamp = err
if SFrames and SFrames.Print then
SFrames:Print("闂備胶鍋撻崕濂搞€侀幋锔藉仼鐎光偓閸曨剚銆冮梺鍛婂浮閺€閬嶅蓟婵犲洦鐓ユ繛鍡樺俯閸? " .. tostring(err))
end
end
end
end
-- Override with clean localized error text.
SafeBankUpdateLayout = function()
local ok, err = pcall(function() SFrames.Bags.Bank:UpdateLayout() end)
if not ok and err then
if bankLayoutErrorStamp ~= err then
bankLayoutErrorStamp = err
if SFrames and SFrames.Print then
SFrames:Print(TEXT_LAYOUT_ERR .. tostring(err))
end
end
end
end
local function SaveBankFramePosition()
if not (SFBankFrame and SFramesDB and SFramesDB.Bags) then return end
local point, _, relPoint, x, y = SFBankFrame:GetPoint()
if not point or not relPoint then return end
SFramesDB.Bags.bankPosition = {
point = point,
relPoint = relPoint,
x = x or 0,
y = y or 0,
}
end
local function ApplyBankFramePosition()
if not SFBankFrame then return end
SFBankFrame:ClearAllPoints()
local pos = SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bankPosition
if pos and pos.point and pos.relPoint and type(pos.x) == "number" and type(pos.y) == "number" then
SFBankFrame:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y)
else
-- Default to right side; bag frame defaults to left side.
SFBankFrame:SetPoint("CENTER", UIParent, "CENTER", 360, 0)
end
end
-- Bank sort: sorts main bank and bank bag containers
local function SortBank()
-- WoW 1.12 doesn't have SortBags() but we can do a basic consolidation
-- by using the container sort API if available
if SortBankBags then
SortBankBags()
end
if SortBags then
-- also sort player bags since items may move there
SortBags()
end
end
local function SetTooltipFromBankContainerItem(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 IsAsciiText(text)
if type(text) ~= "string" then return false end
return string.find(text, "[\128-\255]") == nil
end
local function TextMatchesSearch(name, query)
if not query or query == "" then return true end
if not name or name == "" then return false end
if string.find(name, query, 1, true) then
return true
end
-- Keep Chinese/GBK safe: only do lowercase matching for pure ASCII strings.
if IsAsciiText(name) and IsAsciiText(query) then
return string.find(string.lower(name), string.lower(query), 1, true) ~= nil
end
return false
end
local function GetSafeCoinText(copper)
local value = tonumber(copper) or 0
if value <= 0 then
return "0c"
end
if GetCoinTextureString then
local ok, text = pcall(function() return GetCoinTextureString(value) end)
if ok and text and text ~= "" then
return text
end
end
local g = math.floor(value / 10000)
local s = math.floor(math.mod(value, 10000) / 100)
local c = math.mod(value, 100)
return string.format("%dg %ds %dc", g, s, c)
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
local function IsValidBankInvSlot(id)
return type(id) == "number" and id >= 39 and id <= 74
end
local function GetMainBankInvSlotID(slotID)
if not slotID or slotID <= 0 then return nil end
local liveBtn = _G["BankFrameItem" .. slotID]
if liveBtn and liveBtn.GetID then
local id = liveBtn:GetID()
if IsValidBankInvSlot(id) then
return id
end
end
if not BankButtonIDToInvSlotID then return nil end
local ok, invSlot = pcall(function() return BankButtonIDToInvSlotID(slotID) end)
if ok and IsValidBankInvSlot(invSlot) then
return invSlot
end
ok, invSlot = pcall(function() return BankButtonIDToInvSlotID(slotID, 0) end)
if ok and IsValidBankInvSlot(invSlot) then
return invSlot
end
ok, invSlot = pcall(function() return BankButtonIDToInvSlotID(slotID, 1) end)
if ok and IsValidBankInvSlot(invSlot) then
return invSlot
end
return nil
end
-- Override tooltip resolution with a stronger fallback chain for main bank slots.
SetTooltipFromBankContainerItem = function(bagID, slotID, cachedLink, cachedName)
if not GameTooltip then return false end
GameTooltip:ClearLines()
if bagID == -1 and GameTooltip.SetInventoryItem then
local invSlot = GetMainBankInvSlotID(slotID)
if invSlot then
local ok = pcall(function() GameTooltip:SetInventoryItem("player", invSlot) end)
if ok and HasTooltipText() then
return true
end
end
end
if GameTooltip.SetBagItem then
local ok = pcall(function() GameTooltip:SetBagItem(bagID, slotID) end)
if ok and HasTooltipText() then
return true
end
end
local link = GetContainerItemLink(bagID, slotID) or cachedLink
if link then
local ok = pcall(function() GameTooltip:SetHyperlink(link) end)
if ok and HasTooltipText() then
return true
end
local name = GetItemInfo(link)
if name and name ~= "" then
GameTooltip:SetText(name, 1, 1, 1)
return true
end
end
if cachedName and cachedName ~= "" then
GameTooltip:SetText(cachedName, 1, 1, 1)
return true
end
return false
end
local function SafeGetInventoryItemLink(unit, invSlot)
if not invSlot then return nil end
local ok, link = pcall(function() return GetInventoryItemLink(unit, invSlot) end)
if ok then return link end
return nil
end
local function SafeGetInventoryItemTexture(unit, invSlot)
if not invSlot then return nil end
local ok, tex = pcall(function() return GetInventoryItemTexture(unit, invSlot) end)
if ok then return tex end
return nil
end
local function SafeGetInventorySlotByName(slotName)
if not GetInventorySlotInfo then return nil end
local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end)
if ok and type(slotID) == "number" and slotID > 0 then
return slotID
end
return 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
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 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 IsDefaultBankBagPlaceholder(tex)
if type(tex) ~= "string" then return false end
local lower = string.lower(tex)
lower = string.gsub(lower, "\\", "/")
if string.find(lower, "button-backpack-up", 1, true) then
return true
end
if string.find(lower, "button-backpack-disabled", 1, true) then
return true
end
return false
end
local function IsUsableBankBagIconTexture(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
if IsDefaultBankBagPlaceholder(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 IsBagItemLink(link)
if type(link) ~= "string" or link == "" then
return false
end
local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link)
return equipLoc == "INVTYPE_BAG"
end
local function GetBagLinkState(link)
if type(link) ~= "string" or link == "" then
return "invalid"
end
local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link)
if equipLoc == "INVTYPE_BAG" then
return "bag"
end
if equipLoc == nil or equipLoc == "" then
return "unknown"
end
return "invalid"
end
local function AddInvSlotCandidate(candidates, seen, slotID)
if type(slotID) ~= "number" or slotID <= 0 then
return
end
if seen[slotID] then
return
end
seen[slotID] = true
table.insert(candidates, slotID)
end
local function GetLiveBankBagButton(index)
local names = {
"BankFrameBag" .. index,
"BankFrameBag" .. index .. "Slot",
}
for _, name in ipairs(names) do
local btn = _G[name]
if btn and btn.GetObjectType and btn:GetObjectType() == "Button" then
return btn
end
end
return nil
end
local function AddBankInvSlotCandidate(candidates, seen, slotID)
if type(slotID) ~= "number" or slotID <= 0 then
return
end
-- Real bank bag equipment slots are not normal character inventory slots.
if slotID <= 23 then
return
end
AddInvSlotCandidate(candidates, seen, slotID)
end
local function IsBankBagInvSlotID(slotID)
return type(slotID) == "number" and slotID > 23
end
local function GetBankBagInvSlotID(index)
if type(index) ~= "number" or index <= 0 then
return nil
end
local cached = bankBagInvSlotCache[index]
if type(cached) == "number" and cached > 23 then
return cached
end
bankBagInvSlotCache[index] = nil
local bagID = index + (BANK_BAG_FIRST_ID - 1)
local function TryBankButtonID(buttonID, isBank)
if not BankButtonIDToInvSlotID then return nil end
local ok, slotID = pcall(function() return BankButtonIDToInvSlotID(buttonID, isBank) end)
if ok and IsBankBagInvSlotID(slotID) then
return slotID
end
return nil
end
local function AcceptIfBankSlot(slotID)
if IsBankBagInvSlotID(slotID) then
return slotID
end
return nil
end
-- Primary mapping: bag container id + isBank=1.
local slot = TryBankButtonID(bagID, 1)
if slot then
bankBagInvSlotCache[index] = slot
return slot
end
-- Compatibility fallback: some clients may expect 1..N as first arg.
slot = TryBankButtonID(index, 1)
if slot then
bankBagInvSlotCache[index] = slot
return slot
end
-- Only accept legacy guesses when they actually hold a bag item.
local function AcceptIfBag(slotID)
slotID = AcceptIfBankSlot(slotID)
if not slotID then return nil end
local link = SafeGetInventoryItemLink("player", slotID)
if link and IsBagItemLink(link) then
return slotID
end
return nil
end
slot = TryBankButtonID(bagID, nil)
slot = AcceptIfBag(slot)
if slot then
bankBagInvSlotCache[index] = slot
return slot
end
slot = TryBankButtonID(index, nil)
slot = AcceptIfBag(slot)
if slot then
bankBagInvSlotCache[index] = slot
return slot
end
if ContainerIDToInventoryID then
local ok, result = pcall(function() return ContainerIDToInventoryID(bagID) end)
if ok then
slot = AcceptIfBankSlot(result)
if slot then
bankBagInvSlotCache[index] = slot
return slot
end
end
end
-- BankBagSlotN is a stable bank bag equipment slot token and can be empty.
slot = AcceptIfBankSlot(SafeGetInventorySlotByName("BankBagSlot" .. index))
if slot then
bankBagInvSlotCache[index] = slot
return slot
end
-- Alternate token fallback used by a few legacy clients.
slot = AcceptIfBankSlot(SafeGetInventorySlotByName("BankBag" .. index))
if slot then
bankBagInvSlotCache[index] = slot
return slot
end
slot = AcceptIfBag(SafeGetInventorySlotByName("BankSlot" .. index))
if slot then
bankBagInvSlotCache[index] = slot
return slot
end
local liveBtn = GetLiveBankBagButton(index)
if liveBtn and liveBtn.GetID then
local btnID = liveBtn:GetID()
slot = TryBankButtonID(btnID, 1) or AcceptIfBankSlot(btnID) or AcceptIfBag(btnID)
if slot then
bankBagInvSlotCache[index] = slot
return slot
end
end
return nil
end
local function GetLiveBankBagIconTexture(index)
local btn = GetLiveBankBagButton(index)
if btn then
local icon = _G[btn:GetName() .. "IconTexture"] or _G[btn:GetName() .. "Icon"]
if icon and icon.GetTexture then
local tex = icon:GetTexture()
if type(tex) == "string" and tex ~= "" then
return tex
end
end
end
local directIcon = _G["BankFrameBag" .. index .. "IconTexture"] or _G["BankFrameBag" .. index .. "Icon"]
if directIcon and directIcon.GetTexture then
local tex = directIcon:GetTexture()
if type(tex) == "string" and tex ~= "" then
return tex
end
end
return nil
end
local function IsBankBagSlotUnlocked(index)
local purchased = 0
if GetNumBankSlots then
purchased = GetNumBankSlots() or 0
end
return index <= purchased
end
local function TryPurchaseBankSlot(index)
if not PurchaseSlot then return false end
if IsBankBagSlotUnlocked(index) then return false end
local purchased = (GetNumBankSlots and GetNumBankSlots()) or 0
local nextSlot = purchased + 1
if index ~= nextSlot then return false end
local ok = pcall(function() PurchaseSlot() end)
return ok
end
local function PlaceCursorItemInBankBagSlot(index)
if not CursorHasItem() then return false end
local invSlot = GetBankBagInvSlotID(index)
if not invSlot then return false end
if EquipCursorItem then
local equipOK = pcall(function() EquipCursorItem(invSlot) end)
if equipOK and (not CursorHasItem()) then
return true
end
end
-- This works for both placing and swapping bags in vanilla.
local ok = pcall(function() PickupBagFromSlot(invSlot) end)
if ok and (not CursorHasItem()) then
return true
end
-- Fallback path for clients where PickupBagFromSlot doesn't place cursor item.
if PutItemInBag then
pcall(function() PutItemInBag(invSlot) end)
end
if CursorHasItem() then
pcall(function() PickupInventoryItem(invSlot) end)
end
return not CursorHasItem()
end
local function GetOfflineBankSlotState(offlineDB, slotIndex)
local purchased = 0
if offlineDB and type(offlineDB.bankSlots) == "number" then
purchased = math.max(0, math.floor(offlineDB.bankSlots))
end
if purchased <= 0 and offlineDB and offlineDB.bankBags then
for i = 1, BANK_BAG_COUNT do
local meta = offlineDB.bankBags[i]
local metaSize = 0
if meta and type(meta.size) == "number" then
metaSize = math.max(0, math.floor(meta.size))
end
if meta and (meta.unlocked or metaSize > 0) then
purchased = math.max(purchased, i)
end
end
end
if purchased <= 0 and offlineDB and offlineDB.bank then
for i = 1, BANK_BAG_COUNT do
local bagData = offlineDB.bank[i + (BANK_BAG_FIRST_ID - 1)]
local size = 0
if bagData and type(bagData.size) == "number" then
size = math.max(0, math.floor(bagData.size))
end
if size > 0 then
purchased = math.max(purchased, i)
end
end
end
local bagID = slotIndex + (BANK_BAG_FIRST_ID - 1)
local bagData = offlineDB and offlineDB.bank and offlineDB.bank[bagID]
local bagSlots = 0
if bagData and type(bagData.size) == "number" then
bagSlots = math.max(0, math.floor(bagData.size))
end
local bagMeta = offlineDB and offlineDB.bankBags and offlineDB.bankBags[slotIndex]
if bagMeta and type(bagMeta.size) == "number" and bagMeta.size > bagSlots then
bagSlots = math.max(0, math.floor(bagMeta.size))
end
-- Migration fallback for old offline snapshots that did not store bankSlots:
-- any slot with positive container size implies this slot is available.
if bagSlots > 0 and slotIndex > purchased then
purchased = slotIndex
end
local unlocked = (slotIndex <= purchased)
if (not unlocked) and bagSlots > 0 then
unlocked = true
end
if (not unlocked) and bagMeta and bagMeta.unlocked then
unlocked = true
end
local link = nil
local tex = nil
local rawLink = bagMeta and bagMeta.link or nil
if bagSlots > 0 then
local linkState = GetBagLinkState(rawLink)
if linkState == "bag" or linkState == "unknown" then
link = rawLink
tex = bagMeta and bagMeta.texture or nil
end
end
return unlocked, bagSlots, link, tex, purchased
end
local function CreateBankBagButton(parent, index)
local btn = CreateFrame("Button", "SFramesBankBagBtn" .. index, parent)
btn:SetWidth(BANK_BAG_SIZE)
btn:SetHeight(BANK_BAG_SIZE)
btn.slotIndex = index
btn:SetFrameStrata(parent:GetFrameStrata())
btn:SetFrameLevel(parent:GetFrameLevel() + 20)
-- Rounded backdrop (matching bag slot style)
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.9)
btn:SetBackdropBorderColor(_A.slotBorder[1], _A.slotBorder[2], _A.slotBorder[3], _A.slotBorder[4] or 0.8)
local icon = btn:CreateTexture(nil, "OVERLAY")
icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2)
icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2)
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
icon:SetBlendMode("BLEND")
icon:SetVertexColor(1, 1, 1, 1)
icon:SetAlpha(1)
btn.icon = icon
local lock = btn:CreateTexture(nil, "OVERLAY")
lock:SetTexture("Interface\\Buttons\\UI-GroupLoot-Pass-Up")
lock:SetWidth(12)
lock:SetHeight(12)
lock:SetPoint("CENTER", btn, "CENTER", 0, 0)
lock:Hide()
btn.lockIcon = lock
local highlight = btn:CreateTexture(nil, "HIGHLIGHT")
highlight:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
highlight:SetBlendMode("ADD")
highlight:SetAllPoints(btn)
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
btn:RegisterForDrag("LeftButton")
btn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:ClearLines()
if SFrames.Bags.Bank.isOffline then
local data = nil
if SFrames.Bags.Bank.offlineChar and SFrames.Bags.Offline and SFrames.Bags.Offline.GetCharacterData then
data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Bank.offlineChar)
end
local unlocked, bagSlots, bagLink = GetOfflineBankSlotState(data, this.slotIndex)
if unlocked then
local shown = false
if bagSlots > 0 and bagLink then
local _, _, itemStr = string.find(bagLink, "(item:[%-?%d:]+)")
local ok = false
if itemStr then
ok = pcall(function() GameTooltip:SetHyperlink(itemStr) end)
else
ok = pcall(function() GameTooltip:SetHyperlink(bagLink) end)
end
shown = ok and HasTooltipText()
if not shown then
local name = GetItemNameFromLink(bagLink)
if name and name ~= "" then
GameTooltip:SetText(name, 1, 1, 1)
shown = true
end
end
end
if not shown then
if bagSlots > 0 then
GameTooltip:SetText(string.format("%s %d (%d%s)", TEXT_BANK_BAG, this.slotIndex, bagSlots, TEXT_SLOT_UNIT), 1, 1, 1)
else
GameTooltip:SetText(string.format("%s %d (%s)", TEXT_BANK_BAG_SLOT, this.slotIndex, TEXT_EMPTY), 0.9, 0.9, 0.9)
end
end
GameTooltip:AddLine(TEXT_OFFLINE, 0.75, 0.75, 0.75)
else
GameTooltip:SetText(TEXT_LOCKED_BANK_SLOT, 1, 0.2, 0.2)
GameTooltip:AddLine(TEXT_OFFLINE, 0.75, 0.75, 0.75)
end
else
local unlocked = this.unlocked
if unlocked then
local invSlot = GetBankBagInvSlotID(this.slotIndex)
local bagSlots = GetContainerNumSlots(this.slotIndex + (BANK_BAG_FIRST_ID - 1)) or 0
local shown = false
if bagSlots > 0 and invSlot and GameTooltip.SetInventoryItem then
local ok = pcall(function() GameTooltip:SetInventoryItem("player", invSlot) end)
if ok and HasTooltipText() then
shown = true
end
end
if not shown then
local link = SafeGetInventoryItemLink("player", invSlot)
if bagSlots > 0 and link then
local ok = pcall(function() GameTooltip:SetHyperlink(link) end)
if ok and HasTooltipText() then
shown = true
end
end
end
if not shown then
if bagSlots > 0 then
GameTooltip:SetText(string.format("%s %d (%d%s)", TEXT_BANK_BAG, this.slotIndex, bagSlots, TEXT_SLOT_UNIT), 1, 1, 1)
else
GameTooltip:SetText(string.format("%s %d (%s)", TEXT_BANK_BAG_SLOT, this.slotIndex, TEXT_EMPTY), 0.9, 0.9, 0.9)
end
end
GameTooltip:AddLine(TEXT_DRAG_EQUIP, 0.7, 0.7, 0.7)
GameTooltip:AddLine(TEXT_RIGHT_PICKUP, 0.7, 0.7, 0.7)
else
GameTooltip:SetText(TEXT_LOCKED_BANK_SLOT, 1, 0.2, 0.2)
local purchased = (GetNumBankSlots and GetNumBankSlots()) or 0
local nextSlot = purchased + 1
if this.slotIndex == nextSlot then
local cost = (GetBankSlotCost and GetBankSlotCost()) or 0
if cost > 0 then
GameTooltip:AddLine(TEXT_CLICK_BUY, 1, 0.82, 0)
GameTooltip:AddLine(GetSafeCoinText(cost), 1, 0.82, 0)
else
GameTooltip:AddLine(TEXT_CLICK_BUY, 1, 0.82, 0)
end
else
GameTooltip:AddLine(TEXT_BUY_PREV_FIRST, 0.7, 0.7, 0.7)
end
end
end
if this.unlocked and SFBankFrame and SFBankFrame:IsVisible() then
if SFrames.Bags.Bank.isOffline then
local data = nil
if SFrames.Bags.Bank.offlineChar and SFrames.Bags.Offline and SFrames.Bags.Offline.GetCharacterData then
data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Bank.offlineChar)
end
local _, offlineBagSlots = GetOfflineBankSlotState(data, this.slotIndex)
if offlineBagSlots > 0 then
SFrames.Bags.Bank:PreviewBankBagSlots(this.slotIndex + (BANK_BAG_FIRST_ID - 1))
end
else
local bagSlots = GetContainerNumSlots(this.slotIndex + (BANK_BAG_FIRST_ID - 1)) or 0
if bagSlots > 0 then
SFrames.Bags.Bank:PreviewBankBagSlots(this.slotIndex + (BANK_BAG_FIRST_ID - 1))
end
end
end
GameTooltip:Show()
end)
btn:SetScript("OnLeave", function()
GameTooltip:Hide()
if SFBankFrame and SFBankFrame:IsVisible() then
SFrames.Bags.Bank:ClearBankBagPreview()
end
end)
btn:SetScript("OnDragStart", function()
if SFrames.Bags.Bank.isOffline then return end
if not this.unlocked then return end
local invSlot = GetBankBagInvSlotID(this.slotIndex)
if invSlot then
pcall(function() PickupBagFromSlot(invSlot) end)
end
end)
btn:SetScript("OnReceiveDrag", function()
if SFrames.Bags.Bank.isOffline then return end
if not CursorHasItem() then return end
if this.unlocked then
PlaceCursorItemInBankBagSlot(this.slotIndex)
else
if TryPurchaseBankSlot(this.slotIndex) then
if IsBankBagSlotUnlocked(this.slotIndex) and CursorHasItem() then
PlaceCursorItemInBankBagSlot(this.slotIndex)
end
end
end
SafeBankUpdateLayout()
end)
btn:SetScript("OnClick", function()
if SFrames.Bags.Bank.isOffline then return end
if CursorHasItem() then
if this.unlocked then
PlaceCursorItemInBankBagSlot(this.slotIndex)
else
if TryPurchaseBankSlot(this.slotIndex) and IsBankBagSlotUnlocked(this.slotIndex) then
PlaceCursorItemInBankBagSlot(this.slotIndex)
end
end
SafeBankUpdateLayout()
return
end
if this.unlocked then
local invSlot = GetBankBagInvSlotID(this.slotIndex)
if invSlot then
pcall(function() PickupBagFromSlot(invSlot) end)
end
else
TryPurchaseBankSlot(this.slotIndex)
end
SafeBankUpdateLayout()
end)
return btn
end
-- Create a single item slot button
local function CreateSlot(parent, id)
local button = CreateFrame("Button", "SFramesBankSlot" .. 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)
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
button:SetScript("OnEnter", function()
if this.bagID == nil or this.slotID == nil then return end
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:ClearLines()
if SFrames.Bags.Bank.isOffline and SFrames.Bags.Bank.offlineChar then
local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Bank.offlineChar)
if data and data.bank and data.bank[this.bagID] and data.bank[this.bagID].items[this.slotID] then
local link = data.bank[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 = SetTooltipFromBankContainerItem(this.bagID, this.slotID, this.itemLink, this.itemName)
if not shown then
GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65)
end
end
if IsControlKeyDown() then
ShowInspectCursor()
end
GameTooltip:Show()
end)
button:SetScript("OnUpdate", function()
if GameTooltip:IsOwned(this) then
if IsControlKeyDown() then
if not this.controlDownLast then
this.controlDownLast = true
ShowInspectCursor()
end
else
if this.controlDownLast then
this.controlDownLast = false
ResetCursor()
end
end
end
end)
button:SetScript("OnLeave", function()
GameTooltip:Hide()
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
if self.bagID == -1 then
SplitContainerItem(-1, self.slotID, split)
else
SplitContainerItem(self.bagID, self.slotID, split)
end
end
button:SetScript("OnClick", function()
local bagID = this.bagID
local slotID = this.slotID
local isOffline = SFrames.Bags.Bank.isOffline
-- Helper: get item link for this slot (works both online and offline)
local function GetSlotLink()
if isOffline and SFrames.Bags.Bank.offlineChar then
local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Bank.offlineChar)
if data and data.bank and data.bank[bagID] and data.bank[bagID].items[slotID] then
return data.bank[bagID].items[slotID].link
end
return nil
end
if bagID == -1 then
return GetContainerItemLink(-1, slotID)
else
return GetContainerItemLink(bagID, slotID)
end
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 AutoStoreBankItem then
local ok = pcall(function() AutoStoreBankItem(bagID, slotID) end)
if ok then return end
end
UseContainerItem(bagID, slotID)
else
if bagID == -1 then
PickupContainerItem(-1, slotID)
else
PickupContainerItem(bagID, slotID)
end
end
end)
button:SetScript("OnDragStart", function()
if SFrames.Bags.Bank.isOffline then return end
if CursorHasItem() then return end
local bagID = this.bagID
local slotID = this.slotID
if bagID == -1 then
PickupContainerItem(-1, slotID)
else
PickupContainerItem(bagID, slotID)
end
end)
button:SetScript("OnReceiveDrag", function()
if SFrames.Bags.Bank.isOffline then return end
if CursorHasItem() then
local bagID = this.bagID
local slotID = this.slotID
if bagID == -1 then
PickupContainerItem(-1, slotID)
else
PickupContainerItem(bagID, slotID)
end
end
end)
return button
end
function SFrames.Bags.Bank:PreviewBankBagSlots(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.Bank:ClearBankBagPreview()
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
end
function SFrames.Bags.Bank:UpdateBankBagButtons()
if not SFBankFrame then return end
local isOffline = self.isOffline
local offlineDB = nil
if isOffline and self.offlineChar and SFrames.Bags.Offline and SFrames.Bags.Offline.GetCharacterData then
offlineDB = SFrames.Bags.Offline:GetCharacterData(self.offlineChar)
if not offlineDB then
isOffline = false
self.isOffline = false
self.offlineChar = nil
end
end
local purchased = 0
if isOffline and offlineDB then
local _, _, _, _, offPurchased = GetOfflineBankSlotState(offlineDB, 1)
purchased = offPurchased or 0
else
purchased = (GetNumBankSlots and GetNumBankSlots()) or 0
end
local nextSlot = purchased + 1
local placeholderTex = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Bag"
for i = 1, BANK_BAG_COUNT do
local btn = BankBagButtons[i]
if btn then
local bagID = i + (BANK_BAG_FIRST_ID - 1)
local unlocked = false
local tex = nil
if isOffline and offlineDB then
local bagSlots, offlineLink, offlineTex
unlocked, bagSlots, offlineLink, offlineTex = GetOfflineBankSlotState(offlineDB, i)
if offlineLink then
tex = GetIconFromItemLink(offlineLink)
end
if (not tex) and offlineLink and IsUsableBankBagIconTexture(offlineTex) then
tex = offlineTex
end
if not tex then
tex = placeholderTex
end
else
local purchasedSlot = (i <= purchased)
local bagSlots = GetContainerNumSlots(bagID) or 0
local invSlot = GetBankBagInvSlotID(i)
local invLink = SafeGetInventoryItemLink("player", invSlot)
local invTex = SafeGetInventoryItemTexture("player", invSlot)
local liveTex = GetLiveBankBagIconTexture(i)
if invLink and IsBagItemLink(invLink) then
tex = GetIconFromItemLink(invLink)
if (not tex) and IsUsableBankBagIconTexture(invTex) then
tex = invTex
end
end
if (not tex) and IsUsableBankBagIconTexture(liveTex) then
tex = liveTex
end
local hasBag = (tex ~= nil) or (bagSlots > 0)
unlocked = purchasedSlot or hasBag
if not tex then
tex = placeholderTex
end
end
btn.unlocked = unlocked
btn.icon:SetTexture(tex)
btn:Enable()
if btn.icon.SetDesaturated then
pcall(function() btn.icon:SetDesaturated(false) end)
end
if btn.bg then btn.bg:SetTexture(0, 0, 0, 0) end
btn:SetAlpha(1)
btn.icon:SetAlpha(1)
if unlocked then
btn.icon:SetVertexColor(1, 1, 1, 1)
btn.lockIcon:Hide()
else
btn.icon:SetVertexColor(0.5, 0.5, 0.5, 1)
if (not isOffline) and i == nextSlot then
btn.lockIcon:Hide()
else
btn.lockIcon:Show()
end
end
btn:Show()
end
end
if SFBankFrame.purchaseBtn then
if isOffline then
SFBankFrame.purchaseBtn:Hide()
else
if nextSlot <= BANK_BAG_COUNT then
local cost = (GetBankSlotCost and GetBankSlotCost()) or 0
SFBankFrame.purchaseBtn.cost = cost
SFBankFrame.purchaseBtn:Show()
else
SFBankFrame.purchaseBtn:Hide()
end
end
end
end
-- Build/Update the item slot grid
function SFrames.Bags.Bank:UpdateLayout()
if not SFBankFrame then return end
local cols = (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bankColumns) or 12
local spacing = (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bankSpacing) 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 SFBankFrame and SFBankFrame.RefreshCharacterSelectorText then
SFBankFrame.RefreshCharacterSelectorText()
end
end
end
-- Update money display (online/offline)
if SFBankFrame.moneyFrame then
local copper = 0
if isOffline and offlineDB then
copper = offlineDB.money or 0
else
copper = GetMoney()
end
SetCoinDisplayMoney(SFBankFrame.moneyFrame, copper)
end
local bankBags = { -1 }
for bag = BANK_BAG_FIRST_ID, BANK_BAG_LAST_ID do
table.insert(bankBags, bag)
end
for _, bag in ipairs(bankBags) do
local size = 0
if isOffline and offlineDB then
if offlineDB.bank and offlineDB.bank[bag] then size = offlineDB.bank[bag].size end
else
if bag == -1 then
-- Try API first, fallback to hardcoded 24
local apiSize = GetContainerNumSlots(-1)
size = (apiSize and apiSize > 0) and apiSize or 24
else
size = GetContainerNumSlots(bag)
end
end
for slot = 1, size do
table.insert(slots, { bag = bag, slot = slot })
end
end
-- Apply search filter (hide non-matching items, show dim-greyed slots instead)
local searchFilter = bankSearchText or ""
if searchFilter ~= "" then
local filtered = {}
for _, meta in ipairs(slots) do
local link
if isOffline and offlineDB then
if offlineDB.bank and offlineDB.bank[meta.bag] and offlineDB.bank[meta.bag].items[meta.slot] then
link = offlineDB.bank[meta.bag].items[meta.slot].link
end
else
if meta.bag == -1 then
link = GetContainerItemLink(-1, meta.slot)
else
link = GetContainerItemLink(meta.bag, meta.slot)
end
end
-- Keep slot if it matches OR if it's empty (so empty slots remain in grid)
local keep = true
if link then
local name = GetItemInfo(link)
if not name then
local _, _, parsedName = string.find(link, "%[(.+)%]")
name = parsedName
end
keep = TextMatchesSearch(name, searchFilter)
end
if keep then
table.insert(filtered, meta)
end
end
slots = filtered
end
local numSlots = table.getn(slots)
if numSlots == 0 then numSlots = 1 end -- Prevent zero-size window
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 + BOTTOM_OFFSET
SFBankFrame:SetWidth(math.max(width, 160))
SFBankFrame:SetHeight(height)
-- Position & update slots
for i, meta in ipairs(slots) do
local btn = ItemSlots[i]
if not btn then
btn = CreateSlot(SFBankFrame, 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", SFBankFrame, "TOPLEFT",
MARGIN + col * (SLOT_SIZE + spacing),
-(MARGIN + TOP_OFFSET + row * (SLOT_SIZE + spacing)))
-- Fetch item info
local texture, count, quality, link
if isOffline and offlineDB then
if offlineDB.bank and offlineDB.bank[meta.bag] and offlineDB.bank[meta.bag].items[meta.slot] then
local item = offlineDB.bank[meta.bag].items[meta.slot]
texture = item.texture
count = item.count
quality = item.quality
link = item.link
end
else
if meta.bag == -1 then
-- Main bank slots: GetContainerItemInfo(-1, slot) is the correct API
-- This requires the bank to already be open and data populated (0.5s delay ensures this)
local t, c, _, q = GetContainerItemInfo(-1, meta.slot)
texture = t; count = c; quality = q
link = GetContainerItemLink(-1, meta.slot)
else
local t, c, _, q = GetContainerItemInfo(meta.bag, meta.slot)
texture = t; count = c; quality = q
link = GetContainerItemLink(meta.bag, meta.slot)
end
end
btn.itemLink = link
if link then
btn.itemName = GetItemInfo(link)
else
btn.itemName = nil
end
SetItemButtonTexture(btn, texture)
SetItemButtonCount(btn, count)
local iconTex = _G[btn:GetName() .. "IconTexture"]
if iconTex then iconTex:SetVertexColor(1, 1, 1) end
if btn.previewGlow then btn.previewGlow:Hide() end
-- Quality border & Grey marker
btn:HideBorder()
btn.junkIcon:Hide()
if link then
-- Safest 1.12 generic approach: Read the color straight out of the hyperlink!
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
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
-- Fallback
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
self:UpdateBankBagButtons()
end
-- Main frame initialization (called once after PLAYER_LOGIN)
function SFrames.Bags.Bank:Initialize()
if SFBankFrame then return end
SFBankFrame = CreateFrame("Frame", "SFramesBankFrame", UIParent)
SFBankFrame:SetWidth(420)
SFBankFrame:SetHeight(200)
SFBankFrame:SetFrameStrata("HIGH")
SFBankFrame:SetToplevel(true)
SFBankFrame:EnableMouse(true)
SFBankFrame:SetMovable(true)
SFBankFrame:SetClampedToScreen(true)
SFBankFrame:RegisterForDrag("LeftButton")
SFBankFrame:SetScript("OnDragStart", function() this:StartMoving() end)
SFBankFrame:SetScript("OnDragStop", function()
this:StopMovingOrSizing()
SaveBankFramePosition()
end)
ApplyBankFramePosition()
tinsert(UISpecialFrames, "SFramesBankFrame")
SFBankFrame:Hide()
-- ESC menu style rounded backdrop
SFBankFrame: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
if _A and _A.panelBg then
SFBankFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], _A.panelBg[4] or 0.95)
SFBankFrame:SetBackdropBorderColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], _A.panelBorder[4] or 0.9)
else
SFBankFrame:SetBackdropColor(0.12, 0.06, 0.10, 0.95)
SFBankFrame:SetBackdropBorderColor(0.55, 0.30, 0.42, 0.9)
end
local bankShadow = CreateFrame("Frame", nil, SFBankFrame)
bankShadow:SetPoint("TOPLEFT", SFBankFrame, "TOPLEFT", -5, 5)
bankShadow:SetPoint("BOTTOMRIGHT", SFBankFrame, "BOTTOMRIGHT", 5, -5)
bankShadow:SetFrameLevel(math.max(SFBankFrame:GetFrameLevel() - 1, 0))
bankShadow: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 },
})
bankShadow:SetBackdropColor(0, 0, 0, 0.55)
bankShadow:SetBackdropBorderColor(0, 0, 0, 0.4)
local scale = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.bankScale) == "number" and SFramesDB.Bags.bankScale) or 0.85
SFBankFrame:SetScale(scale)
local bankAlpha = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.bankAlpha) == "number" and SFramesDB.Bags.bankAlpha) or 1
SFBankFrame:SetAlpha(bankAlpha)
local bankTitleIco = SFrames:CreateIcon(SFBankFrame, "gold", 14)
bankTitleIco:SetDrawLayer("OVERLAY")
bankTitleIco:SetPoint("TOPLEFT", SFBankFrame, "TOPLEFT", 10, -7)
bankTitleIco:SetVertexColor(_A.title[1], _A.title[2], _A.title[3])
local titleFS = SFrames:CreateFontString(SFBankFrame, 12, "LEFT")
titleFS:SetPoint("LEFT", bankTitleIco, "RIGHT", 4, 0)
titleFS:SetText(TEXT_BANK_TITLE)
titleFS:SetTextColor(_A.title[1], _A.title[2], _A.title[3])
SFBankFrame.title = titleFS
-- Close button
local closeBtn = CreateFrame("Button", "SFramesBankClose", SFBankFrame, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", SFBankFrame, "TOPRIGHT", 0, 0)
closeBtn:SetScript("OnClick", function() SFrames.Bags.Bank:Close() end)
-- Money display
SFBankFrame.moneyFrame = CreateCoinDisplay(SFBankFrame, "SFramesBankMoneyFrame")
-- Temporary anchor; final anchor is set after purchase button is created.
SFBankFrame.moneyFrame:SetPoint("RIGHT", SFBankFrame, "BOTTOMRIGHT", -8, 17)
SetCoinDisplayMoney(SFBankFrame.moneyFrame, 0)
-- Search bar (row 2: below title)
local searchEB = CreateFrame("EditBox", "SFramesBankSearchBox", SFBankFrame, "InputBoxTemplate")
searchEB:SetWidth(120)
searchEB:SetHeight(18)
searchEB:SetPoint("TOPLEFT", SFBankFrame, "TOPLEFT", 10, -27)
searchEB:SetAutoFocus(false)
searchEB:SetScript("OnEnterPressed", function() this:ClearFocus() end)
searchEB:SetScript("OnEscapePressed", function()
this:ClearFocus()
this:SetText("")
bankSearchText = ""
SafeBankUpdateLayout()
end)
searchEB:SetScript("OnTextChanged", function()
bankSearchText = this:GetText() or ""
SafeBankUpdateLayout()
end)
SFBankFrame.searchEB = searchEB
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
-- Sort button (icon)
local sortBtn = CreateHeaderIconButton("SFramesBankSortBtn", SFBankFrame, "Interface\\Icons\\INV_Misc_Note_05")
sortBtn:SetPoint("LEFT", searchEB, "RIGHT", 6, 0)
SFrames:SetIcon(sortBtn.icon, "gold")
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()
local reverse = (arg1 == "RightButton")
if SFrames.Bags.Sort and SFrames.Bags.Sort.StartBank then
SFrames.Bags.Sort:StartBank(reverse)
return
end
-- Fallback for environments with a native bank sort API.
SortBank()
local elapsed = 0
local t = CreateFrame("Frame")
t:SetScript("OnUpdate", function()
elapsed = elapsed + arg1
if elapsed >= 0.3 then
this:SetScript("OnUpdate", nil)
SafeBankUpdateLayout()
end
end)
end)
SFBankFrame.sortBtn = sortBtn
-- Bank bag slots (equip/unequip + purchase flow)
for i = 1, BANK_BAG_COUNT do
local bagBtn = CreateBankBagButton(SFBankFrame, i)
if i == 1 then
bagBtn:SetPoint("BOTTOMLEFT", SFBankFrame, "BOTTOMLEFT", 8, 6)
else
bagBtn:SetPoint("LEFT", BankBagButtons[i - 1], "RIGHT", BANK_BAG_SPACING, 0)
end
BankBagButtons[i] = bagBtn
end
local purchaseBtn = CreateHeaderIconButton("SFramesBankPurchaseBtn", SFBankFrame, "Interface\\Icons\\INV_Misc_Coin_01")
purchaseBtn:SetWidth(BANK_BAG_SIZE)
purchaseBtn:SetHeight(BANK_BAG_SIZE)
purchaseBtn:SetPoint("BOTTOMRIGHT", SFBankFrame, "BOTTOMRIGHT", -8, 6)
purchaseBtn.cost = 0
purchaseBtn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:ClearLines()
GameTooltip:SetText(TEXT_BUY_SLOT, 1, 1, 1)
if SFrames.Bags.Bank.isOffline then
GameTooltip:AddLine(TEXT_UNAVAILABLE_OFFLINE, 0.75, 0.75, 0.75)
else
local cost = this.cost or 0
if cost > 0 then
GameTooltip:AddLine(GetSafeCoinText(cost), 1, 0.82, 0)
end
GameTooltip:AddLine(TEXT_CLICK_BUY, 0.75, 0.75, 0.75)
end
GameTooltip:Show()
end)
purchaseBtn:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
purchaseBtn:SetScript("OnClick", function()
if SFrames.Bags.Bank.isOffline then return end
if PurchaseSlot then
pcall(function() PurchaseSlot() end)
SafeBankUpdateLayout()
end
end)
SFBankFrame.purchaseBtn = purchaseBtn
-- Keep bank money display on the same bottom row as bank bag controls.
if SFBankFrame.moneyFrame then
SFBankFrame.moneyFrame:ClearAllPoints()
SFBankFrame.moneyFrame:SetPoint("RIGHT", purchaseBtn, "LEFT", -6, 0)
end
-- Character selector: button-triggered dropdown (stable text, avoids UIDropDown label glitches)
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 charBtn = CreateHeaderIconButton("SFramesBankCharBtn", SFBankFrame, CHARACTER_SELECTOR_ICON)
charBtn:SetPoint("TOPRIGHT", SFBankFrame, "TOPRIGHT", -8, -26)
SFBankFrame.charSelectBtn = charBtn
local dd = CreateFrame("Frame", "SFramesBankOfflineDD", SFBankFrame, "UIDropDownMenuTemplate")
dd:Hide()
dd:SetPoint("TOPRIGHT", charBtn, "BOTTOMRIGHT", 0, 0)
local function RefreshCharacterSelectorText()
if not SFBankFrame or not SFBankFrame.charSelectBtn then return end
if SFrames.Bags.Bank.isOffline and SFrames.Bags.Bank.offlineChar then
SFBankFrame.charSelectorLabel = SFrames.Bags.Bank.offlineChar .. " (" .. TEXT_OFFLINE .. ")"
else
SFBankFrame.charSelectorLabel = GetCurrentCharacterName() .. " (" .. TEXT_ONLINE .. ")"
end
end
SFBankFrame.RefreshCharacterSelectorText = RefreshCharacterSelectorText
local function OnSelect()
local char = this.value
if char == "CURRENT" then
SFrames.Bags.Bank.isOffline = false
SFrames.Bags.Bank.offlineChar = nil
else
SFrames.Bags.Bank.isOffline = true
SFrames.Bags.Bank.offlineChar = char
end
RefreshCharacterSelectorText()
SafeBankUpdateLayout()
end
UIDropDownMenu_Initialize(dd, function()
local info = UIDropDownMenu_CreateInfo and UIDropDownMenu_CreateInfo() or {}
local currentName = GetCurrentCharacterName()
info.text = currentName .. " (" .. TEXT_ONLINE .. ")"
info.value = "CURRENT"
info.func = OnSelect
info.checked = (not SFrames.Bags.Bank.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 = OnSelect
info.checked = SFrames.Bags.Bank.isOffline and SFrames.Bags.Bank.offlineChar == char
UIDropDownMenu_AddButton(info)
end
end
end)
charBtn:SetScript("OnClick", function()
ToggleDropDownMenu(1, nil, dd, this, 0, 0)
end)
charBtn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(SFBankFrame.charSelectorLabel or TEXT_CHARACTER, 1, 1, 1)
GameTooltip:Show()
end)
charBtn:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
RefreshCharacterSelectorText()
-- Use BANKFRAME_OPENED / CLOSED events (same as Guda)
-- IMPORTANT: WoW 1.12 needs a short delay before GetContainerItemInfo(-1,slot)
-- returns valid data after BANKFRAME_OPENED fires.
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("BANKFRAME_OPENED")
eventFrame:RegisterEvent("BANKFRAME_CLOSED")
eventFrame:RegisterEvent("BAG_UPDATE")
eventFrame:RegisterEvent("PLAYERBANKSLOTS_CHANGED")
eventFrame:RegisterEvent("PLAYERBANKBAGSLOTS_CHANGED")
eventFrame:RegisterEvent("PLAYER_MONEY")
eventFrame:SetScript("OnEvent", function()
if not (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.enable) then return end
if event == "BANKFRAME_OPENED" then
-- CRITICAL: Do NOT call BankFrame:Hide() here!
-- BankFrame has an OnHide script that calls CloseBankFrame(), which
-- immediately closes the bank session and makes GetContainerItemInfo(-1,slot) return nil.
-- Instead, move BankFrame off-screen so the session stays open but it's invisible.
local bf = _G["BankFrame"]
if bf then
bf:ClearAllPoints()
bf:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -5000, 5000)
bf:EnableMouse(false) -- Prevent accidental clicks
end
-- Open once; Open() already performs a delayed refresh.
SFrames.Bags.Bank:Open()
elseif event == "BANKFRAME_CLOSED" then
if not isClosing then
SFrames.Bags.Bank:Close()
end
elseif event == "BAG_UPDATE" or event == "PLAYERBANKSLOTS_CHANGED" or event == "PLAYERBANKBAGSLOTS_CHANGED" or event == "PLAYER_MONEY" then
if event == "PLAYERBANKBAGSLOTS_CHANGED" then
bankBagInvSlotCache = {}
elseif event == "BAG_UPDATE" and type(arg1) == "number" and arg1 >= BANK_BAG_FIRST_ID and arg1 <= BANK_BAG_LAST_ID then
bankBagInvSlotCache = {}
end
if SFBankFrame:IsVisible() and not SFrames.Bags.Bank.isOffline then
SafeBankUpdateLayout()
end
end
end)
end
function SFrames.Bags.Bank:Open()
if not SFBankFrame then return end
self.isOffline = false
self.offlineChar = nil
bankBagInvSlotCache = {}
if SFBankFrame and SFBankFrame.RefreshCharacterSelectorText then
SFBankFrame.RefreshCharacterSelectorText()
end
-- Bank-open layout: bank on left, bags on right.
if SFBankFrame then
SFBankFrame:ClearAllPoints()
SFBankFrame:SetPoint("CENTER", UIParent, "CENTER", -360, 0)
end
local bagFrame = _G["SFramesBagFrame"]
if bagFrame then
bagFrame:ClearAllPoints()
bagFrame:SetPoint("CENTER", UIParent, "CENTER", 360, 0)
end
SFBankFrame:Show()
local elapsed = 0
local nextRefresh = 1
local refreshPoints = { 0.06, 0.20, 0.45, 0.85, 1.30 }
local refreshTimer = CreateFrame("Frame")
refreshTimer:SetScript("OnUpdate", function()
elapsed = elapsed + arg1
if nextRefresh <= table.getn(refreshPoints) and elapsed >= refreshPoints[nextRefresh] then
if SFBankFrame:IsVisible() and not SFrames.Bags.Bank.isOffline then
SafeBankUpdateLayout()
end
nextRefresh = nextRefresh + 1
end
if nextRefresh > table.getn(refreshPoints) then
this:SetScript("OnUpdate", nil)
end
end)
-- Keep bank bag state synced: some clients delay slot/container updates.
local syncFrame = CreateFrame("Frame")
syncFrame.timer = 0
syncFrame:SetScript("OnUpdate", function()
if not (SFBankFrame and SFBankFrame:IsVisible()) then
this.timer = 0
return
end
if SFrames.Bags.Bank.isOffline then
this.timer = 0
return
end
this.timer = this.timer + (arg1 or 0)
if this.timer >= 0.5 then
this.timer = 0
SFrames.Bags.Bank:UpdateBankBagButtons()
end
end)
end
function SFrames.Bags.Bank:OpenOffline(charName)
if not SFBankFrame then
if self.Initialize then
self:Initialize()
end
end
if not SFBankFrame then
return false
end
local targetChar = nil
if type(charName) == "string" and charName ~= "" then
targetChar = charName
elseif SFrames.Bags.Offline and SFrames.Bags.Offline.GetCurrentPlayerName then
targetChar = SFrames.Bags.Offline:GetCurrentPlayerName()
end
if not targetChar or targetChar == "" then
if SFrames and SFrames.Print then
SFrames:Print(TEXT_UNAVAILABLE_OFFLINE)
end
return false
end
local data = nil
if SFrames.Bags.Offline and SFrames.Bags.Offline.GetCharacterData then
data = SFrames.Bags.Offline:GetCharacterData(targetChar)
end
if not data then
if SFrames and SFrames.Print then
SFrames:Print(TEXT_UNAVAILABLE_OFFLINE)
end
return false
end
self.isOffline = true
self.offlineChar = targetChar
bankBagInvSlotCache = {}
if SFBankFrame.RefreshCharacterSelectorText then
SFBankFrame.RefreshCharacterSelectorText()
end
-- Match live-bank layout so bag and bank are shown side by side.
SFBankFrame:ClearAllPoints()
SFBankFrame:SetPoint("CENTER", UIParent, "CENTER", -360, 0)
local bagFrame = _G["SFramesBagFrame"]
if bagFrame then
bagFrame:ClearAllPoints()
bagFrame:SetPoint("CENTER", UIParent, "CENTER", 360, 0)
end
SFBankFrame:Show()
SafeBankUpdateLayout()
return true
end
function SFrames.Bags.Bank:Close()
if isClosing then return end -- Already closing, prevent recursion
isClosing = true
local wasOffline = self.isOffline
if SFBankFrame then SFBankFrame:Hide() end
-- Restore BankFrame so WoW's close sequence works properly,
-- then let CloseBankFrame() trigger its own OnHide naturally.
local bf = _G["BankFrame"]
if bf then
bf:EnableMouse(true)
bf:ClearAllPoints()
bf:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
end
if (not wasOffline) and CloseBankFrame then
CloseBankFrame() -- Tell server to close bank session (triggers BankFrame OnHide and closes it)
end
isClosing = false
end
function SFrames.Bags.Bank:Toggle()
if SFBankFrame and SFBankFrame:IsVisible() then
self:Close()
else
self:Open()
end
end
SLASH_NANAMIBANKDBG1 = "/nanamibankdbg"
SlashCmdList["NANAMIBANKDBG"] = function()
if not (SFrames and SFrames.Print) then return end
local purchased = (GetNumBankSlots and GetNumBankSlots()) or 0
SFrames:Print("BankDbg purchased=" .. tostring(purchased))
for i = 1, BANK_BAG_COUNT do
local bagID = i + (BANK_BAG_FIRST_ID - 1)
local bagSlots = GetContainerNumSlots(bagID) or 0
local invSlot = GetBankBagInvSlotID(i)
local link = invSlot and SafeGetInventoryItemLink("player", invSlot) or nil
local isBag = link and IsBagItemLink(link)
local liveTex = GetLiveBankBagIconTexture(i)
local unlocked = IsBankBagSlotUnlocked(i)
local mapBag1 = nil
local mapIdx1 = nil
if BankButtonIDToInvSlotID then
local okA, resA = pcall(function() return BankButtonIDToInvSlotID(bagID, 1) end)
if okA then mapBag1 = resA end
local okB, resB = pcall(function() return BankButtonIDToInvSlotID(i, 1) end)
if okB then mapIdx1 = resB end
end
local line =
"slot" .. i ..
" bagID=" .. tostring(bagID) ..
" size=" .. tostring(bagSlots) ..
" unlocked=" .. tostring(unlocked) ..
" invSlot=" .. tostring(invSlot) ..
" link=" .. tostring(link) ..
" isBag=" .. tostring(isBag) ..
" liveTex=" .. tostring(liveTex) ..
" map(bag,1)=" .. tostring(mapBag1) ..
" map(idx,1)=" .. tostring(mapIdx1)
SFrames:Print(line)
end
end