-------------------------------------------------------------------------------- -- Nanami-UI: TradeSkill / Craft UI (TradeSkillUI.lua) -- Replaces TradeSkillFrame and CraftFrame with Nanami-UI styled interface -- NOTE: Lua 5.0 upvalue limit = 32 per closure; all state packed into tables. -------------------------------------------------------------------------------- SFrames = SFrames or {} SFrames.TradeSkillUI = {} local TSUI = SFrames.TradeSkillUI SFramesDB = SFramesDB or {} -------------------------------------------------------------------------------- -- Theme (Pink Cat-Paw) -------------------------------------------------------------------------------- local T = SFrames.Theme:Extend({ reagentOk = { 0.60, 0.90, 0.60 }, reagentLack = { 0.90, 0.30, 0.30 }, DIFFICULTY = { optimal = { 1.00, 0.50, 0.25 }, medium = { 1.00, 1.00, 0.00 }, easy = { 0.25, 0.75, 0.25 }, trivial = { 0.50, 0.50, 0.50 }, difficult = { 1.00, 0.50, 0.25 }, }, QUALITY = { [0] = { 0.62, 0.62, 0.62 }, [1] = { 1.00, 1.00, 1.00 }, [2] = { 0.12, 1.00, 0.00 }, [3] = { 0.00, 0.44, 0.87 }, [4] = { 0.64, 0.21, 0.93 }, [5] = { 1.00, 0.50, 0.00 }, }, }) T.DIFFICULTY.header = { T.catHeader[1], T.catHeader[2], T.catHeader[3] } -------------------------------------------------------------------------------- -- Layout (packed into table to save upvalues) -------------------------------------------------------------------------------- local L = { FRAME_W = 700, FRAME_H = 500, HEADER_H = 46, SIDE_PAD = 10, FILTER_H = 50, LIST_ROW_H = 26, CAT_ROW_H = 20, BOTTOM_H = 40, SCROLL_STEP = 40, SCROLLBAR_W = 12, MAX_ROWS = 80, MAX_REAGENTS = 8, LEFT_W = 304, } L.RIGHT_W = L.FRAME_W - L.LEFT_W L.CONTENT_W = L.RIGHT_W - L.SIDE_PAD * 2 L.LIST_ROW_W = L.LEFT_W - L.SIDE_PAD * 2 - L.SCROLLBAR_W - 4 -------------------------------------------------------------------------------- -- State (packed into table to save upvalues) -------------------------------------------------------------------------------- local S = { MainFrame = nil, selectedIndex = nil, currentFilter = "all", searchText = "", displayList = {}, rowButtons = {}, collapsedCats = {}, craftAmount = 1, currentMode = "tradeskill", reagentSlots = {}, profTabs = {}, profList = {}, switchStartTime = nil, } -- Professions that can open a crafting window (spell name -> true) local PROF_SPELLS = { ["附魔"]=true,["Enchanting"]=true, ["裁缝"]=true,["裁缝术"]=true,["Tailoring"]=true, ["皮革加工"]=true,["皮革制作"]=true,["制皮"]=true,["Leatherworking"]=true, ["锻造"]=true,["锻造术"]=true,["Blacksmithing"]=true, ["工程学"]=true,["Engineering"]=true, ["炼金术"]=true,["炼金"]=true,["Alchemy"]=true, ["珠宝加工"]=true,["Jewelcrafting"]=true, ["烹饪"]=true,["Cooking"]=true, ["急救"]=true,["First Aid"]=true, ["熔炼"]=true,["Smelting"]=true, ["毒药"]=true,["Poisons"]=true, ["生存"]=true,["Survival"]=true, } -------------------------------------------------------------------------------- -- Mode Abstraction Layer (packed into table) -------------------------------------------------------------------------------- local API = {} function API.GetNumRecipes() if S.currentMode == "craft" then return GetNumCrafts and GetNumCrafts() or 0 end return GetNumTradeSkills and GetNumTradeSkills() or 0 end function API.GetRecipeInfo(i) if S.currentMode == "craft" then if not GetCraftInfo then return nil end local name, rank, skillType = GetCraftInfo(i) return name, skillType, 0, false end if not GetTradeSkillInfo then return nil end local name, skillType, numAvail, isExpanded = GetTradeSkillInfo(i) return name, skillType, numAvail or 0, isExpanded end function API.GetRecipeIcon(i) if S.currentMode == "craft" then return GetCraftIcon and GetCraftIcon(i) end return GetTradeSkillIcon and GetTradeSkillIcon(i) end function API.GetSkillLineName() if S.currentMode == "craft" then if GetCraftDisplaySkillLine then local name, cur, mx = GetCraftDisplaySkillLine() if name and name ~= "" then return name, cur, mx end end local n = GetCraftName and GetCraftName() or "" return n, 0, 0 end if GetTradeSkillLine then local name, cur, mx = GetTradeSkillLine() return name or "", cur or 0, mx or 0 end return "", 0, 0 end function API.GetNumReagents(i) if S.currentMode == "craft" then return GetCraftNumReagents and GetCraftNumReagents(i) or 0 end return GetTradeSkillNumReagents and GetTradeSkillNumReagents(i) or 0 end function API.GetReagentInfo(i, j) if S.currentMode == "craft" then if not GetCraftReagentInfo then return nil, nil, 0, 0 end local name, tex, count, playerCount = GetCraftReagentInfo(i, j) return name, tex, count or 0, playerCount or 0 end if not GetTradeSkillReagentInfo then return nil, nil, 0, 0 end local name, tex, count, playerCount = GetTradeSkillReagentInfo(i, j) return name, tex, count or 0, playerCount or 0 end function API.DoRecipe(i, num) if S.currentMode == "craft" then if DoCraft then DoCraft(i) end else if DoTradeSkill then DoTradeSkill(i, num or 1) end end end function API.CloseRecipe() if S.currentMode == "craft" then if CloseCraft then pcall(CloseCraft) end else if CloseTradeSkill then pcall(CloseTradeSkill) end end end function API.SelectRecipe(i) if S.currentMode == "craft" then if SelectCraft then pcall(SelectCraft, i) end else if SelectTradeSkill then pcall(SelectTradeSkill, i) end end end function API.GetCooldown(i) if S.currentMode == "craft" then return nil end if not GetTradeSkillCooldown then return nil end local ok, cd = pcall(GetTradeSkillCooldown, i) if ok then return cd end return nil end function API.GetDescription(i) if S.currentMode == "craft" then if GetCraftDescription then local ok, desc = pcall(GetCraftDescription, i) if ok and desc then return desc end end end return "" end function API.GetNumMade(i) if S.currentMode == "craft" then return 1, 1 end if not GetTradeSkillNumMade then return 1, 1 end local ok, lo, hi = pcall(GetTradeSkillNumMade, i) if ok then return lo or 1, hi or 1 end return 1, 1 end function API.GetItemLink(i) if S.currentMode == "craft" then if GetCraftItemLink then local ok, l = pcall(GetCraftItemLink, i); if ok then return l end end else if GetTradeSkillItemLink then local ok, l = pcall(GetTradeSkillItemLink, i); if ok then return l end end end return nil end function API.GetReagentItemLink(i, j) if S.currentMode == "craft" then if GetCraftReagentItemLink then local ok, l = pcall(GetCraftReagentItemLink, i, j); if ok then return l end end else if GetTradeSkillReagentItemLink then local ok, l = pcall(GetTradeSkillReagentItemLink, i, j); if ok then return l end end end return nil end function API.GetItemQuality(i) local link = API.GetItemLink(i) if not link then return nil end local _, _, colorHex = string.find(link, "|c(%x+)|") if not colorHex then return nil end local qualityMap = { ["ff9d9d9d"] = 0, ["ffffffff"] = 1, ["ff1eff00"] = 2, ["ff0070dd"] = 3, ["ffa335ee"] = 4, ["ffff8000"] = 5, } return qualityMap[colorHex] end function API.GetTools(i) if S.currentMode == "craft" then if GetCraftSpellFocus then local ok, focus = pcall(GetCraftSpellFocus, i) if ok and focus then return focus end end return nil end if not GetTradeSkillTools then return nil end local ok, r1, r2, r3, r4, r5, r6, r7, r8, r9 = pcall(GetTradeSkillTools, i) if not ok or not r1 then return nil end -- GetTradeSkillTools may return two formats: -- Standard: desc, tool1, has1, tool2, has2, ... -- Alt (some servers): tool1, has1, tool2, has2, ... -- Detect: if r2 is a string → standard (r1=desc); otherwise alt (r1=tool1) local toolPairs = {} if type(r2) == "string" then if type(r2) == "string" then table.insert(toolPairs, {r2, r3}) end if type(r4) == "string" then table.insert(toolPairs, {r4, r5}) end if type(r6) == "string" then table.insert(toolPairs, {r6, r7}) end if type(r8) == "string" then table.insert(toolPairs, {r8, r9}) end else if type(r1) == "string" then table.insert(toolPairs, {r1, r2}) end if type(r3) == "string" then table.insert(toolPairs, {r3, r4}) end if type(r5) == "string" then table.insert(toolPairs, {r5, r6}) end if type(r7) == "string" then table.insert(toolPairs, {r7, r8}) end end if table.getn(toolPairs) == 0 then if type(r1) == "string" then return r1 end return nil end local parts = {} for idx = 1, table.getn(toolPairs) do local name = toolPairs[idx][1] local has = toolPairs[idx][2] if has then table.insert(parts, "|cff60e060" .. name .. "|r") else table.insert(parts, "|cffff3030" .. name .. "|r") end end return table.concat(parts, ", ") end function API.GetCraftedItemID(i) local link = API.GetItemLink(i) if not link then return nil end local _, _, id = string.find(link, "item:(%d+)") if id then return tonumber(id) end return nil end function API.GetSkillThresholds(i) if not NanamiTradeSkillDB then return nil end local itemID = API.GetCraftedItemID(i) if itemID and NanamiTradeSkillDB[itemID] then return NanamiTradeSkillDB[itemID] end local name = API.GetRecipeInfo(i) if name and NanamiTradeSkillDB[name] then return NanamiTradeSkillDB[name] end return nil end function API.FormatThresholds(thresholds) if not thresholds then return nil end return "|cffff7f3f" .. thresholds[1] .. "|r " .. "|cffffff00" .. thresholds[2] .. "|r " .. "|cff3fbf3f" .. thresholds[3] .. "|r " .. "|cff7f7f7f" .. thresholds[4] .. "|r" end local SOURCE_LABELS = { T = { "|cffffffff训练师|r", "|cffffffff[T]|r" }, A = { "|cffffffff自动学习|r", "|cffffffff[A]|r" }, D = { "|cffa335ee掉落|r", "|cffa335ee[D]|r" }, V = { "|cff1eff00商人|r", "|cff1eff00[V]|r" }, Q = { "|cff0070dd任务|r", "|cff0070dd[Q]|r" }, F = { "|cff40bfff钓鱼|r", "|cff40bfff[F]|r" }, O = { "|cffffff00世界物体|r", "|cffffff00[O]|r" }, E = { "|cffff8000工程制作|r", "|cffff8000[E]|r" }, G = { "|cff888888赠予|r", "|cff888888[G]|r" }, ["?"] = { "|cffffff00未知|r", "|cffffff00[?]|r" }, } function API.GetRecipeSource(i) if not NanamiTradeSkillSources then return nil end local itemID = API.GetCraftedItemID(i) if itemID and NanamiTradeSkillSources[itemID] then return NanamiTradeSkillSources[itemID] end local name = API.GetRecipeInfo(i) if name and NanamiTradeSkillSources[name] then return NanamiTradeSkillSources[name] end return nil end function API.ParseSource(src) if not src then return nil, nil end local stype = string.sub(src, 1, 1) local detail = nil if string.len(src) > 2 then detail = string.sub(src, 3) end return stype, detail end local PROF_CN_TO_EN = { ["炼金术"] = "Alchemy", ["附魔"] = "Enchanting", ["锻造"] = "Blacksmithing", ["制皮"] = "Leatherworking", ["裁缝"] = "Tailoring", ["工程学"] = "Engineering", ["烹饪"] = "Cooking", ["急救"] = "First Aid", ["采矿"] = "Mining", ["熔炼"] = "Smelting", ["草药学"] = "Herbalism", ["珠宝加工"] = "Jewelcrafting", ["毒药"] = "Poisons", ["生存"] = "Survival", } function API.GetUnlearnedRecipes() if not NanamiTradeSkillByProf then return nil end local skillName = API.GetSkillLineName() if not skillName or skillName == "" then return nil end local profKey = PROF_CN_TO_EN[skillName] or skillName local allRecipes = NanamiTradeSkillByProf[profKey] if not allRecipes then return nil end local knownItems = {} local numRecipes = API.GetNumRecipes() for i = 1, numRecipes do local name, skillType = API.GetRecipeInfo(i) if name and skillType ~= "header" then local itemID = API.GetCraftedItemID(i) if itemID then knownItems[itemID] = true end end end local unlearned = {} for _, entry in ipairs(allRecipes) do local craftItemID = entry[1] local cnName = entry[2] local src = entry[3] if not knownItems[craftItemID] then table.insert(unlearned, { itemID = craftItemID, name = cnName, source = src, }) end end return unlearned end -------------------------------------------------------------------------------- -- Helpers (module methods to avoid extra upvalues) -------------------------------------------------------------------------------- function TSUI.GetFont() if SFrames and SFrames.GetFont then return SFrames:GetFont() end return "Fonts\\ARIALN.TTF" end function TSUI.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 function TSUI.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 function TSUI.FormatCooldown(seconds) if not seconds or seconds <= 0 then return nil end if seconds >= 86400 then return string.format("%.1f 天", seconds / 86400) elseif seconds >= 3600 then return string.format("%.1f 小时", seconds / 3600) elseif seconds >= 60 then return string.format("%d 分钟", math.floor(seconds / 60)) else return string.format("%d 秒", math.floor(seconds)) end end function TSUI.FormatRecipeForChat(recipeIndex) local name = API.GetRecipeInfo(recipeIndex) if not name then return nil end local itemLink = API.GetItemLink(recipeIndex) local header = itemLink or name local numReagents = API.GetNumReagents(recipeIndex) if numReagents == 0 then return header end local parts = {} for j = 1, numReagents do local rName, _, rCount = API.GetReagentInfo(recipeIndex, j) if rName then local rLink = API.GetReagentItemLink(recipeIndex, j) local display = rLink or rName if rCount and rCount > 1 then table.insert(parts, display .. "x" .. rCount) else table.insert(parts, display) end end end return header .. " = " .. table.concat(parts, ", ") end function TSUI.LinkRecipeToChat(recipeIndex) local msg = TSUI.FormatRecipeForChat(recipeIndex) if not msg then return end if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then ChatFrameEditBox:Insert(msg) else if ChatFrame_OpenChat then ChatFrame_OpenChat(msg) elseif ChatFrameEditBox then ChatFrameEditBox:Show() ChatFrameEditBox:SetText(msg) ChatFrameEditBox:SetFocus() end end end local SOURCE_NAMES = { T = "训练师", A = "自动学习", D = "掉落", V = "商人", Q = "任务", F = "钓鱼", O = "世界物体", ["?"] = "未知", } function TSUI.LinkUnlearnedToChat(data) if not data or not data.name then return end local msg = "[未学习] " .. data.name local reagents = NanamiTradeSkillReagents and data.itemID and NanamiTradeSkillReagents[data.itemID] if reagents and table.getn(reagents) > 0 then local rParts = {} for _, entry in ipairs(reagents) do local rID, rCount = entry[1], entry[2] local rName = GetItemInfo and GetItemInfo(rID) or ("#" .. rID) if rCount > 1 then table.insert(rParts, rName .. "x" .. rCount) else table.insert(rParts, rName) end end msg = msg .. " = " .. table.concat(rParts, ", ") end if data.source then local stype = API.ParseSource(data.source) local sname = SOURCE_NAMES[stype] or stype msg = msg .. " (来源: " .. sname .. ")" end if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then ChatFrameEditBox:Insert(msg) else if ChatFrame_OpenChat then ChatFrame_OpenChat(msg) elseif ChatFrameEditBox then ChatFrameEditBox:Show() ChatFrameEditBox:SetText(msg) ChatFrameEditBox:SetFocus() end end end function TSUI.MatchSearch(name) if not S.searchText or S.searchText == "" then return true end if not name then return false end local lowerSearch = string.lower(S.searchText) local lowerName = string.lower(name) return string.find(lowerName, lowerSearch, 1, true) ~= nil end -------------------------------------------------------------------------------- -- Widget Factories (module methods) -------------------------------------------------------------------------------- function TSUI.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(TSUI.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 function TSUI.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(TSUI.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 function TSUI.CreateSpinner(parent, x, y) local frame = CreateFrame("Frame", nil, parent) frame:SetWidth(80); frame:SetHeight(24) frame:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", x, y) local bg = CreateFrame("Frame", nil, frame) bg:SetAllPoints() bg:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } }) bg:SetBackdropColor(T.searchBg[1], T.searchBg[2], T.searchBg[3], T.searchBg[4]) bg:SetBackdropBorderColor(T.searchBorder[1], T.searchBorder[2], T.searchBorder[3], T.searchBorder[4]) local font = TSUI.GetFont() local minus = CreateFrame("Button", nil, frame) minus:SetWidth(20); minus:SetHeight(22); minus:SetPoint("LEFT", frame, "LEFT", 1, 0) local minusFS = minus:CreateFontString(nil, "OVERLAY") minusFS:SetFont(font, 14, "OUTLINE"); minusFS:SetPoint("CENTER", 0, 0) minusFS:SetText("-"); minusFS:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) minus:SetScript("OnEnter", function() minusFS:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end) minus:SetScript("OnLeave", function() minusFS:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end) local plus = CreateFrame("Button", nil, frame) plus:SetWidth(20); plus:SetHeight(22); plus:SetPoint("RIGHT", frame, "RIGHT", -1, 0) local plusFS = plus:CreateFontString(nil, "OVERLAY") plusFS:SetFont(font, 14, "OUTLINE"); plusFS:SetPoint("CENTER", 0, 0) plusFS:SetText("+"); plusFS:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) plus:SetScript("OnEnter", function() plusFS:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) end) plus:SetScript("OnLeave", function() plusFS:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end) local editBox = CreateFrame("EditBox", nil, frame) editBox:SetWidth(36); editBox:SetHeight(20) editBox:SetPoint("CENTER", 0, 0) editBox:SetFont(font, 12, "OUTLINE") editBox:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) editBox:SetJustifyH("CENTER") editBox:SetAutoFocus(false) editBox:SetNumeric(true) editBox:SetMaxLetters(4) editBox:SetText("1") editBox:SetScript("OnEscapePressed", function() this:ClearFocus() end) editBox:SetScript("OnEnterPressed", function() this:ClearFocus() end) editBox:SetScript("OnEditFocusLost", function() local v = tonumber(this:GetText()) or 1 if v < 1 then v = 1 end frame.value = v this:SetText(tostring(v)) end) frame.editBox = editBox; frame.value = 1 function frame:SetValue(v) if v < 1 then v = 1 end self.value = v self.editBox:SetText(tostring(v)) end function frame:GetValue() local v = tonumber(self.editBox:GetText()) or self.value if v < 1 then v = 1 end return v end minus:SetScript("OnClick", function() local c = frame:GetValue(); frame:SetValue(IsShiftKeyDown() and (c - 10) or (c - 1)) end) plus:SetScript("OnClick", function() local c = frame:GetValue(); frame:SetValue(IsShiftKeyDown() and (c + 10) or (c + 1)) end) return frame end function TSUI.CreateListRow(parent, idx) local row = CreateFrame("Button", nil, parent) row:SetWidth(L.CONTENT_W); row:SetHeight(L.LIST_ROW_H) local iconFrame = CreateFrame("Frame", nil, row) iconFrame:SetWidth(L.LIST_ROW_H - 4); iconFrame:SetHeight(L.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]) 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.8) local glowSize = (L.LIST_ROW_H - 4) * 1.9 qualGlow:SetWidth(glowSize); qualGlow:SetHeight(glowSize) qualGlow:SetPoint("CENTER", iconFrame, "CENTER", 0, 0) qualGlow:Hide(); row.qualGlow = qualGlow local font = TSUI.GetFont() local nameFS = row:CreateFontString(nil, "OVERLAY") nameFS:SetFont(font, 11, "OUTLINE") nameFS:SetPoint("LEFT", iconFrame, "RIGHT", 6, 0) nameFS:SetPoint("RIGHT", row, "RIGHT", -105, 0) nameFS:SetJustifyH("LEFT"); nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) row.nameFS = nameFS local skillFS = row:CreateFontString(nil, "OVERLAY") skillFS:SetFont(font, 8, "OUTLINE"); skillFS:SetPoint("RIGHT", row, "RIGHT", -46, 0) skillFS:SetJustifyH("RIGHT") skillFS:Hide(); row.skillFS = skillFS local srcTagFS = row:CreateFontString(nil, "OVERLAY") srcTagFS:SetFont(font, 8, "OUTLINE"); srcTagFS:SetPoint("RIGHT", row, "RIGHT", -26, 0) srcTagFS:SetJustifyH("RIGHT") srcTagFS:Hide(); row.srcTagFS = srcTagFS local countFS = row:CreateFontString(nil, "OVERLAY") countFS:SetFont(font, 10, "OUTLINE"); countFS:SetPoint("RIGHT", row, "RIGHT", -4, 0) countFS:SetJustifyH("RIGHT"); countFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) countFS:Hide(); row.countFS = countFS local catFS = row:CreateFontString(nil, "OVERLAY") catFS:SetFont(font, 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 selBg = row:CreateTexture(nil, "ARTWORK") selBg:SetTexture("Interface\\Buttons\\WHITE8X8") selBg:SetAllPoints(row) selBg:SetVertexColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.40) selBg:Hide(); row.selBg = selBg local selGlow = row:CreateTexture(nil, "ARTWORK") selGlow:SetTexture("Interface\\Buttons\\WHITE8X8") selGlow:SetWidth(4); selGlow:SetHeight(L.LIST_ROW_H) selGlow:SetPoint("LEFT", row, "LEFT", 0, 0) selGlow:SetVertexColor(1, 0.65, 0.85, 1) selGlow:Hide(); row.selGlow = selGlow 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.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.8) 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.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.8) selBot:Hide(); row.selBot = selBot local hl = row:CreateTexture(nil, "HIGHLIGHT") hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") hl:SetBlendMode("ADD"); hl:SetAllPoints(row); hl:SetAlpha(0.3) row.highlight = hl; row.recipeIndex = nil; row.isHeader = false; row.headerIndex = nil row:SetScript("OnEnter", function() if this.recipeIndex and not this.isHeader then GameTooltip:SetOwner(this, "ANCHOR_RIGHT") local ok if S.currentMode == "craft" then local link = GetCraftItemLink and GetCraftItemLink(this.recipeIndex) if link then ok = pcall(GameTooltip.SetCraftItem, GameTooltip, this.recipeIndex) else ok = pcall(GameTooltip.SetCraftSpell, GameTooltip, this.recipeIndex) end else ok = pcall(GameTooltip.SetTradeSkillItem, GameTooltip, this.recipeIndex) end 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(L.CAT_ROW_H) self.iconFrame:Hide(); self.nameFS:Hide(); self.countFS:Hide(); self.skillFS:Hide() self.srcTagFS:Hide() self.diffDot:Hide(); self.qualGlow:Hide(); self.highlight:SetAlpha(0.15) self.selBg:Hide(); self.selGlow:Hide(); self.selTop:Hide(); self.selBot:Hide() self.catFS:SetText((collapsed and "+" or "-") .. " " .. (name or "")) self.catFS:Show(); self.catSep:Show() end local diffDot = row:CreateTexture(nil, "OVERLAY") diffDot:SetTexture("Interface\\Buttons\\WHITE8X8") diffDot:SetWidth(4); diffDot:SetHeight(L.LIST_ROW_H - 8) diffDot:SetPoint("LEFT", row, "LEFT", 0, 0) row.diffDot = diffDot iconFrame:ClearAllPoints() iconFrame:SetPoint("LEFT", row, "LEFT", 6, 0) nameFS:ClearAllPoints() nameFS:SetPoint("LEFT", iconFrame, "RIGHT", 6, 0) nameFS:SetPoint("RIGHT", row, "RIGHT", -105, 0) function row:SetAsRecipe(recipe) self.isHeader = false; self.recipeIndex = recipe.index; self.unlearnedData = nil self:SetHeight(L.LIST_ROW_H); self.iconFrame:Show(); self.nameFS:Show() self.catFS:Hide(); self.catSep:Hide(); self.highlight:SetAlpha(0.3) self.icon:SetTexture(API.GetRecipeIcon(recipe.index)) self.nameFS:SetText(recipe.name) local dc = T.DIFFICULTY[recipe.difficulty] or T.DIFFICULTY.trivial self.nameFS:SetTextColor(dc[1], dc[2], dc[3]) self.diffDot:SetVertexColor(dc[1], dc[2], dc[3], 1) self.diffDot:Show() local qc = recipe.quality and recipe.quality >= 2 and T.QUALITY[recipe.quality] if qc then self.qualGlow:SetVertexColor(qc[1], qc[2], qc[3]); self.qualGlow:Show() else self.qualGlow:Hide() end if recipe.difficulty == "trivial" then self.icon:SetVertexColor(0.6, 0.6, 0.6) else self.icon:SetVertexColor(1, 1, 1) end if recipe.numAvail and recipe.numAvail > 0 then self.countFS:SetText("[" .. recipe.numAvail .. "]"); self.countFS:Show() else self.countFS:Hide() end local thresholds = API.GetSkillThresholds(recipe.index) if thresholds then self.skillFS:SetText(API.FormatThresholds(thresholds)) self.skillFS:Show() else self.skillFS:Hide() end local src = API.GetRecipeSource(recipe.index) if src then local stype = API.ParseSource(src) local lbl = SOURCE_LABELS[stype] if lbl then self.srcTagFS:SetText(lbl[2]) self.srcTagFS:Show() else self.srcTagFS:Hide() end else self.srcTagFS:Hide() end end function row:SetAsUnlearned(data) self.isHeader = false; self.recipeIndex = nil self.unlearnedData = data self:SetHeight(L.LIST_ROW_H); self.iconFrame:Show(); self.nameFS:Show() self.catFS:Hide(); self.catSep:Hide(); self.highlight:SetAlpha(0.15) self.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark") self.icon:SetVertexColor(0.5, 0.5, 0.5) self.nameFS:SetText("|cff888888" .. (data.name or "?") .. "|r") self.nameFS:SetTextColor(0.55, 0.55, 0.55) self.diffDot:SetVertexColor(0.4, 0.4, 0.4, 1); self.diffDot:Show() self.qualGlow:Hide(); self.countFS:Hide() self.selBg:Hide(); self.selGlow:Hide(); self.selTop:Hide(); self.selBot:Hide() if data.thresholds then self.skillFS:SetText(API.FormatThresholds(data.thresholds)) self.skillFS:Show() else self.skillFS:Hide() end local stype = data.sourceType local lbl = SOURCE_LABELS[stype] if lbl then self.srcTagFS:SetText(lbl[2]) self.srcTagFS:Show() else self.srcTagFS:Hide() end end function row:Clear() self.recipeIndex = nil; self.headerIndex = nil; self.isHeader = false self.unlearnedData = nil self.countFS:Hide(); self.diffDot:Hide(); self.qualGlow:Hide(); self.skillFS:Hide() self.srcTagFS:Hide() self.selBg:Hide(); self.selGlow:Hide(); self.selTop:Hide(); self.selBot:Hide() self:Hide() end return row end function TSUI.CreateReagentSlot(parent, i) local slot = CreateFrame("Frame", nil, parent) slot:SetWidth(L.CONTENT_W / 2 - 4); slot:SetHeight(30) local rIconFrame = CreateFrame("Frame", nil, slot) rIconFrame:SetWidth(28); rIconFrame:SetHeight(28); rIconFrame:SetPoint("LEFT", slot, "LEFT", 0, 0) rIconFrame: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 } }) rIconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) rIconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) slot.iconFrame = rIconFrame local rIcon = rIconFrame:CreateTexture(nil, "ARTWORK") rIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92) rIcon:SetPoint("TOPLEFT", rIconFrame, "TOPLEFT", 3, -3) rIcon:SetPoint("BOTTOMRIGHT", rIconFrame, "BOTTOMRIGHT", -3, 3) slot.icon = rIcon local font = TSUI.GetFont() local rNameFS = slot:CreateFontString(nil, "OVERLAY") rNameFS:SetFont(font, 11, "OUTLINE"); rNameFS:SetPoint("LEFT", rIconFrame, "RIGHT", 6, 0) rNameFS:SetPoint("RIGHT", slot, "RIGHT", -46, 0); rNameFS:SetJustifyH("LEFT") rNameFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]); slot.nameFS = rNameFS local rCountFS = slot:CreateFontString(nil, "OVERLAY") rCountFS:SetFont(font, 11, "OUTLINE"); rCountFS:SetPoint("RIGHT", slot, "RIGHT", -2, 0) rCountFS:SetJustifyH("RIGHT"); slot.countFS = rCountFS slot.reagentIndex = nil; slot:EnableMouse(true) slot:SetScript("OnEnter", function() if S.selectedIndex and this.reagentIndex then GameTooltip:SetOwner(this, "ANCHOR_RIGHT") local ok if S.currentMode == "craft" then local link = GetCraftItemLink and GetCraftItemLink(S.selectedIndex) if link then ok = pcall(GameTooltip.SetCraftItem, GameTooltip, S.selectedIndex, this.reagentIndex) else ok = pcall(GameTooltip.SetCraftSpell, GameTooltip, S.selectedIndex) end else ok = pcall(GameTooltip.SetTradeSkillItem, GameTooltip, S.selectedIndex, this.reagentIndex) end if ok then GameTooltip:Show() else GameTooltip:Hide() end end end) slot:SetScript("OnLeave", function() GameTooltip:Hide() end) slot:SetScript("OnMouseUp", function() if IsShiftKeyDown() and S.selectedIndex and this.reagentIndex then local link = API.GetReagentItemLink(S.selectedIndex, this.reagentIndex) if link then if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then ChatFrameEditBox:Insert(link) else if ChatFrame_OpenChat then ChatFrame_OpenChat(link) elseif ChatFrameEditBox then ChatFrameEditBox:Show() ChatFrameEditBox:SetText(link) ChatFrameEditBox:SetFocus() end end end end end) slot:Hide() return slot end -------------------------------------------------------------------------------- -- Logic (module methods referencing S, L, T, API only) -------------------------------------------------------------------------------- function TSUI.BuildDisplayList() S.displayList = {} if S.currentFilter == "unlearned" then local unlearned = API.GetUnlearnedRecipes() if not unlearned or table.getn(unlearned) == 0 then return end local filtered = {} for _, entry in ipairs(unlearned) do if TSUI.MatchSearch(entry.name) then local stype = API.ParseSource(entry.source or "T") local th = NanamiTradeSkillDB and NanamiTradeSkillDB[entry.itemID] table.insert(filtered, { type = "unlearned", data = { name = entry.name, itemID = entry.itemID, source = entry.source, sourceType = stype, thresholds = th, }, }) end end if table.getn(filtered) == 0 then return end table.insert(S.displayList, { type = "header", name = "未学习的配方 (" .. table.getn(filtered) .. ")", collapsed = false, headerIndex = 0 }) for _, item in ipairs(filtered) do table.insert(S.displayList, item) end return end local numRecipes = API.GetNumRecipes() if numRecipes == 0 then return end local currentCat = nil local catRecipes = {} local catOrder = {} for i = 1, numRecipes do local name, skillType, numAvail, isExpanded = API.GetRecipeInfo(i) if name then if skillType == "header" then currentCat = name if not catRecipes[name] then catRecipes[name] = {} table.insert(catOrder, { name = name, index = i }) end else if not currentCat then currentCat = "配方" if not catRecipes[currentCat] then catRecipes[currentCat] = {} table.insert(catOrder, { name = currentCat, index = 0 }) end end local show = true if S.currentFilter == "available" then show = (skillType ~= "trivial" and numAvail > 0) elseif S.currentFilter == "optimal" then show = (skillType == "optimal" or skillType == "difficult" or skillType == "medium") elseif S.currentFilter == "hasmat" then show = (numAvail and numAvail > 0) end if show and not TSUI.MatchSearch(name) then show = false end if show then local quality = API.GetItemQuality(i) table.insert(catRecipes[currentCat], { index = i, name = name, difficulty = skillType or "trivial", numAvail = numAvail or 0, quality = quality, }) end end end end local hasCats = table.getn(catOrder) > 1 for _, catInfo in ipairs(catOrder) do local catName = catInfo.name local recipes = catRecipes[catName] if recipes and table.getn(recipes) > 0 then if hasCats then table.insert(S.displayList, { type = "header", name = catName, collapsed = S.collapsedCats[catName], headerIndex = catInfo.index }) end if not S.collapsedCats[catName] then for _, recipe in ipairs(recipes) do table.insert(S.displayList, { type = "recipe", data = recipe }) end end end end end function TSUI.UpdateList() if not S.MainFrame or not S.MainFrame:IsVisible() then return end TSUI.BuildDisplayList() local content = S.MainFrame.listScroll.content local count = table.getn(S.displayList) local y = 0 for i = 1, L.MAX_ROWS do local row = S.rowButtons[i] if i <= count then local entry = S.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.headerIndex = entry.headerIndex row:Show(); y = y + L.CAT_ROW_H elseif entry.type == "unlearned" then row:SetAsUnlearned(entry.data) row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) row.catName = nil; row:Show(); y = y + L.LIST_ROW_H if S.selectedUnlearned and S.selectedUnlearned.itemID == entry.data.itemID then row.selBg:Show(); row.selGlow:Show(); row.selTop:Show(); row.selBot:Show() end else row:SetAsRecipe(entry.data) row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) row.catName = nil; row:Show(); y = y + L.LIST_ROW_H if S.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.selBg:Show() row.selGlow:Show() row.selTop:Show() row.selBot:Show() local dc = T.DIFFICULTY[entry.data.difficulty] or T.DIFFICULTY.trivial row.nameFS:SetTextColor( math.min(1, dc[1] + 0.3), math.min(1, dc[2] + 0.3), math.min(1, dc[3] + 0.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.selBg:Hide() row.selGlow:Hide() row.selTop:Hide() row.selBot:Hide() end end else row:Clear() end end content:SetHeight(math.max(1, y)) end function TSUI.UpdateDetail() if not S.MainFrame then return end local detail = S.MainFrame.detail if not S.selectedIndex then if S.selectedUnlearned then TSUI.UpdateDetailUnlearned(S.selectedUnlearned) return end detail.iconFrame:Hide(); detail.qualGlow:Hide() detail.nameFS:SetText(""); detail.descFS:SetText("") detail.cooldownFS:SetText(""); detail.toolsFS:SetText(""); detail.madeFS:SetText("") if detail.diffDot then detail.diffDot:Hide() end if detail.diffFS then detail.diffFS:SetText("") end if detail.sourceFS then detail.sourceFS:SetText("") end for i = 1, L.MAX_REAGENTS do S.reagentSlots[i]:Hide() end S.MainFrame.createBtn:SetDisabled(true); S.MainFrame.createAllBtn:SetDisabled(true) return end local name, skillType, numAvail = API.GetRecipeInfo(S.selectedIndex) detail.icon:SetTexture(API.GetRecipeIcon(S.selectedIndex)); detail.iconFrame:Show() detail.nameFS:SetText(name or "") local dc = T.DIFFICULTY[skillType] or T.DIFFICULTY.trivial detail.nameFS:SetTextColor(dc[1], dc[2], dc[3]) local quality = API.GetItemQuality(S.selectedIndex) local qc = quality and quality >= 2 and T.QUALITY[quality] if qc then detail.qualGlow:SetVertexColor(qc[1], qc[2], qc[3]); detail.qualGlow:Show() else detail.qualGlow:Hide() end local cdText = TSUI.FormatCooldown(API.GetCooldown(S.selectedIndex)) detail.cooldownFS:SetText(cdText and ("|cffff8040冷却: " .. cdText .. "|r") or "") local tools = API.GetTools(S.selectedIndex) detail.toolsFS:SetText(tools and ("|cffffcc80需要: |r" .. tools) or "") detail.descFS:SetText(API.GetDescription(S.selectedIndex) or "") local loMade, hiMade = API.GetNumMade(S.selectedIndex) if loMade and hiMade and (loMade > 1 or hiMade > 1) then local mt = (loMade == hiMade) and ("产出: " .. loMade) or ("产出: " .. loMade .. "-" .. hiMade) detail.madeFS:SetText("|cffa0a0a0" .. mt .. "|r") else detail.madeFS:SetText("") end if detail.diffDot then local DIFF_NAMES = { optimal = { "橙色", "必涨点" }, difficult = { "橙色", "必涨点" }, medium = { "黄色", "大概率涨点" }, easy = { "绿色", "小概率涨点" }, trivial = { "灰色", "不涨点" }, } local info = DIFF_NAMES[skillType] local thresholds = API.GetSkillThresholds(S.selectedIndex) if info then detail.diffDot:SetVertexColor(dc[1], dc[2], dc[3], 1) detail.diffDot:Show() if thresholds then local thText = API.FormatThresholds(thresholds) detail.diffFS:SetText(info[2] .. " |cffbbbbbb技能:|r " .. thText) else detail.diffFS:SetText("当前难度: " .. info[1] .. " (" .. info[2] .. ")") end detail.diffFS:SetTextColor(dc[1], dc[2], dc[3]) else detail.diffDot:Hide() detail.diffFS:SetText("") end end if detail.sourceFS then local src = API.GetRecipeSource(S.selectedIndex) if src then local stype, sdetail = API.ParseSource(src) local lbl = SOURCE_LABELS[stype] if lbl then local text = "|cffbbbbbb来源:|r " .. lbl[1] if sdetail and sdetail ~= "" then text = text .. " - " .. sdetail end detail.sourceFS:SetText(text) else detail.sourceFS:SetText("") end else detail.sourceFS:SetText("") end end local numReagents = API.GetNumReagents(S.selectedIndex) local canCreate = true for i = 1, L.MAX_REAGENTS do if i <= numReagents then local rName, rTex, rCount, rPC = API.GetReagentInfo(S.selectedIndex, i) S.reagentSlots[i].icon:SetTexture(rTex) S.reagentSlots[i].nameFS:SetText(rName or "") local rQuality local rLink = API.GetReagentItemLink(S.selectedIndex, i) if rLink then local _, _, itemString = string.find(rLink, "item:(%d+)") if itemString and GetItemInfo then local _, _, scanRarity = GetItemInfo("item:" .. itemString) rQuality = scanRarity end end if rQuality and rQuality > 1 and GetItemQualityColor then local qr, qg, qb = GetItemQualityColor(rQuality) S.reagentSlots[i].nameFS:SetTextColor(qr, qg, qb) else S.reagentSlots[i].nameFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) end S.reagentSlots[i].countFS:SetText((rPC or 0) .. "/" .. (rCount or 0)) if (rPC or 0) >= (rCount or 0) then S.reagentSlots[i].countFS:SetTextColor(T.reagentOk[1], T.reagentOk[2], T.reagentOk[3]) if rQuality and rQuality > 1 and GetItemQualityColor then local qr, qg, qb = GetItemQualityColor(rQuality) S.reagentSlots[i].iconFrame:SetBackdropBorderColor(qr, qg, qb, 1) else S.reagentSlots[i].iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) end else S.reagentSlots[i].countFS:SetTextColor(T.reagentLack[1], T.reagentLack[2], T.reagentLack[3]) S.reagentSlots[i].iconFrame:SetBackdropBorderColor(T.reagentLack[1], T.reagentLack[2], T.reagentLack[3], 0.8) canCreate = false end S.reagentSlots[i].reagentIndex = i; S.reagentSlots[i]:Show() else S.reagentSlots[i]:Hide() end end S.MainFrame.createBtn:SetDisabled(not canCreate) S.MainFrame.createAllBtn:SetDisabled(not canCreate or not numAvail or numAvail <= 0) end function TSUI.UpdateFilters() if not S.MainFrame then return end S.MainFrame.filterAll:SetActive(S.currentFilter == "all") S.MainFrame.filterAvail:SetActive(S.currentFilter == "available") S.MainFrame.filterOptimal:SetActive(S.currentFilter == "optimal") S.MainFrame.filterHasMat:SetActive(S.currentFilter == "hasmat") S.MainFrame.filterUnlearned:SetActive(S.currentFilter == "unlearned") end function TSUI.FullUpdate() TSUI.UpdateFilters(); TSUI.UpdateList(); TSUI.UpdateDetail() if TSUI.UpdateScrollbar then TSUI.UpdateScrollbar() end end function TSUI.SelectRecipe(index) S.selectedIndex = index; S.selectedUnlearned = nil API.SelectRecipe(index) S.craftAmount = 1 if S.MainFrame and S.MainFrame.spinner then S.MainFrame.spinner:SetValue(1) end TSUI.FullUpdate() end function TSUI.SelectUnlearned(data) S.selectedIndex = nil; S.selectedUnlearned = data TSUI.UpdateDetailUnlearned(data) if S.MainFrame then S.MainFrame.createBtn:SetDisabled(true) S.MainFrame.createAllBtn:SetDisabled(true) end end function TSUI.UpdateDetailUnlearned(data) if not S.MainFrame then return end local detail = S.MainFrame.detail detail.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark") detail.iconFrame:Show(); detail.qualGlow:Hide() detail.nameFS:SetText("|cff888888" .. (data.name or "?") .. "|r") detail.nameFS:SetTextColor(0.55, 0.55, 0.55) detail.descFS:SetText("") detail.cooldownFS:SetText(""); detail.toolsFS:SetText("") detail.madeFS:SetText("|cffff5555未学习|r") if detail.diffDot then if data.thresholds then detail.diffDot:SetVertexColor(0.5, 0.5, 0.5, 1); detail.diffDot:Show() detail.diffFS:SetText("|cffbbbbbb技能:|r " .. API.FormatThresholds(data.thresholds)) detail.diffFS:SetTextColor(0.6, 0.6, 0.6) else detail.diffDot:Hide(); detail.diffFS:SetText("") end end if detail.sourceFS then local src = data.source if src then local stype, sdetail = API.ParseSource(src) local lbl = SOURCE_LABELS[stype] if lbl then local text = "|cffbbbbbb来源:|r " .. lbl[1] if sdetail and sdetail ~= "" then text = text .. " - " .. sdetail end detail.sourceFS:SetText(text) else detail.sourceFS:SetText("") end else detail.sourceFS:SetText("") end end local reagents = NanamiTradeSkillReagents and data.itemID and NanamiTradeSkillReagents[data.itemID] for i = 1, L.MAX_REAGENTS do if reagents and i <= table.getn(reagents) then local entry = reagents[i] local rItemID, rCount = entry[1], entry[2] local rName, rTex, rQuality if GetItemInfo then local n, _, q, _, _, _, _, _, t = GetItemInfo(rItemID) rName = n; rTex = t; rQuality = q end if rName then S.reagentSlots[i].icon:SetTexture(rTex) S.reagentSlots[i].nameFS:SetText(rName) if rQuality and rQuality > 1 and GetItemQualityColor then local qr, qg, qb = GetItemQualityColor(rQuality) S.reagentSlots[i].nameFS:SetTextColor(qr, qg, qb) S.reagentSlots[i].iconFrame:SetBackdropBorderColor(qr, qg, qb, 1) else S.reagentSlots[i].nameFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) S.reagentSlots[i].iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) end else S.reagentSlots[i].icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark") S.reagentSlots[i].nameFS:SetText("|cff888888物品#" .. rItemID .. "|r") S.reagentSlots[i].nameFS:SetTextColor(0.55, 0.55, 0.55) S.reagentSlots[i].iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) end S.reagentSlots[i].countFS:SetText("x" .. rCount) S.reagentSlots[i].countFS:SetTextColor(0.7, 0.7, 0.7) S.reagentSlots[i].reagentIndex = nil S.reagentSlots[i]:Show() else S.reagentSlots[i]:Hide() end end end function TSUI.ToggleCategory(catName) if S.collapsedCats[catName] then S.collapsedCats[catName] = nil else S.collapsedCats[catName] = true end TSUI.FullUpdate() end function TSUI.UpdateProgressBar() local skillName, cur, mx = API.GetSkillLineName() S.MainFrame.titleFS:SetText(skillName or "") if mx and mx > 0 and cur then local barWidth = S.MainFrame.progressFrame:GetWidth() - 4 local fill = math.min(1, cur / mx) S.MainFrame.progressBar:SetWidth(math.max(1, barWidth * fill)) S.MainFrame.progressText:SetText(cur .. " / " .. mx) else S.MainFrame.progressBar:SetWidth(1) S.MainFrame.progressText:SetText("") end end -------------------------------------------------------------------------------- -- Profession Tabs (right-side icon strip, spellbook style) -------------------------------------------------------------------------------- local PROF_ALIAS = { ["熔炼"]="采矿", ["采矿"]="熔炼", ["Smelting"]="Mining", ["Mining"]="Smelting", } function TSUI.ProfNamesMatch(a, b) if not a or not b or a == "" or b == "" then return false end if a == b then return true end if string.find(a, b, 1, true) or string.find(b, a, 1, true) then return true end local a2 = PROF_ALIAS[a] if a2 and (a2 == b or string.find(a2, b, 1, true) or string.find(b, a2, 1, true)) then return true end local b2 = PROF_ALIAS[b] if b2 and (b2 == a or string.find(b2, a, 1, true) or string.find(a, b2, 1, true)) then return true end return false end function TSUI.ScanProfessions() S.profList = {} if not GetSpellName then return end local seen = {} local idx = 1 local btype = BOOKTYPE_SPELL or "spell" while true do local name, rank = GetSpellName(idx, btype) if not name then break end if PROF_SPELLS[name] and not seen[name] then seen[name] = true local tex = GetSpellTexture(idx, btype) table.insert(S.profList, { name = name, icon = tex }) end idx = idx + 1 end end function TSUI.CreateProfTabs(parent) local TAB_SZ, TAB_GAP, TAB_TOP = 42, 4, 6 for i = 1, 10 do local tab = CreateFrame("Button", nil, parent) tab:SetWidth(TAB_SZ); tab:SetHeight(TAB_SZ) tab:SetPoint("TOPLEFT", parent, "TOPRIGHT", 2, -(TAB_TOP + (i - 1) * (TAB_SZ + TAB_GAP))) tab:SetFrameStrata("HIGH") tab: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 }, }) tab:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) tab:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) local icon = tab:CreateTexture(nil, "ARTWORK") icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) icon:SetPoint("TOPLEFT", tab, "TOPLEFT", 4, -4) icon:SetPoint("BOTTOMRIGHT", tab, "BOTTOMRIGHT", -4, 4) tab.icon = icon local glow = tab:CreateTexture(nil, "OVERLAY") glow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") glow:SetBlendMode("ADD"); glow:SetAlpha(0.7) local gs = TAB_SZ * 1.8 glow:SetWidth(gs); glow:SetHeight(gs) glow:SetPoint("CENTER", tab, "CENTER", 0, 0) glow:Hide(); tab.glow = glow local hl = tab:CreateTexture(nil, "HIGHLIGHT") hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square") hl:SetBlendMode("ADD"); hl:SetAlpha(0.3) hl:SetPoint("TOPLEFT", icon, "TOPLEFT", 0, 0) hl:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 0, 0) local checked = tab:CreateTexture(nil, "BORDER") checked:SetTexture("Interface\\Buttons\\CheckButtonHilight") checked:SetBlendMode("ADD"); checked:SetAlpha(0.35) checked:SetAllPoints(tab); checked:Hide(); tab.checked = checked tab.profName = nil; tab.active = false; tab:Hide() tab:SetScript("OnEnter", function() if this.profName then GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetText(this.profName, 1, 0.82, 0.60) GameTooltip:Show() end end) tab:SetScript("OnLeave", function() GameTooltip:Hide() end) tab:SetScript("OnClick", function() if this.active or not this.profName then return end S.switchStartTime = GetTime() CastSpellByName(this.profName) end) S.profTabs[i] = tab end end function TSUI.UpdateProfTabs() TSUI.ScanProfessions() local currentSkillName = API.GetSkillLineName() for i = 1, 10 do local tab = S.profTabs[i] if not tab then break end local prof = S.profList[i] if prof then tab.profName = prof.name tab.icon:SetTexture(prof.icon) local isActive = TSUI.ProfNamesMatch(prof.name, currentSkillName) tab.active = isActive if isActive then tab:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 1) tab.icon:SetVertexColor(1, 1, 1) tab.glow:SetVertexColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3]) tab.glow:Show(); tab.checked:Show() else tab:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) tab.icon:SetVertexColor(0.75, 0.75, 0.75) tab.glow:Hide(); tab.checked:Hide() end tab:Show() else tab.profName = nil; tab.active = false; tab:Hide() end end end function TSUI.IsTabSwitching() return S.switchStartTime and (GetTime() - S.switchStartTime) < 1.0 end -------------------------------------------------------------------------------- -- Hide Blizzard Frames (module methods) -------------------------------------------------------------------------------- function TSUI.HideBlizzardTradeSkill() if not TradeSkillFrame then return end TradeSkillFrame:SetScript("OnHide", function() end) if TradeSkillFrame:IsVisible() then if HideUIPanel then pcall(HideUIPanel, TradeSkillFrame) else TradeSkillFrame:Hide() end end TradeSkillFrame:SetAlpha(0); TradeSkillFrame:EnableMouse(false) TradeSkillFrame:ClearAllPoints() TradeSkillFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 2000, 2000) end function TSUI.HideBlizzardCraft() if not CraftFrame then return end CraftFrame:SetScript("OnHide", function() end) if CraftFrame:IsVisible() then if HideUIPanel then pcall(HideUIPanel, CraftFrame) else CraftFrame:Hide() end end CraftFrame:SetAlpha(0); CraftFrame:EnableMouse(false) CraftFrame:ClearAllPoints() CraftFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 2000, 2000) end function TSUI.CleanupBlizzardTradeSkill() if not TradeSkillFrame then return end TradeSkillFrame:SetScript("OnHide", function() end) if HideUIPanel then pcall(HideUIPanel, TradeSkillFrame) end if TradeSkillFrame:IsVisible() then TradeSkillFrame:Hide() end TradeSkillFrame:SetAlpha(0); TradeSkillFrame:EnableMouse(false) end function TSUI.CleanupBlizzardCraft() if not CraftFrame then return end CraftFrame:SetScript("OnHide", function() end) if HideUIPanel then pcall(HideUIPanel, CraftFrame) end if CraftFrame:IsVisible() then CraftFrame:Hide() end CraftFrame:SetAlpha(0); CraftFrame:EnableMouse(false) end -------------------------------------------------------------------------------- -- Initialize (left-right layout: list on left, detail on right) -------------------------------------------------------------------------------- function TSUI:Initialize() if S.MainFrame then return end local MF = CreateFrame("Frame", "SFramesTradeSkillFrame", UIParent) S.MainFrame = MF MF:SetWidth(L.FRAME_W); MF:SetHeight(L.FRAME_H) MF:SetPoint("LEFT", UIParent, "LEFT", 36, 0) MF:SetFrameStrata("HIGH"); MF:SetToplevel(true); MF:EnableMouse(true) MF:SetMovable(true); MF:RegisterForDrag("LeftButton") MF:SetScript("OnDragStart", function() this:StartMoving() end) MF:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) TSUI.SetRoundBackdrop(MF); TSUI.CreateShadow(MF) local font = TSUI.GetFont() -- ═══ Header (full width) ═════════════════════════════════════════ local header = CreateFrame("Frame", nil, MF) header:SetPoint("TOPLEFT", 0, 0); header:SetPoint("TOPRIGHT", 0, 0) header:SetHeight(L.HEADER_H) header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" }) header:SetBackdropColor(T.headerBg[1], T.headerBg[2], T.headerBg[3], T.headerBg[4]) local titleFS = header:CreateFontString(nil, "OVERLAY") local titleIco = SFrames:CreateIcon(header, "profession", 16) titleIco:SetDrawLayer("OVERLAY") titleIco:SetPoint("TOPLEFT", header, "TOPLEFT", L.SIDE_PAD, -8) titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3]) titleFS:SetFont(font, 14, "OUTLINE") titleFS:SetPoint("LEFT", titleIco, "RIGHT", 5, 0) titleFS:SetPoint("RIGHT", header, "RIGHT", -30, 0) titleFS:SetJustifyH("LEFT"); titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) MF.titleFS = titleFS local pf = CreateFrame("Frame", nil, header) pf:SetPoint("BOTTOMLEFT", header, "BOTTOMLEFT", L.SIDE_PAD, 5) pf:SetPoint("BOTTOMRIGHT", header, "BOTTOMRIGHT", -L.SIDE_PAD - 24, 5) pf:SetHeight(10) pf:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } }) pf:SetBackdropColor(T.progressBg[1], T.progressBg[2], T.progressBg[3], T.progressBg[4]) pf:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.5) local pb = pf:CreateTexture(nil, "ARTWORK") pb:SetTexture("Interface\\Buttons\\WHITE8X8") pb:SetPoint("TOPLEFT", pf, "TOPLEFT", 2, -2); pb:SetPoint("BOTTOMLEFT", pf, "BOTTOMLEFT", 2, 2) pb:SetWidth(1); pb:SetVertexColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], T.progressFill[4]) MF.progressBar = pb; MF.progressFrame = pf local pt = pf:CreateFontString(nil, "OVERLAY") pt:SetFont(font, 8, "OUTLINE"); pt:SetPoint("CENTER", pf, "CENTER", 0, 0) pt:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]); MF.progressText = pt 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() S.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) -- Separators local hsep = MF:CreateTexture(nil, "ARTWORK") hsep:SetTexture("Interface\\Buttons\\WHITE8X8"); hsep:SetHeight(1) hsep:SetPoint("TOPLEFT", MF, "TOPLEFT", 4, -L.HEADER_H) hsep:SetPoint("TOPRIGHT", MF, "TOPRIGHT", -4, -L.HEADER_H) hsep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) local vdiv = MF:CreateTexture(nil, "ARTWORK") vdiv:SetTexture("Interface\\Buttons\\WHITE8X8"); vdiv:SetWidth(1) vdiv:SetPoint("TOP", MF, "TOPLEFT", L.LEFT_W, -(L.HEADER_H + 2)) vdiv:SetPoint("BOTTOM", MF, "BOTTOMLEFT", L.LEFT_W, 4) vdiv:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) -- ═══ LEFT PANEL: Filters + Recipe List ═══════════════════════════ local fb = CreateFrame("Frame", nil, MF) fb:SetPoint("TOPLEFT", MF, "TOPLEFT", L.SIDE_PAD, -(L.HEADER_H + 4)) fb:SetWidth(L.LEFT_W - L.SIDE_PAD * 2); fb:SetHeight(22) local fAll = TSUI.CreateFilterBtn(fb, "全部", 38) fAll:SetPoint("LEFT", fb, "LEFT", 0, 0) fAll:SetScript("OnClick", function() S.currentFilter = "all"; TSUI.FullUpdate() end) MF.filterAll = fAll local fAvail = TSUI.CreateFilterBtn(fb, "可做", 38) fAvail:SetPoint("LEFT", fAll, "RIGHT", 3, 0) fAvail:SetScript("OnClick", function() S.currentFilter = "available"; TSUI.FullUpdate() end) MF.filterAvail = fAvail local fOpt = TSUI.CreateFilterBtn(fb, "橙/黄", 42) fOpt:SetPoint("LEFT", fAvail, "RIGHT", 3, 0) fOpt:SetScript("OnClick", function() S.currentFilter = "optimal"; TSUI.FullUpdate() end) MF.filterOptimal = fOpt local fMat = TSUI.CreateFilterBtn(fb, "有材料", 46) fMat:SetPoint("LEFT", fOpt, "RIGHT", 3, 0) fMat:SetScript("OnClick", function() S.currentFilter = "hasmat"; TSUI.FullUpdate() end) MF.filterHasMat = fMat local fUnlearned = TSUI.CreateFilterBtn(fb, "未学", 38) fUnlearned:SetPoint("LEFT", fMat, "RIGHT", 3, 0) fUnlearned:SetScript("OnClick", function() S.currentFilter = "unlearned"; TSUI.FullUpdate() end) MF.filterUnlearned = fUnlearned local sb = CreateFrame("EditBox", "SFramesTSSearchBox", MF) sb:SetPoint("TOPLEFT", MF, "TOPLEFT", L.SIDE_PAD, -(L.HEADER_H + 28)) sb:SetWidth(L.LEFT_W - L.SIDE_PAD * 2); sb:SetHeight(20) sb:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } }) sb:SetBackdropColor(T.searchBg[1], T.searchBg[2], T.searchBg[3], T.searchBg[4]) sb:SetBackdropBorderColor(T.searchBorder[1], T.searchBorder[2], T.searchBorder[3], T.searchBorder[4]) sb:SetFont(font, 11); sb:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) sb:SetTextInsets(6, 6, 2, 2); sb:SetAutoFocus(false); sb:SetMaxLetters(30) sb:SetScript("OnEscapePressed", function() this:ClearFocus() end) sb:SetScript("OnEnterPressed", function() this:ClearFocus() end) sb:SetScript("OnTextChanged", function() S.searchText = this:GetText() or ""; TSUI.FullUpdate() end) MF.searchBox = sb local sLabel = sb:CreateFontString(nil, "OVERLAY") sLabel:SetFont(font, 11, "OUTLINE"); sLabel:SetPoint("LEFT", sb, "LEFT", 6, 0) sLabel:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3], 0.6); sLabel:SetText("搜索...") MF.searchLabel = sLabel sb:SetScript("OnEditFocusGained", function() sLabel:Hide() end) sb:SetScript("OnEditFocusLost", function() if (this:GetText() or "") == "" then sLabel:Show() end end) -- List scroll area (fills left panel below filters) local listTop = L.HEADER_H + L.FILTER_H + 4 local ls = CreateFrame("ScrollFrame", "SFramesTSListScroll", MF) ls:SetPoint("TOPLEFT", MF, "TOPLEFT", L.SIDE_PAD, -listTop) ls:SetPoint("BOTTOMRIGHT", MF, "BOTTOMLEFT", L.SIDE_PAD + L.LIST_ROW_W, 6) local lc = CreateFrame("Frame", "SFramesTSListContent", ls) lc:SetWidth(L.LIST_ROW_W); lc:SetHeight(1) ls:SetScrollChild(lc) ls:EnableMouseWheel(true) -- Scrollbar track local sbTrack = CreateFrame("Frame", nil, MF) sbTrack:SetWidth(L.SCROLLBAR_W) sbTrack:SetPoint("TOPLEFT", ls, "TOPRIGHT", 2, 0) sbTrack:SetPoint("BOTTOMLEFT", ls, "BOTTOMRIGHT", 2, 0) sbTrack:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } }) sbTrack:SetBackdropColor(T.progressBg[1], T.progressBg[2], T.progressBg[3], 0.6) sbTrack:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.3) -- Scrollbar thumb local sbThumb = CreateFrame("Button", nil, sbTrack) sbThumb:SetWidth(L.SCROLLBAR_W - 2); sbThumb:SetHeight(30) sbThumb:SetPoint("TOP", sbTrack, "TOP", 0, -1) sbThumb:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } }) sbThumb:SetBackdropColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], 0.7) sbThumb:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.5) sbThumb:EnableMouse(true); sbThumb:SetMovable(true) sbThumb:RegisterForDrag("LeftButton") sbThumb._dragging = false sbThumb:SetScript("OnEnter", function() this:SetBackdropColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], 1) end) sbThumb:SetScript("OnLeave", function() this:SetBackdropColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], 0.7) end) sbThumb:SetScript("OnDragStart", function() this._dragging = true this._startY = select(2, GetCursorPosition()) / (this:GetEffectiveScale()) this._startScroll = ls:GetVerticalScroll() end) sbThumb:SetScript("OnDragStop", function() this._dragging = false end) sbThumb:SetScript("OnUpdate", function() if not this._dragging then return end local cursorY = select(2, GetCursorPosition()) / (this:GetEffectiveScale()) local delta = this._startY - cursorY local trackH = sbTrack:GetHeight() - this:GetHeight() if trackH <= 0 then return end local scrollMax = TSUI.GetScrollMax() local newScroll = this._startScroll + (delta / trackH) * scrollMax newScroll = math.max(0, math.min(scrollMax, newScroll)) ls:SetVerticalScroll(newScroll) TSUI.UpdateScrollbar() end) sbTrack:EnableMouse(true) sbTrack:SetScript("OnMouseDown", function() local trackTop = sbTrack:GetTop() local cursorY = select(2, GetCursorPosition()) / (sbTrack:GetEffectiveScale()) local clickRatio = (trackTop - cursorY) / sbTrack:GetHeight() clickRatio = math.max(0, math.min(1, clickRatio)) ls:SetVerticalScroll(clickRatio * TSUI.GetScrollMax()) TSUI.UpdateScrollbar() end) MF.sbTrack = sbTrack; MF.sbThumb = sbThumb function TSUI.GetScrollMax() local contentH = ls.content and ls.content:GetHeight() or 0 local viewH = ls:GetHeight() or 0 return math.max(0, contentH - viewH) end function TSUI.UpdateScrollbar() if not MF.sbThumb or not MF.sbTrack then return end local scrollMax = TSUI.GetScrollMax() if scrollMax <= 0 then MF.sbThumb:Hide(); return end MF.sbThumb:Show() local trackH = MF.sbTrack:GetHeight() local curScroll = ls:GetVerticalScroll() local ratio = curScroll / scrollMax ratio = math.max(0, math.min(1, ratio)) local thumbH = math.max(20, trackH * (trackH / (trackH + scrollMax))) MF.sbThumb:SetHeight(thumbH) local maxOffset = trackH - thumbH - 2 MF.sbThumb:ClearAllPoints() MF.sbThumb:SetPoint("TOP", MF.sbTrack, "TOP", 0, -(1 + ratio * maxOffset)) end ls:SetScript("OnMouseWheel", function() local cur = this:GetVerticalScroll(); local mx = TSUI.GetScrollMax() if arg1 > 0 then this:SetVerticalScroll(math.max(0, cur - L.SCROLL_STEP)) else this:SetVerticalScroll(math.min(mx, cur + L.SCROLL_STEP)) end TSUI.UpdateScrollbar() end) ls:SetScript("OnScrollRangeChanged", function() TSUI.UpdateScrollbar() end) ls.content = lc; MF.listScroll = ls for i = 1, L.MAX_ROWS do local row = TSUI.CreateListRow(lc, i) row:SetWidth(L.LIST_ROW_W) row:EnableMouseWheel(true) row:SetScript("OnMouseWheel", function() local sf = S.MainFrame.listScroll local cur = sf:GetVerticalScroll() local mx = TSUI.GetScrollMax() if arg1 > 0 then sf:SetVerticalScroll(math.max(0, cur - L.SCROLL_STEP)) else sf:SetVerticalScroll(math.min(mx, cur + L.SCROLL_STEP)) end TSUI.UpdateScrollbar() end) row:SetScript("OnClick", function() if IsShiftKeyDown() and this.recipeIndex and not this.isHeader then TSUI.LinkRecipeToChat(this.recipeIndex) elseif IsShiftKeyDown() and this.unlearnedData then TSUI.LinkUnlearnedToChat(this.unlearnedData) elseif this.isHeader and this.catName then TSUI.ToggleCategory(this.catName) elseif this.recipeIndex then TSUI.SelectRecipe(this.recipeIndex) elseif this.unlearnedData then TSUI.SelectUnlearned(this.unlearnedData) end end) S.rowButtons[i] = row end -- ═══ RIGHT PANEL: Recipe Detail ══════════════════════════════════ local rightX = L.LEFT_W + L.SIDE_PAD local rightW = L.CONTENT_W local det = CreateFrame("Frame", nil, MF) det:SetPoint("TOPLEFT", MF, "TOPLEFT", rightX, -(L.HEADER_H + 6)) det:SetPoint("BOTTOMRIGHT", MF, "BOTTOMRIGHT", -L.SIDE_PAD, L.BOTTOM_H + 2) MF.detail = det -- Recipe icon local dIF = CreateFrame("Frame", nil, det) dIF:SetWidth(40); dIF:SetHeight(40); dIF:SetPoint("TOPLEFT", det, "TOPLEFT", 0, 0) dIF: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 } }) dIF:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) dIF:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) dIF:Hide(); det.iconFrame = dIF local dIcon = dIF:CreateTexture(nil, "ARTWORK") dIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92) dIcon:SetPoint("TOPLEFT", dIF, "TOPLEFT", 3, -3) dIcon:SetPoint("BOTTOMRIGHT", dIF, "BOTTOMRIGHT", -3, 3) det.icon = dIcon local dGlow = dIF:CreateTexture(nil, "OVERLAY") dGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") dGlow:SetBlendMode("ADD"); dGlow:SetAlpha(0.8) dGlow:SetWidth(80); dGlow:SetHeight(80) dGlow:SetPoint("CENTER", dIF, "CENTER", 0, 0) dGlow:Hide(); det.qualGlow = dGlow dIF:EnableMouse(true) dIF:SetScript("OnEnter", function() if S.selectedIndex then GameTooltip:SetOwner(this, "ANCHOR_RIGHT") local ok if S.currentMode == "craft" then local link = GetCraftItemLink and GetCraftItemLink(S.selectedIndex) if link then ok = pcall(GameTooltip.SetCraftItem, GameTooltip, S.selectedIndex) else ok = pcall(GameTooltip.SetCraftSpell, GameTooltip, S.selectedIndex) end else ok = pcall(GameTooltip.SetTradeSkillItem, GameTooltip, S.selectedIndex) end if ok then GameTooltip:Show() else GameTooltip:Hide() end end end) dIF:SetScript("OnLeave", function() GameTooltip:Hide() end) local dName = det:CreateFontString(nil, "OVERLAY") dName:SetFont(font, 13, "OUTLINE") dName:SetPoint("TOPLEFT", dIF, "TOPRIGHT", 8, -2) dName:SetPoint("RIGHT", det, "RIGHT", -4, 0) dName:SetJustifyH("LEFT"); dName:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) det.nameFS = dName local dMade = det:CreateFontString(nil, "OVERLAY") dMade:SetFont(font, 10, "OUTLINE") dMade:SetPoint("TOPLEFT", dName, "BOTTOMLEFT", 0, -2) dMade:SetJustifyH("LEFT"); det.madeFS = dMade local dCD = det:CreateFontString(nil, "OVERLAY") dCD:SetFont(font, 10, "OUTLINE") dCD:SetPoint("TOPLEFT", det, "TOPLEFT", 0, -48) dCD:SetPoint("RIGHT", det, "RIGHT", -4, 0) dCD:SetJustifyH("LEFT"); det.cooldownFS = dCD local dTools = det:CreateFontString(nil, "OVERLAY") dTools:SetFont(font, 10, "OUTLINE") dTools:SetPoint("TOPLEFT", dCD, "BOTTOMLEFT", 0, -2) dTools:SetPoint("RIGHT", det, "RIGHT", -4, 0) dTools:SetJustifyH("LEFT"); det.toolsFS = dTools local dDesc = det:CreateFontString(nil, "OVERLAY") dDesc:SetFont(font, 10) dDesc:SetPoint("TOPLEFT", dTools, "BOTTOMLEFT", 0, -2) dDesc:SetPoint("RIGHT", det, "RIGHT", -4, 0) dDesc:SetJustifyH("LEFT") dDesc:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]); det.descFS = dDesc -- ── Current Difficulty ────────────────────────────────────────── local diffSep1 = det:CreateTexture(nil, "ARTWORK") diffSep1:SetTexture("Interface\\Buttons\\WHITE8X8"); diffSep1:SetHeight(1) diffSep1:SetPoint("TOPLEFT", det, "TOPLEFT", 0, -86) diffSep1:SetPoint("RIGHT", det, "RIGHT", 0, 0) diffSep1:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.3) local diffDot = det:CreateTexture(nil, "ARTWORK") diffDot:SetTexture("Interface\\Buttons\\WHITE8X8") diffDot:SetWidth(10); diffDot:SetHeight(10) diffDot:SetPoint("TOPLEFT", det, "TOPLEFT", 2, -94) diffDot:Hide(); det.diffDot = diffDot local diffFS = det:CreateFontString(nil, "OVERLAY") diffFS:SetFont(font, 11, "OUTLINE") diffFS:SetPoint("LEFT", diffDot, "RIGHT", 6, 0) diffFS:SetJustifyH("LEFT"); det.diffFS = diffFS local sourceFS = det:CreateFontString(nil, "OVERLAY") sourceFS:SetFont(font, 10, "OUTLINE") sourceFS:SetPoint("TOPLEFT", det, "TOPLEFT", 2, -108) sourceFS:SetJustifyH("LEFT"); det.sourceFS = sourceFS -- ── Reagent Section ───────────────────────────────────────────── local reagSep = det:CreateTexture(nil, "ARTWORK") reagSep:SetTexture("Interface\\Buttons\\WHITE8X8"); reagSep:SetHeight(1) reagSep:SetPoint("TOPLEFT", det, "TOPLEFT", 0, -126) reagSep:SetPoint("RIGHT", det, "RIGHT", 0, 0) reagSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.3) local rLabel = det:CreateFontString(nil, "OVERLAY") rLabel:SetFont(font, 10, "OUTLINE") rLabel:SetPoint("TOPLEFT", det, "TOPLEFT", 0, -132) rLabel:SetTextColor(T.sectionTitle[1], T.sectionTitle[2], T.sectionTitle[3]) rLabel:SetText("所需材料:") local rStartY = -150 for i = 1, L.MAX_REAGENTS do local slot = TSUI.CreateReagentSlot(det, i) local col = math.mod(i - 1, 2) local ri = math.floor((i - 1) / 2) slot:SetPoint("TOPLEFT", det, "TOPLEFT", col * (rightW / 2 + 2), rStartY - ri * 34) S.reagentSlots[i] = slot end -- ── Bottom Bar (right panel bottom) ───────────────────────────── local bsep = MF:CreateTexture(nil, "ARTWORK") bsep:SetTexture("Interface\\Buttons\\WHITE8X8"); bsep:SetHeight(1) bsep:SetPoint("BOTTOMLEFT", MF, "BOTTOMLEFT", L.LEFT_W + 4, L.BOTTOM_H) bsep:SetPoint("BOTTOMRIGHT", MF, "BOTTOMRIGHT", -4, L.BOTTOM_H) bsep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) MF.spinner = TSUI.CreateSpinner(MF, rightX, 8) local cBtn = TSUI.CreateActionBtn(MF, "制作", 70) cBtn:SetPoint("BOTTOMRIGHT", MF, "BOTTOMRIGHT", -(L.SIDE_PAD + 76), 8) cBtn:SetScript("OnClick", function() if this.disabled then return end if S.selectedIndex then local amt = S.MainFrame.spinner:GetValue() if S.currentMode == "craft" then for ci = 1, amt do API.DoRecipe(S.selectedIndex, 1) end else API.DoRecipe(S.selectedIndex, amt) end end end) MF.createBtn = cBtn local caBtn = TSUI.CreateActionBtn(MF, "全部制作", 70) caBtn:SetPoint("BOTTOMRIGHT", MF, "BOTTOMRIGHT", -L.SIDE_PAD, 8) caBtn:SetScript("OnClick", function() if this.disabled then return end if S.selectedIndex then local _, _, na = API.GetRecipeInfo(S.selectedIndex) if na and na > 0 then if S.currentMode == "craft" then for ci = 1, na do API.DoRecipe(S.selectedIndex, 1) end else API.DoRecipe(S.selectedIndex, na) end end end end) MF.createAllBtn = caBtn -- ═══ Events ══════════════════════════════════════════════════════ MF:SetScript("OnHide", function() S.switchStartTime = nil API.CloseRecipe() if S.currentMode == "tradeskill" then TSUI.CleanupBlizzardTradeSkill() else TSUI.CleanupBlizzardCraft() end end) MF:RegisterEvent("TRADE_SKILL_SHOW") MF:RegisterEvent("TRADE_SKILL_UPDATE") MF:RegisterEvent("TRADE_SKILL_CLOSE") MF:RegisterEvent("CRAFT_SHOW") MF:RegisterEvent("CRAFT_UPDATE") MF:RegisterEvent("CRAFT_CLOSE") MF:SetScript("OnEvent", function() if event == "TRADE_SKILL_SHOW" then S.switchStartTime = nil S.currentMode = "tradeskill" if TradeSkillFrame then TradeSkillFrame:SetScript("OnHide", function() end) TradeSkillFrame:SetAlpha(0); TradeSkillFrame:EnableMouse(false) end TSUI.ResetAndShow() S.MainFrame._hideBlizzTimer = 0 S.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) TSUI.CleanupBlizzardTradeSkill() end end) elseif event == "TRADE_SKILL_UPDATE" then if S.MainFrame:IsVisible() and S.currentMode == "tradeskill" then TSUI.UpdateProgressBar(); TSUI.FullUpdate() end elseif event == "TRADE_SKILL_CLOSE" then TSUI.CleanupBlizzardTradeSkill() if TSUI.IsTabSwitching() then -- switching: keep panel open else S.MainFrame._hideBlizzTimer = nil S.MainFrame:SetScript("OnUpdate", nil) S.MainFrame:Hide() end elseif event == "CRAFT_SHOW" then local craftName = GetCraftName and GetCraftName() or "" if craftName == "训练野兽" or craftName == "Beast Training" then return end S.switchStartTime = nil S.currentMode = "craft" if CraftFrame then CraftFrame:SetScript("OnHide", function() end) CraftFrame:SetAlpha(0); CraftFrame:EnableMouse(false) end TSUI.ResetAndShow() S.MainFrame._hideBlizzTimer = 0 S.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) TSUI.CleanupBlizzardCraft() end end) elseif event == "CRAFT_UPDATE" then if S.MainFrame:IsVisible() and S.currentMode == "craft" then TSUI.UpdateProgressBar(); TSUI.FullUpdate() end elseif event == "CRAFT_CLOSE" then TSUI.CleanupBlizzardCraft() if TSUI.IsTabSwitching() then -- switching: keep panel open else S.MainFrame._hideBlizzTimer = nil S.MainFrame:SetScript("OnUpdate", nil) S.MainFrame:Hide() end end end) TSUI.CreateProfTabs(MF) MF:Hide() tinsert(UISpecialFrames, "SFramesTradeSkillFrame") end function TSUI.ResetAndShow() if NanamiTradeSkillDB_Init then NanamiTradeSkillDB_Init() end S.selectedIndex = nil; S.selectedUnlearned = nil; S.currentFilter = "all"; S.searchText = "" S.collapsedCats = {}; S.craftAmount = 1 if S.MainFrame.searchBox then S.MainFrame.searchBox:SetText("") end if S.MainFrame.spinner then S.MainFrame.spinner:SetValue(1) end if S.MainFrame.listScroll then S.MainFrame.listScroll:SetVerticalScroll(0) end TSUI.UpdateProgressBar(); S.MainFrame:Show(); TSUI.FullUpdate() TSUI.UpdateScrollbar() TSUI.UpdateProfTabs() for _, entry in ipairs(S.displayList) do if entry.type == "recipe" then TSUI.SelectRecipe(entry.data.index); break end end 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.enableTradeSkill == nil then SFramesDB.enableTradeSkill = true end if SFramesDB.enableTradeSkill ~= false then TSUI:Initialize() end elseif event == "ADDON_LOADED" then if arg1 == "Blizzard_TradeSkillUI" then if S.MainFrame and TradeSkillFrame then TSUI.HideBlizzardTradeSkill() end elseif arg1 == "Blizzard_CraftUI" then if S.MainFrame and CraftFrame then TSUI.HideBlizzardCraft() end end end end)