2088 lines
72 KiB
Lua
2088 lines
72 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 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
|