-------------------------------------------------------------------------------- -- 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 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:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetText(TEXT_SORT, 1, 1, 1) GameTooltip:Show() end) sortBtn:SetScript("OnLeave", function() GameTooltip:Hide() end) sortBtn:SetScript("OnClick", function() if SFrames.Bags.Sort and SFrames.Bags.Sort.StartBank then SFrames.Bags.Sort:StartBank() 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