-------------------------------------------------------------------------------- -- Nanami-UI: Trainer UI (TrainerUI.lua) -- Replaces ClassTrainerFrame with Nanami-UI styled interface -------------------------------------------------------------------------------- SFrames = SFrames or {} SFrames.TrainerUI = {} local TUI = SFrames.TrainerUI SFramesDB = SFramesDB or {} -------------------------------------------------------------------------------- -- Theme (Pink Cat-Paw) -------------------------------------------------------------------------------- local T = SFrames.Theme:Extend({ moneyGold = { 1, 0.84, 0.0 }, moneySilver = { 0.78, 0.78, 0.78 }, moneyCopper = { 0.71, 0.43, 0.18 }, available = { 0.25, 1.0, 0.25 }, unavailable = { 0.80, 0.20, 0.20 }, used = { 0.50, 0.50, 0.50 }, }) local QUALITY_COLORS = { [0] = { 0.62, 0.62, 0.62 }, [1] = { 1, 1, 1 }, [2] = { 0.12, 1, 0 }, [3] = { 0.0, 0.44, 0.87 }, [4] = { 0.64, 0.21, 0.93 }, [5] = { 1, 0.5, 0 }, } local function ColorToQuality(r, g, b) if not r then return nil end if r > 0.9 and g > 0.35 and g < 0.65 and b < 0.15 then return 5 end if r > 0.5 and r < 0.8 and g < 0.35 and b > 0.8 then return 4 end if r < 0.15 and g > 0.3 and g < 0.6 and b > 0.7 then return 3 end if r < 0.25 and g > 0.85 and b < 0.15 then return 2 end if r > 0.5 and r < 0.75 and g > 0.5 and g < 0.75 and b > 0.5 and b < 0.75 then return 0 end return 1 end -------------------------------------------------------------------------------- -- Layout (Left-Right structure like TradeSkill frame) -------------------------------------------------------------------------------- local FRAME_W = 620 local FRAME_H = 480 local HEADER_H = 34 local SIDE_PAD = 10 local FILTER_H = 28 local LIST_W = 320 local DETAIL_W = FRAME_W - LIST_W - SIDE_PAD * 3 local LIST_ROW_H = 28 local CAT_ROW_H = 20 local BOTTOM_H = 48 local SCROLL_STEP = 40 local MAX_ROWS = 60 -------------------------------------------------------------------------------- -- State -------------------------------------------------------------------------------- local MainFrame = nil local selectedIndex = nil local currentFilter = "all" local displayList = {} local rowButtons = {} local collapsedCats = {} local isTradeskillTrainerCached = false local function HideBlizzardTrainer() if not ClassTrainerFrame then return end ClassTrainerFrame:SetScript("OnHide", function() end) ClassTrainerFrame:UnregisterAllEvents() if ClassTrainerFrame:IsVisible() then if HideUIPanel then pcall(HideUIPanel, ClassTrainerFrame) else ClassTrainerFrame:Hide() end end ClassTrainerFrame:SetAlpha(0) ClassTrainerFrame:EnableMouse(false) ClassTrainerFrame:ClearAllPoints() ClassTrainerFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 2000, 2000) end -------------------------------------------------------------------------------- -- Helpers -------------------------------------------------------------------------------- local function GetFont() if SFrames and SFrames.GetFont then return SFrames:GetFont() end return "Fonts\\ARIALN.TTF" end local function SetRoundBackdrop(frame) frame: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 }, }) frame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) frame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) end local function CreateShadow(parent) local s = CreateFrame("Frame", nil, parent) s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) s: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 }, }) s:SetBackdropColor(0, 0, 0, 0.45) s:SetBackdropBorderColor(0, 0, 0, 0.6) s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1)) return s end local function FormatMoneyText(copper) if not copper or copper <= 0 then return "" end local g = math.floor(copper / 10000) local s = math.floor(math.mod(copper, 10000) / 100) local c = math.mod(copper, 100) local parts = {} if g > 0 then table.insert(parts, "|cffffd700" .. g .. "g|r") end if s > 0 then table.insert(parts, "|cffc7c7cf" .. s .. "s|r") end if c > 0 then table.insert(parts, "|cffeda55f" .. c .. "c|r") end if table.getn(parts) == 0 then return "|cffc7c7cf0c|r" end return table.concat(parts, " ") end local function SetMoneyFrame(moneyFrame, value) if not moneyFrame then return end value = tonumber(value) or 0 if value < 0 then value = 0 end if SmallMoneyFrame_SetAmount then pcall(SmallMoneyFrame_SetAmount, moneyFrame, value) elseif MoneyFrame_Update and moneyFrame.GetName then local name = moneyFrame:GetName() if name and name ~= "" then pcall(MoneyFrame_Update, name, value) end end end local function IsServiceHeader(index) local name, _, category = GetTrainerServiceInfo(index) if not name then return false end local hasIcon = GetTrainerServiceIcon and GetTrainerServiceIcon(index) if not hasIcon or hasIcon == "" then return true end local ok, cost = pcall(GetTrainerServiceCost, index) if not ok then return true end if category ~= "available" and category ~= "unavailable" and category ~= "used" then return true end return false end -------------------------------------------------------------------------------- -- Enhanced category detection for better filtering -- Trust Blizzard API for class trainers (considers spell rank requirements) -- Only do extra verification for tradeskill trainers -------------------------------------------------------------------------------- local function GetVerifiedCategory(index) local name, _, category = GetTrainerServiceInfo(index) if not name then return nil end if category == "available" or category == "unavailable" or category == "used" then return category end return "unavailable" end local scanTip = nil local function GetServiceTooltipInfo(index) return "", "" end local function GetServiceQuality(index) return nil end -------------------------------------------------------------------------------- -- Filter Button Factory -------------------------------------------------------------------------------- local function CreateFilterBtn(parent, text, w) local btn = CreateFrame("Button", nil, parent) btn:SetWidth(w or 60) btn:SetHeight(20) btn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.5) local fs = btn:CreateFontString(nil, "OVERLAY") fs:SetFont(GetFont(), 11, "OUTLINE") fs:SetPoint("CENTER", 0, 0) fs:SetText(text) fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) btn.label = fs btn:SetScript("OnEnter", function() this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) end) btn:SetScript("OnLeave", function() if this.active then this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) this:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 1) else this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.5) end end) function btn:SetActive(flag) self.active = flag if flag then self:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) self:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 1) self.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) else self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) self:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.5) self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end end return btn end -------------------------------------------------------------------------------- -- Action Button Factory -------------------------------------------------------------------------------- local function CreateActionBtn(parent, text, w) local btn = CreateFrame("Button", nil, parent) btn:SetWidth(w or 100) btn:SetHeight(28) btn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) local fs = btn:CreateFontString(nil, "OVERLAY") fs:SetFont(GetFont(), 12, "OUTLINE") fs:SetPoint("CENTER", 0, 0) fs:SetText(text) fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) btn.label = fs btn.disabled = false function btn:SetDisabled(flag) self.disabled = flag if flag then self.label:SetTextColor(T.btnDisabledText[1], T.btnDisabledText[2], T.btnDisabledText[3]) self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], 0.5) else self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) end end btn:SetScript("OnEnter", function() if not this.disabled then this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end end) btn:SetScript("OnLeave", function() if not this.disabled then this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end end) return btn end -------------------------------------------------------------------------------- -- Quality cache (lazy per-row instead of bulk tooltip scan) -------------------------------------------------------------------------------- local qualityCache = {} local function GetCachedServiceQuality(index) return nil end -------------------------------------------------------------------------------- -- List Row Factory (reusable for both headers and services) -------------------------------------------------------------------------------- local function CreateListRow(parent, idx) local row = CreateFrame("Button", nil, parent) row:SetWidth(LIST_W - 30) -- Account for scrollbar row:SetHeight(LIST_ROW_H) local iconFrame = CreateFrame("Frame", nil, row) iconFrame:SetWidth(LIST_ROW_H - 4) iconFrame:SetHeight(LIST_ROW_H - 4) iconFrame:SetPoint("LEFT", row, "LEFT", 0, 0) iconFrame: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 }, }) iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) iconFrame:EnableMouse(false) -- Don't block mouse events row.iconFrame = iconFrame local icon = iconFrame:CreateTexture(nil, "ARTWORK") icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) icon:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", 3, -3) icon:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -3, 3) row.icon = icon local qualGlow = iconFrame:CreateTexture(nil, "OVERLAY") qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") qualGlow:SetBlendMode("ADD") qualGlow:SetAlpha(0.7) qualGlow:SetWidth((LIST_ROW_H - 4) * 1.8) qualGlow:SetHeight((LIST_ROW_H - 4) * 1.8) qualGlow:SetPoint("CENTER", iconFrame, "CENTER", 0, 0) qualGlow:Hide() row.qualGlow = qualGlow local nameFS = row:CreateFontString(nil, "OVERLAY") nameFS:SetFont(GetFont(), 11, "OUTLINE") nameFS:SetPoint("LEFT", iconFrame, "RIGHT", 6, 5) nameFS:SetPoint("RIGHT", row, "RIGHT", -90, 0) nameFS:SetJustifyH("LEFT") nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) row.nameFS = nameFS local subFS = row:CreateFontString(nil, "OVERLAY") subFS:SetFont(GetFont(), 9, "OUTLINE") subFS:SetPoint("LEFT", iconFrame, "RIGHT", 6, -6) subFS:SetPoint("RIGHT", row, "RIGHT", -90, 0) subFS:SetJustifyH("LEFT") subFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) row.subFS = subFS local costFS = row:CreateFontString(nil, "OVERLAY") costFS:SetFont(GetFont(), 11, "OUTLINE") costFS:SetPoint("RIGHT", row, "RIGHT", -4, 0) costFS:SetJustifyH("RIGHT") costFS:Hide() row.costFS = costFS local costMoney = CreateFrame("Frame", "SFTRow" .. idx .. "Money", row, "SmallMoneyFrameTemplate") costMoney:SetPoint("RIGHT", row, "RIGHT", -2, 0) costMoney:SetWidth(100) costMoney:SetHeight(14) costMoney:SetFrameLevel(row:GetFrameLevel() + 2) costMoney:SetScale(0.85) costMoney:UnregisterAllEvents() costMoney:SetScript("OnEvent", nil) costMoney:SetScript("OnShow", nil) costMoney:EnableMouse(false) -- Don't block mouse events -- Disable mouse on child buttons too local moneyName = "SFTRow" .. idx .. "Money" for _, suffix in ipairs({"GoldButton", "SilverButton", "CopperButton"}) do local child = _G[moneyName .. suffix] if child and child.EnableMouse then child:EnableMouse(false) end end costMoney.moneyType = nil costMoney.hasPickup = nil costMoney.small = 1 row.costMoney = costMoney local catFS = row:CreateFontString(nil, "OVERLAY") catFS:SetFont(GetFont(), 11, "OUTLINE") catFS:SetPoint("LEFT", row, "LEFT", 4, 0) catFS:SetJustifyH("LEFT") catFS:SetTextColor(T.catHeader[1], T.catHeader[2], T.catHeader[3]) catFS:Hide() row.catFS = catFS local catSep = row:CreateTexture(nil, "ARTWORK") catSep:SetTexture("Interface\\Buttons\\WHITE8X8") catSep:SetHeight(1) catSep:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0) catSep:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0) catSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.3) catSep:Hide() row.catSep = catSep local highlight = row:CreateTexture(nil, "HIGHLIGHT") highlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") highlight:SetBlendMode("ADD") highlight:SetAllPoints(row) highlight:SetAlpha(0.3) row.highlight = highlight local selectedBg = row:CreateTexture(nil, "ARTWORK") selectedBg:SetTexture("Interface\\Buttons\\WHITE8X8") selectedBg:SetAllPoints(row) selectedBg:SetVertexColor(T.selectedRowBg[1], T.selectedRowBg[2], T.selectedRowBg[3], 0.40) selectedBg:Hide() row.selectedBg = selectedBg local selectedGlow = row:CreateTexture(nil, "ARTWORK") selectedGlow:SetTexture("Interface\\Buttons\\WHITE8X8") selectedGlow:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0) selectedGlow:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0) selectedGlow:SetWidth(4) selectedGlow:SetVertexColor(1, 0.65, 0.85, 1) selectedGlow:Hide() row.selectedGlow = selectedGlow local selTop = row:CreateTexture(nil, "OVERLAY") selTop:SetTexture("Interface\\Buttons\\WHITE8X8") selTop:SetHeight(1) selTop:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0) selTop:SetPoint("TOPRIGHT", row, "TOPRIGHT", 0, 0) selTop:SetVertexColor(T.selectedRowBorder[1], T.selectedRowBorder[2], T.selectedRowBorder[3], T.selectedRowBorder[4]) selTop:Hide() row.selTop = selTop local selBot = row:CreateTexture(nil, "OVERLAY") selBot:SetTexture("Interface\\Buttons\\WHITE8X8") selBot:SetHeight(1) selBot:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0) selBot:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0) selBot:SetVertexColor(T.selectedRowBorder[1], T.selectedRowBorder[2], T.selectedRowBorder[3], T.selectedRowBorder[4]) selBot:Hide() row.selBot = selBot row.serviceIndex = nil row.isHeader = false row:SetScript("OnEnter", function() if this.serviceIndex and not this.isHeader then GameTooltip:SetOwner(this, "ANCHOR_RIGHT") local ok = pcall(GameTooltip.SetTrainerService, GameTooltip, this.serviceIndex) if ok then GameTooltip:Show() else GameTooltip:Hide() end end end) row:SetScript("OnLeave", function() GameTooltip:Hide() end) function row:SetAsHeader(name, collapsed) self.isHeader = true self:SetHeight(CAT_ROW_H) self.iconFrame:Hide() self.qualGlow:Hide() self.nameFS:Hide() self.subFS:Hide() self.costFS:Hide() self.costMoney:Hide() self.selectedBg:Hide() self.selectedGlow:Hide() self.selTop:Hide() self.selBot:Hide() self.highlight:SetAlpha(0.15) local arrow = collapsed and "+" or "-" self.catFS:SetText(arrow .. " " .. (name or "")) self.catFS:Show() self.catSep:Show() end function row:SetAsService(svc) self.isHeader = false self.serviceIndex = svc.index self:SetHeight(LIST_ROW_H) self.iconFrame:Show() self.nameFS:Show() self.subFS:Show() self.costFS:Hide() self.costMoney:Show() self.catFS:Hide() self.catSep:Hide() self.highlight:SetAlpha(0.3) local iconTex = GetTrainerServiceIcon and GetTrainerServiceIcon(svc.index) self.icon:SetTexture(iconTex) self.nameFS:SetText(svc.name) self.subFS:SetText(svc.subText) if svc.category == "available" then self.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3]) self.icon:SetVertexColor(1, 1, 1) elseif svc.category == "unavailable" then self.nameFS:SetTextColor(T.unavailable[1], T.unavailable[2], T.unavailable[3]) self.icon:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) else self.nameFS:SetTextColor(T.used[1], T.used[2], T.used[3]) self.icon:SetVertexColor(T.passive[1], T.passive[2], T.passive[3]) end self.qualGlow:Hide() self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) local ok, cost = pcall(GetTrainerServiceCost, svc.index) if ok and cost and cost > 0 then SetMoneyFrame(self.costMoney, cost) else SetMoneyFrame(self.costMoney, 0) end end function row:Clear() self.serviceIndex = nil self.isHeader = false self.qualGlow:Hide() self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) self.selectedBg:Hide() self.selectedGlow:Hide() self.selTop:Hide() self.selBot:Hide() self.costMoney:Hide() self:Hide() end -- Enable mouse wheel scrolling on rows to pass through to scrollbar row:EnableMouseWheel(true) row:SetScript("OnMouseWheel", function() local scrollBar = MainFrame and MainFrame.scrollBar if scrollBar then local cur = scrollBar:GetValue() local min, max = scrollBar:GetMinMaxValues() if arg1 > 0 then scrollBar:SetValue(math.max(min, cur - SCROLL_STEP)) else scrollBar:SetValue(math.min(max, cur + SCROLL_STEP)) end end end) return row end -------------------------------------------------------------------------------- -- Build Display List (with categories) -------------------------------------------------------------------------------- local function BuildDisplayList() displayList = {} local numServices = GetNumTrainerServices and GetNumTrainerServices() or 0 if numServices == 0 then return end local currentCat = nil -- Each category stores three buckets: available, used, unavailable local catBuckets = {} local catOrder = {} for i = 1, numServices do local name, subText, category = GetTrainerServiceInfo(i) if name then local isHdr = IsServiceHeader(i) if isHdr then currentCat = name if not catBuckets[name] then catBuckets[name] = { available = {}, used = {}, unavailable = {} } table.insert(catOrder, name) end else if not currentCat then currentCat = "技能" if not catBuckets[currentCat] then catBuckets[currentCat] = { available = {}, used = {}, unavailable = {} } table.insert(catOrder, currentCat) end end -- Use verified category for more accurate filtering local cat = GetVerifiedCategory(i) or "unavailable" local show = (currentFilter == "all") or (currentFilter == cat) if show then local bucket = catBuckets[currentCat][cat] or catBuckets[currentCat]["unavailable"] table.insert(bucket, { index = i, name = name, subText = subText or "", category = cat, }) end end end end local hasCats = table.getn(catOrder) > 1 for _, catName in ipairs(catOrder) do local buckets = catBuckets[catName] local totalCount = table.getn(buckets.available) + table.getn(buckets.used) + table.getn(buckets.unavailable) if totalCount > 0 then if hasCats then table.insert(displayList, { type = "header", name = catName, collapsed = collapsedCats[catName], }) end if not collapsedCats[catName] then -- available → used → unavailable for _, svc in ipairs(buckets.available) do table.insert(displayList, { type = "service", data = svc }) end for _, svc in ipairs(buckets.used) do table.insert(displayList, { type = "service", data = svc }) end for _, svc in ipairs(buckets.unavailable) do table.insert(displayList, { type = "service", data = svc }) end end end end end -------------------------------------------------------------------------------- -- Update Functions -------------------------------------------------------------------------------- local function UpdateList() if not MainFrame or not MainFrame:IsVisible() then return end BuildDisplayList() local content = MainFrame.listScroll.content local count = table.getn(displayList) local y = 0 for i = 1, MAX_ROWS do local row = rowButtons[i] if i <= count then local entry = displayList[i] row:ClearAllPoints() if entry.type == "header" then row:SetAsHeader(entry.name, entry.collapsed) row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) row.catName = entry.name row:Show() y = y + CAT_ROW_H else row:SetAsService(entry.data) row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) row.catName = nil row:Show() y = y + LIST_ROW_H if selectedIndex == entry.data.index then row.iconFrame:SetBackdropBorderColor(1, 0.65, 0.85, 1) row.iconFrame:SetBackdropColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.5) row.selectedBg:Show() row.selectedGlow:Show() row.selTop:Show() row.selBot:Show() row.nameFS:SetTextColor(T.selectedNameText[1], T.selectedNameText[2], T.selectedNameText[3]) else row.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) row.iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) row.selectedBg:Hide() row.selectedGlow:Hide() row.selTop:Hide() row.selBot:Hide() end end else row:Clear() end end content:SetHeight(math.max(1, y)) -- Update scrollbar range if MainFrame.scrollBar then local visibleHeight = MainFrame.listScroll:GetHeight() or 1 local maxScroll = math.max(0, y - visibleHeight) MainFrame.scrollBar:SetMinMaxValues(0, maxScroll) if maxScroll <= 0 then MainFrame.scrollBar:Hide() else MainFrame.scrollBar:Show() end end end local function UpdateDetail() if not MainFrame then return end local detail = MainFrame.detail if not selectedIndex then detail.iconFrame:Hide() detail.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) detail.nameFS:SetText("") detail.subFS:SetText("") detail.reqFS:SetText("") detail.infoFS:SetText("") detail.descFS:SetText("") detail.descDivider:Hide() MainFrame.trainBtn:SetDisabled(true) SetMoneyFrame(MainFrame.costMoney, 0) SetMoneyFrame(MainFrame.availMoney, 0) MainFrame.costLabel:SetText("花费:") return end local name, subText, _ = GetTrainerServiceInfo(selectedIndex) local category = GetVerifiedCategory(selectedIndex) local iconTex = GetTrainerServiceIcon and GetTrainerServiceIcon(selectedIndex) local ok, cost = pcall(GetTrainerServiceCost, selectedIndex) if not ok then cost = 0 end local levelReq = 0 if GetTrainerServiceLevelReq then local ok2, lr = pcall(GetTrainerServiceLevelReq, selectedIndex) if ok2 then levelReq = lr or 0 end end detail.icon:SetTexture(iconTex) detail.iconFrame:Show() detail.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) detail.nameFS:SetText(name or "") if category == "available" then detail.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3]) elseif category == "unavailable" then detail.nameFS:SetTextColor(T.unavailable[1], T.unavailable[2], T.unavailable[3]) else detail.nameFS:SetTextColor(T.used[1], T.used[2], T.used[3]) end detail.subFS:SetText(subText or "") local reqParts = {} if levelReq and levelReq > 0 then local pLvl = UnitLevel("player") or 60 local color = pLvl >= levelReq and "|cff40ff40" or "|cffff4040" table.insert(reqParts, color .. "需要等级 " .. levelReq .. "|r") end if GetTrainerServiceSkillReq then local ok3, skillName, skillRank, hasReq = pcall(GetTrainerServiceSkillReq, selectedIndex) if ok3 and skillName and skillName ~= "" then local color = hasReq and "|cff40ff40" or "|cffff4040" table.insert(reqParts, color .. "需要 " .. skillName .. " (" .. (skillRank or 0) .. ")|r") end end detail.reqFS:SetText(table.concat(reqParts, " ")) detail.infoFS:SetText("") detail.descFS:SetText("") detail.descDivider:Hide() detail.descScroll:GetScrollChild():SetHeight(1) detail.descScroll:SetVerticalScroll(0) local canTrain = (category == "available") and cost and (GetMoney() >= cost) MainFrame.trainBtn:SetDisabled(not canTrain) if cost and cost > 0 then MainFrame.costLabel:SetText("花费:") SetMoneyFrame(MainFrame.costMoney, cost) else MainFrame.costLabel:SetText("花费: 免费") SetMoneyFrame(MainFrame.costMoney, 0) end SetMoneyFrame(MainFrame.availMoney, GetMoney()) end local function UpdateFilters() if not MainFrame then return end MainFrame.filterAll:SetActive(currentFilter == "all") MainFrame.filterAvail:SetActive(currentFilter == "available") MainFrame.filterUnavail:SetActive(currentFilter == "unavailable") MainFrame.filterUsed:SetActive(currentFilter == "used") end local _isUpdating = false local function FullUpdate() if _isUpdating then return end _isUpdating = true local ok, err = pcall(function() UpdateFilters() UpdateList() UpdateDetail() end) _isUpdating = false if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI TrainerUI: FullUpdate error: " .. tostring(err) .. "|r") end end local function SelectService(index) selectedIndex = index local ok = pcall(SelectTrainerService, index) FullUpdate() end local function ToggleCategory(catName) if collapsedCats[catName] then collapsedCats[catName] = nil else collapsedCats[catName] = true end FullUpdate() end -------------------------------------------------------------------------------- -- Initialize -------------------------------------------------------------------------------- function TUI:Initialize() if MainFrame then return end MainFrame = CreateFrame("Frame", "SFramesTrainerFrame", UIParent) MainFrame:SetWidth(FRAME_W) MainFrame:SetHeight(FRAME_H) MainFrame:SetPoint("LEFT", UIParent, "LEFT", 64, 0) MainFrame:SetFrameStrata("HIGH") MainFrame:SetToplevel(true) MainFrame:EnableMouse(true) MainFrame:SetMovable(true) MainFrame:RegisterForDrag("LeftButton") MainFrame:SetScript("OnDragStart", function() this:StartMoving() end) MainFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) SetRoundBackdrop(MainFrame) CreateShadow(MainFrame) -- Header local header = CreateFrame("Frame", nil, MainFrame) header:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 0, 0) header:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", 0, 0) header:SetHeight(HEADER_H) header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" }) header:SetBackdropColor(T.headerBg[1], T.headerBg[2], T.headerBg[3], T.headerBg[4]) local titleIco = SFrames:CreateIcon(header, "spellbook", 16) titleIco:SetDrawLayer("OVERLAY") titleIco:SetPoint("LEFT", header, "LEFT", SIDE_PAD, 0) titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3]) local npcNameFS = header:CreateFontString(nil, "OVERLAY") npcNameFS:SetFont(GetFont(), 14, "OUTLINE") npcNameFS:SetPoint("LEFT", titleIco, "RIGHT", 5, 0) npcNameFS:SetPoint("RIGHT", header, "RIGHT", -30, 0) npcNameFS:SetJustifyH("LEFT") npcNameFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) MainFrame.npcNameFS = npcNameFS local closeBtn = CreateFrame("Button", nil, header) closeBtn:SetWidth(20); closeBtn:SetHeight(20) closeBtn:SetPoint("TOPRIGHT", header, "TOPRIGHT", -8, -6) local closeTex = closeBtn:CreateTexture(nil, "ARTWORK") closeTex:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\icon") closeTex:SetTexCoord(0.25, 0.375, 0, 0.125) closeTex:SetAllPoints() closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) closeBtn:SetScript("OnClick", function() MainFrame:Hide() end) closeBtn:SetScript("OnEnter", function() closeTex:SetVertexColor(1, 0.6, 0.7) end) closeBtn:SetScript("OnLeave", function() closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) end) local headerSep = MainFrame:CreateTexture(nil, "ARTWORK") headerSep:SetTexture("Interface\\Buttons\\WHITE8X8") headerSep:SetHeight(1) headerSep:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", 6, -HEADER_H) headerSep:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", -6, -HEADER_H) headerSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) -- Filter bar (above left list) local filterBar = CreateFrame("Frame", nil, MainFrame) filterBar:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD, -(HEADER_H + 4)) filterBar:SetWidth(LIST_W) filterBar:SetHeight(FILTER_H) local fAvail = CreateFilterBtn(filterBar, "可学习", 55) fAvail:SetPoint("LEFT", filterBar, "LEFT", 0, 0) fAvail:SetScript("OnClick", function() currentFilter = "available"; FullUpdate() end) MainFrame.filterAvail = fAvail local fUnavail = CreateFilterBtn(filterBar, "不可学", 55) fUnavail:SetPoint("LEFT", fAvail, "RIGHT", 2, 0) fUnavail:SetScript("OnClick", function() currentFilter = "unavailable"; FullUpdate() end) MainFrame.filterUnavail = fUnavail local fUsed = CreateFilterBtn(filterBar, "已学会", 55) fUsed:SetPoint("LEFT", fUnavail, "RIGHT", 2, 0) fUsed:SetScript("OnClick", function() currentFilter = "used"; FullUpdate() end) MainFrame.filterUsed = fUsed local fAll = CreateFilterBtn(filterBar, "全部", 45) fAll:SetPoint("LEFT", fUsed, "RIGHT", 2, 0) fAll:SetScript("OnClick", function() currentFilter = "all"; FullUpdate() end) MainFrame.filterAll = fAll -- Left side: Scrollable list area local listTop = HEADER_H + FILTER_H + 8 local listBg = CreateFrame("Frame", nil, MainFrame) listBg:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD, -listTop) listBg:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", SIDE_PAD, BOTTOM_H + 4) listBg:SetWidth(LIST_W) listBg: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 }, }) listBg:SetBackdropColor(0, 0, 0, 0.3) listBg:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.5) -- Scrollbar local scrollBar = CreateFrame("Slider", "SFramesTrainerScrollBar", listBg) scrollBar:SetWidth(16) scrollBar:SetPoint("TOPRIGHT", listBg, "TOPRIGHT", -4, -4) scrollBar:SetPoint("BOTTOMRIGHT", listBg, "BOTTOMRIGHT", -4, 4) scrollBar:SetOrientation("VERTICAL") scrollBar:SetThumbTexture("Interface\\Buttons\\UI-ScrollBar-Knob") scrollBar:SetMinMaxValues(0, 1) scrollBar:SetValue(0) scrollBar:SetValueStep(SCROLL_STEP) scrollBar:EnableMouseWheel(true) local scrollBg = scrollBar:CreateTexture(nil, "BACKGROUND") scrollBg:SetAllPoints() scrollBg:SetTexture(0, 0, 0, 0.3) local scrollUp = CreateFrame("Button", nil, scrollBar) scrollUp:SetWidth(16); scrollUp:SetHeight(16) scrollUp:SetPoint("BOTTOM", scrollBar, "TOP", 0, 0) scrollUp:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up") scrollUp:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Down") scrollUp:SetDisabledTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Disabled") scrollUp:SetHighlightTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Highlight") scrollUp:SetScript("OnClick", function() local val = scrollBar:GetValue() scrollBar:SetValue(val - SCROLL_STEP) end) local scrollDown = CreateFrame("Button", nil, scrollBar) scrollDown:SetWidth(16); scrollDown:SetHeight(16) scrollDown:SetPoint("TOP", scrollBar, "BOTTOM", 0, 0) scrollDown:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up") scrollDown:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Down") scrollDown:SetDisabledTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Disabled") scrollDown:SetHighlightTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Highlight") scrollDown:SetScript("OnClick", function() local val = scrollBar:GetValue() scrollBar:SetValue(val + SCROLL_STEP) end) local listScroll = CreateFrame("ScrollFrame", "SFramesTrainerListScroll", listBg) listScroll:SetPoint("TOPLEFT", listBg, "TOPLEFT", 4, -4) listScroll:SetPoint("BOTTOMRIGHT", scrollBar, "BOTTOMLEFT", -2, 0) local listContent = CreateFrame("Frame", "SFramesTrainerListContent", listScroll) listContent:SetWidth(LIST_W - 30) listContent:SetHeight(1) listScroll:SetScrollChild(listContent) -- Sync scrollbar with scroll frame scrollBar:SetScript("OnValueChanged", function() listScroll:SetVerticalScroll(this:GetValue()) end) scrollBar:SetScript("OnMouseWheel", function() local cur = this:GetValue() local min, max = this:GetMinMaxValues() if arg1 > 0 then this:SetValue(math.max(min, cur - SCROLL_STEP)) else this:SetValue(math.min(max, cur + SCROLL_STEP)) end end) -- Mouse wheel on list area local function OnListMouseWheel() local cur = scrollBar:GetValue() local min, max = scrollBar:GetMinMaxValues() if arg1 > 0 then scrollBar:SetValue(math.max(min, cur - SCROLL_STEP)) else scrollBar:SetValue(math.min(max, cur + SCROLL_STEP)) end end listBg:EnableMouseWheel(true) listBg:SetScript("OnMouseWheel", OnListMouseWheel) listScroll:EnableMouseWheel(true) listScroll:SetScript("OnMouseWheel", OnListMouseWheel) listScroll.content = listContent MainFrame.listScroll = listScroll MainFrame.listBg = listBg MainFrame.scrollBar = scrollBar for i = 1, MAX_ROWS do local row = CreateListRow(listContent, i) row:SetScript("OnClick", function() if this.isHeader and this.catName then ToggleCategory(this.catName) elseif this.serviceIndex then SelectService(this.serviceIndex) end end) rowButtons[i] = row end -- Right side: Detail panel local detailPanel = CreateFrame("Frame", nil, MainFrame) detailPanel:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD + LIST_W + SIDE_PAD, -listTop) detailPanel:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, BOTTOM_H + 4) detailPanel: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 }, }) detailPanel:SetBackdropColor(0, 0, 0, 0.3) detailPanel:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.5) MainFrame.detail = detailPanel -- Icon in detail panel local dIconFrame = CreateFrame("Frame", nil, detailPanel) dIconFrame:SetWidth(48); dIconFrame:SetHeight(48) dIconFrame:SetPoint("TOPLEFT", detailPanel, "TOPLEFT", 10, -10) dIconFrame:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 14, insets = { left = 2, right = 2, top = 2, bottom = 2 }, }) dIconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) dIconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) dIconFrame:Hide() detailPanel.iconFrame = dIconFrame local dIcon = dIconFrame:CreateTexture(nil, "ARTWORK") dIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92) dIcon:SetPoint("TOPLEFT", dIconFrame, "TOPLEFT", 3, -3) dIcon:SetPoint("BOTTOMRIGHT", dIconFrame, "BOTTOMRIGHT", -3, 3) detailPanel.icon = dIcon -- Name next to icon local dNameFS = detailPanel:CreateFontString(nil, "OVERLAY") dNameFS:SetFont(GetFont(), 14, "OUTLINE") dNameFS:SetPoint("TOPLEFT", dIconFrame, "TOPRIGHT", 8, -4) dNameFS:SetPoint("RIGHT", detailPanel, "RIGHT", -10, 0) dNameFS:SetJustifyH("LEFT") dNameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) detailPanel.nameFS = dNameFS -- Subtext (rank) local dSubFS = detailPanel:CreateFontString(nil, "OVERLAY") dSubFS:SetFont(GetFont(), 11, "OUTLINE") dSubFS:SetPoint("TOPLEFT", dNameFS, "BOTTOMLEFT", 0, -2) dSubFS:SetJustifyH("LEFT") dSubFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) detailPanel.subFS = dSubFS -- Requirements local dReqFS = detailPanel:CreateFontString(nil, "OVERLAY") dReqFS:SetFont(GetFont(), 11, "OUTLINE") dReqFS:SetPoint("TOPLEFT", dIconFrame, "BOTTOMLEFT", 0, -10) dReqFS:SetPoint("RIGHT", detailPanel, "RIGHT", -10, 0) dReqFS:SetJustifyH("LEFT") detailPanel.reqFS = dReqFS -- Spell info local dInfoFS = detailPanel:CreateFontString(nil, "OVERLAY") dInfoFS:SetFont(GetFont(), 11) dInfoFS:SetPoint("TOPLEFT", dReqFS, "BOTTOMLEFT", 0, -6) dInfoFS:SetPoint("RIGHT", detailPanel, "RIGHT", -10, 0) dInfoFS:SetJustifyH("LEFT") dInfoFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) detailPanel.infoFS = dInfoFS -- Divider line local descDivider = detailPanel:CreateTexture(nil, "ARTWORK") descDivider:SetTexture("Interface\\Buttons\\WHITE8X8") descDivider:SetHeight(1) descDivider:SetPoint("TOPLEFT", dInfoFS, "BOTTOMLEFT", 0, -8) descDivider:SetPoint("RIGHT", detailPanel, "RIGHT", -10, 0) descDivider:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.5) detailPanel.descDivider = descDivider -- Description scroll area local descScroll = CreateFrame("ScrollFrame", nil, detailPanel) descScroll:SetPoint("TOPLEFT", descDivider, "BOTTOMLEFT", 0, -6) descScroll:SetPoint("BOTTOMRIGHT", detailPanel, "BOTTOMRIGHT", -10, 8) descScroll:EnableMouseWheel(true) descScroll:SetScript("OnMouseWheel", function() local cur = this:GetVerticalScroll() local maxVal = this:GetVerticalScrollRange() if arg1 > 0 then this:SetVerticalScroll(math.max(0, cur - 14)) else this:SetVerticalScroll(math.min(maxVal, cur + 14)) end end) detailPanel.descScroll = descScroll local descContent = CreateFrame("Frame", nil, descScroll) descContent:SetWidth(DETAIL_W - 20) descContent:SetHeight(1) descScroll:SetScrollChild(descContent) local dDescFS = descContent:CreateFontString(nil, "OVERLAY") dDescFS:SetFont(GetFont(), 11) dDescFS:SetPoint("TOPLEFT", descContent, "TOPLEFT", 0, 0) dDescFS:SetWidth(DETAIL_W - 20) dDescFS:SetJustifyH("LEFT") dDescFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) detailPanel.descFS = dDescFS -- Bottom bar local bottomSep = MainFrame:CreateTexture(nil, "ARTWORK") bottomSep:SetTexture("Interface\\Buttons\\WHITE8X8") bottomSep:SetHeight(1) bottomSep:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", 6, BOTTOM_H) bottomSep:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -6, BOTTOM_H) bottomSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) local costLabel = MainFrame:CreateFontString(nil, "OVERLAY") costLabel:SetFont(GetFont(), 11, "OUTLINE") costLabel:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", SIDE_PAD, 32) costLabel:SetJustifyH("LEFT") costLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) costLabel:SetText("花费:") MainFrame.costLabel = costLabel local costMoney = CreateFrame("Frame", "SFramesTrainerCostMoney", MainFrame, "SmallMoneyFrameTemplate") costMoney:SetPoint("LEFT", costLabel, "RIGHT", 4, 0) costMoney:SetWidth(120) costMoney:SetHeight(14) costMoney:SetFrameLevel(MainFrame:GetFrameLevel() + 5) costMoney:UnregisterAllEvents() costMoney:SetScript("OnEvent", nil) costMoney:SetScript("OnShow", nil) costMoney.moneyType = nil costMoney.small = 1 MainFrame.costMoney = costMoney local availLabel = MainFrame:CreateFontString(nil, "OVERLAY") availLabel:SetFont(GetFont(), 11, "OUTLINE") availLabel:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", SIDE_PAD, 16) availLabel:SetJustifyH("LEFT") availLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) availLabel:SetText("可用:") MainFrame.availLabel = availLabel local availMoney = CreateFrame("Frame", "SFramesTrainerAvailMoney", MainFrame, "SmallMoneyFrameTemplate") availMoney:SetPoint("LEFT", availLabel, "RIGHT", 4, 0) availMoney:SetWidth(120) availMoney:SetHeight(14) availMoney:SetFrameLevel(MainFrame:GetFrameLevel() + 5) availMoney:UnregisterAllEvents() availMoney:SetScript("OnEvent", nil) availMoney:SetScript("OnShow", nil) availMoney.moneyType = nil availMoney.small = 1 MainFrame.availMoney = availMoney local trainBtn = CreateActionBtn(MainFrame, "训练", 80) trainBtn:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -(SIDE_PAD + 90), 8) trainBtn:SetScript("OnClick", function() if this.disabled then return end if IsControlKeyDown() and BuyTrainerService then local numServices = GetNumTrainerServices and GetNumTrainerServices() or 0 local gold = GetMoney() for i = 1, numServices do local name = GetTrainerServiceInfo(i) local cat = GetVerifiedCategory(i) if name and cat == "available" and not IsServiceHeader(i) then local ok, cost = pcall(GetTrainerServiceCost, i) if ok and cost and gold >= cost then pcall(BuyTrainerService, i) gold = gold - cost end end end return end if selectedIndex and BuyTrainerService then BuyTrainerService(selectedIndex) end end) trainBtn:SetScript("OnEnter", function() if not this.disabled then this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4]) this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end GameTooltip:SetOwner(this, "ANCHOR_TOP") GameTooltip:AddLine("Ctrl+点击: 学习所有可学技能", 0.8, 0.8, 0.8) GameTooltip:Show() end) trainBtn:SetScript("OnLeave", function() if not this.disabled then this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end GameTooltip:Hide() end) MainFrame.trainBtn = trainBtn local exitBtn = CreateActionBtn(MainFrame, "退出", 80) exitBtn:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, 8) exitBtn:SetScript("OnClick", function() MainFrame:Hide() end) -- Events local function CleanupBlizzardTrainer() if not ClassTrainerFrame then return end ClassTrainerFrame:SetScript("OnHide", function() end) ClassTrainerFrame:UnregisterAllEvents() if HideUIPanel then pcall(HideUIPanel, ClassTrainerFrame) end if ClassTrainerFrame:IsVisible() then ClassTrainerFrame:Hide() end ClassTrainerFrame:SetAlpha(0) ClassTrainerFrame:EnableMouse(false) end MainFrame:SetScript("OnHide", function() if CloseTrainer then pcall(CloseTrainer) end if CloseGossip then pcall(CloseGossip) end CleanupBlizzardTrainer() end) MainFrame:RegisterEvent("TRAINER_SHOW") MainFrame:RegisterEvent("TRAINER_UPDATE") MainFrame:RegisterEvent("TRAINER_CLOSED") MainFrame:SetScript("OnEvent", function() if event == "TRAINER_SHOW" then if ClassTrainerFrame then ClassTrainerFrame:SetScript("OnHide", function() end) ClassTrainerFrame:UnregisterAllEvents() ClassTrainerFrame:SetAlpha(0) ClassTrainerFrame:EnableMouse(false) end selectedIndex = nil currentFilter = "available" collapsedCats = {} qualityCache = {} -- Cache tradeskill trainer status once (avoid repeated API calls) isTradeskillTrainerCached = IsTradeskillTrainer and IsTradeskillTrainer() or false local npcName = UnitName("npc") or "训练师" if isTradeskillTrainerCached then npcName = npcName .. " - 专业训练" end MainFrame.npcNameFS:SetText(npcName) _isUpdating = true if SetTrainerServiceTypeFilter then pcall(SetTrainerServiceTypeFilter, "available", 1) pcall(SetTrainerServiceTypeFilter, "unavailable", 1) pcall(SetTrainerServiceTypeFilter, "used", 1) end _isUpdating = false MainFrame:Show() BuildDisplayList() for _, entry in ipairs(displayList) do if entry.type == "service" then selectedIndex = entry.data.index pcall(SelectTrainerService, entry.data.index) break end end FullUpdate() MainFrame._hideBlizzTimer = 0 MainFrame:SetScript("OnUpdate", function() if not this._hideBlizzTimer then return end this._hideBlizzTimer = this._hideBlizzTimer + arg1 if this._hideBlizzTimer > 0.05 then this._hideBlizzTimer = nil this:SetScript("OnUpdate", nil) CleanupBlizzardTrainer() end end) elseif event == "TRAINER_UPDATE" then if MainFrame:IsVisible() then FullUpdate() end elseif event == "TRAINER_CLOSED" then MainFrame._hideBlizzTimer = nil MainFrame:SetScript("OnUpdate", nil) CleanupBlizzardTrainer() MainFrame:Hide() end end) MainFrame:Hide() tinsert(UISpecialFrames, "SFramesTrainerFrame") end -------------------------------------------------------------------------------- -- Bootstrap -------------------------------------------------------------------------------- local bootstrap = CreateFrame("Frame") bootstrap:RegisterEvent("PLAYER_LOGIN") bootstrap:RegisterEvent("ADDON_LOADED") bootstrap:SetScript("OnEvent", function() if event == "PLAYER_LOGIN" then if SFramesDB.enableTrainer == nil then SFramesDB.enableTrainer = true end if SFramesDB.enableTrainer ~= false then TUI:Initialize() end elseif event == "ADDON_LOADED" and arg1 == "Blizzard_TrainerUI" then if MainFrame and ClassTrainerFrame then ClassTrainerFrame:SetScript("OnHide", function() end) ClassTrainerFrame:SetAlpha(0) ClassTrainerFrame:EnableMouse(false) end end end)