-------------------------------------------------------------------------------- -- Nanami-UI: ConsumableUI.lua -- 食物药剂百科窗口 -------------------------------------------------------------------------------- SFrames = SFrames or {} SFrames.ConsumableUI = SFrames.ConsumableUI or {} local CUI = SFrames.ConsumableUI local FRAME_W = 600 local FRAME_H = 500 local HEADER_H = 34 local FILTER_H = 28 local ROLE_H = 28 local COL_H = 22 local ROW_H = 20 local MAX_ROWS = 18 local SIDE_PAD = 10 local COL_CAT_W = 86 local COL_NAME_W = 168 local COL_EFF_W = 232 local COL_DUR_W = 76 local S = { frame = nil, searchBox = nil, searchText = "", activeRole = "全部", rows = {}, tabBtns = {}, displayList = {}, filteredCount = 0, scrollOffset = 0, scrollMax = 0, summaryFS = nil, emptyFS = nil, scrollBar = nil, } local function GetTheme() local base = { panelBg = { 0.07, 0.07, 0.10, 0.96 }, panelBorder = { 0.35, 0.30, 0.40, 1.00 }, headerBg = { 0.10, 0.10, 0.14, 1.00 }, text = { 0.92, 0.88, 0.95 }, dimText = { 0.55, 0.50, 0.58 }, gold = { 1.00, 0.82, 0.40 }, slotBg = { 0.10, 0.10, 0.14, 0.90 }, slotBorder = { 0.30, 0.28, 0.35, 0.80 }, slotSelected = { 0.80, 0.60, 1.00 }, divider = { 0.28, 0.26, 0.32, 0.80 }, rowAlt = { 0.11, 0.10, 0.15, 0.60 }, green = { 0.40, 0.90, 0.50 }, } if SFrames and SFrames.ActiveTheme then for key, value in pairs(SFrames.ActiveTheme) do base[key] = value end end return base end local function GetFont() return SFrames and SFrames.GetFont and SFrames:GetFont() or "Fonts\\ARIALN.TTF" end local function ApplyBackdrop(frame, bg, border) local theme = GetTheme() local backdropBg = bg or theme.panelBg local backdropBorder = border or theme.panelBorder 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(backdropBg[1], backdropBg[2], backdropBg[3], backdropBg[4] or 0.96) frame:SetBackdropBorderColor( backdropBorder[1], backdropBorder[2], backdropBorder[3], backdropBorder[4] or 1.00 ) end local function SetDivider(parent, y, rightPadding) local theme = GetTheme() local divider = parent:CreateTexture(nil, "ARTWORK") divider:SetTexture("Interface\\Buttons\\WHITE8X8") divider:SetHeight(1) divider:SetPoint("TOPLEFT", parent, "TOPLEFT", SIDE_PAD, y) divider:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -(SIDE_PAD + (rightPadding or 0)), y) divider:SetVertexColor(theme.divider[1], theme.divider[2], theme.divider[3], theme.divider[4] or 0.8) end local function Utf8DisplayWidth(value) local width = 0 local index = 1 local len = string.len(value or "") while index <= len do local byte = string.byte(value, index) if byte < 128 then width = width + 7 index = index + 1 elseif byte < 224 then width = width + 7 index = index + 2 elseif byte < 240 then width = width + 11 index = index + 3 else width = width + 11 index = index + 4 end end return width end local function GetDatabase() local db = SFrames and SFrames.ConsumableDB if not db then return nil end if db.groups then return db end return { groups = db, roleOrder = nil, categoryOrder = nil, summary = nil, generatedAt = nil, } end local function GetGroups() local db = GetDatabase() return db and db.groups or nil end local function CountAllItems(groups) local count = 0 for _, group in ipairs(groups or {}) do count = count + table.getn(group.items or {}) end return count end local function CountAllCategories(groups) local seen = {} local count = 0 for _, group in ipairs(groups or {}) do for _, item in ipairs(group.items or {}) do if item.cat and item.cat ~= "" and not seen[item.cat] then seen[item.cat] = true count = count + 1 end end end return count end local function BuildRoleList() local db = GetDatabase() local roles = { "全部" } local seen = { ["全部"] = true } if not db or not db.groups then return roles end if db.roleOrder then for _, role in ipairs(db.roleOrder) do if role and role ~= "" and not seen[role] then seen[role] = true table.insert(roles, role) end end end for _, group in ipairs(db.groups) do if group.role and group.role ~= "" and not seen[group.role] then seen[group.role] = true table.insert(roles, group.role) end end return roles end local function RoleMatches(group) if S.activeRole == "全部" then return true end return group.role == S.activeRole end local function ItemMatches(item) if S.searchText == "" then return true end local query = string.lower(S.searchText) return string.find(string.lower(item.name or ""), query, 1, true) or string.find(string.lower(item.effect or ""), query, 1, true) or string.find(string.lower(item.cat or ""), query, 1, true) end function CUI:RefreshSummary() if not S.summaryFS then return end local db = GetDatabase() local groups = db and db.groups or {} local summary = db and db.summary or nil local roleCount = (summary and summary.roleCount) or table.getn(groups) local categoryCount = (summary and summary.categoryCount) or CountAllCategories(groups) local totalCount = (summary and summary.itemCount) or CountAllItems(groups) local text = string.format("%d 定位 / %d 类别 / %d 条", roleCount, categoryCount, totalCount) if S.activeRole ~= "全部" or S.searchText ~= "" then text = string.format("当前筛出 %d 条,数据库共 %d 条", S.filteredCount or 0, totalCount) end if db and db.generatedAt then text = text .. " · 更新于 " .. db.generatedAt end S.summaryFS:SetText(text) end function CUI:Filter() S.displayList = {} S.filteredCount = 0 local groups = GetGroups() if not groups then S.scrollOffset = 0 S.scrollMax = 0 self:RefreshSummary() return end for _, group in ipairs(groups) do if RoleMatches(group) then local matched = {} for _, item in ipairs(group.items or {}) do if ItemMatches(item) then table.insert(matched, item) end end if table.getn(matched) > 0 then table.insert(S.displayList, { isHeader = true, group = group, count = table.getn(matched), }) for _, item in ipairs(matched) do table.insert(S.displayList, { isHeader = false, item = item, }) end S.filteredCount = S.filteredCount + table.getn(matched) end end end S.scrollOffset = 0 S.scrollMax = math.max(0, table.getn(S.displayList) - MAX_ROWS) self:RefreshSummary() end local function UpdateScrollbar() local scrollBar = S.scrollBar if not scrollBar then return end local total = table.getn(S.displayList) if total <= MAX_ROWS then scrollBar:Hide() return end scrollBar:Show() local trackHeight = scrollBar:GetHeight() local thumbHeight = math.max(20, math.floor(trackHeight * MAX_ROWS / total + 0.5)) local percent = 0 if S.scrollMax > 0 then percent = S.scrollOffset / S.scrollMax end local thumbY = math.floor((trackHeight - thumbHeight) * percent + 0.5) if scrollBar.thumb then scrollBar.thumb:SetHeight(thumbHeight) scrollBar.thumb:SetPoint("TOP", scrollBar, "TOP", 0, -thumbY) end end function CUI:Render() local theme = GetTheme() local total = table.getn(S.displayList) local visibleItemIndex = 0 for i = 1, MAX_ROWS do local row = S.rows[i] if not row then break end local displayIndex = S.scrollOffset + i local entry = S.displayList[displayIndex] if entry then row:Show() if entry.isHeader then local headerText = entry.group.detail or entry.group.role or "未命名分组" if entry.count and entry.count > 0 then headerText = string.format("%s (%d)", headerText, entry.count) end row.catFS:SetText(headerText) row.catFS:SetTextColor( entry.group.color and entry.group.color[1] or theme.gold[1], entry.group.color and entry.group.color[2] or theme.gold[2], entry.group.color and entry.group.color[3] or theme.gold[3] ) row.catFS:SetWidth(COL_CAT_W + COL_NAME_W + COL_EFF_W + COL_DUR_W - 10) row.catFS:SetPoint("TOPLEFT", row, "TOPLEFT", 6, -3) row.nameFS:SetText("") row.effFS:SetText("") row.durFS:SetText("") row:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 0, }) row:SetBackdropColor(theme.headerBg[1], theme.headerBg[2], theme.headerBg[3], 0.85) row.entry = nil else local item = entry.item visibleItemIndex = visibleItemIndex + 1 row:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 0, }) if math.mod(visibleItemIndex, 2) == 0 then row:SetBackdropColor(theme.rowAlt[1], theme.rowAlt[2], theme.rowAlt[3], theme.rowAlt[4] or 0.6) else row:SetBackdropColor(theme.slotBg[1], theme.slotBg[2], theme.slotBg[3], 0.0) end row.catFS:SetWidth(COL_CAT_W - 8) row.catFS:SetText(item.cat or "") row.catFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3]) row.catFS:SetPoint("TOPLEFT", row, "TOPLEFT", 6, -3) if item.id and item.id > 0 then row.nameFS:SetTextColor(theme.gold[1], theme.gold[2], theme.gold[3]) else row.nameFS:SetTextColor(theme.text[1], theme.text[2], theme.text[3]) end row.nameFS:SetText(item.name or "") row.effFS:SetText(item.effect or "") row.effFS:SetTextColor(theme.text[1], theme.text[2], theme.text[3]) row.durFS:SetText(item.duration or "") row.durFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3]) row.entry = item end else row:Hide() row.entry = nil end end if S.emptyFS then if total == 0 then S.emptyFS:SetText("没有匹配到条目,试试更短的关键词或切回“全部”。") S.emptyFS:Show() else S.emptyFS:Hide() end end UpdateScrollbar() end local function RefreshTabs() local theme = GetTheme() for _, button in ipairs(S.tabBtns or {}) do local active = (button.roleName == S.activeRole) if active then button:SetBackdropBorderColor( theme.slotSelected[1], theme.slotSelected[2], theme.slotSelected[3], 1.00 ) button.fs:SetTextColor( theme.slotSelected[1], theme.slotSelected[2], theme.slotSelected[3] ) else button:SetBackdropBorderColor( theme.slotBorder[1], theme.slotBorder[2], theme.slotBorder[3], theme.slotBorder[4] or 0.8 ) button.fs:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3]) end end end local function BuildColumnLabel(parent, text, x, width) local font = GetFont() local theme = GetTheme() local fs = parent:CreateFontString(nil, "OVERLAY") fs:SetFont(font, 10, "OUTLINE") fs:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3]) fs:SetPoint("TOPLEFT", parent, "TOPLEFT", x, -4) fs:SetWidth(width) fs:SetJustifyH("LEFT") fs:SetText(text) return fs end function CUI:Build() local theme = GetTheme() local font = GetFont() local frame = CreateFrame("Frame", "NanamiConsumableFrame", UIParent) S.frame = frame frame:SetWidth(FRAME_W) frame:SetHeight(FRAME_H) frame:SetPoint("CENTER", UIParent, "CENTER", 60, 20) frame:SetFrameStrata("HIGH") frame:SetToplevel(true) frame:EnableMouse(true) frame:SetMovable(true) frame:RegisterForDrag("LeftButton") frame:SetScript("OnDragStart", function() this:StartMoving() end) frame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) frame:EnableMouseWheel(true) frame:SetScript("OnMouseWheel", function() if arg1 > 0 then S.scrollOffset = math.max(0, S.scrollOffset - 1) else S.scrollOffset = math.min(S.scrollMax, S.scrollOffset + 1) end CUI:Render() end) ApplyBackdrop(frame) tinsert(UISpecialFrames, "NanamiConsumableFrame") local header = CreateFrame("Frame", nil, frame) header:SetPoint("TOPLEFT", 0, 0) header:SetPoint("TOPRIGHT", 0, 0) header:SetHeight(HEADER_H) header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" }) header:SetBackdropColor(theme.headerBg[1], theme.headerBg[2], theme.headerBg[3], theme.headerBg[4] or 1.0) local titleFS = header:CreateFontString(nil, "OVERLAY") titleFS:SetFont(font, 13, "OUTLINE") titleFS:SetPoint("LEFT", header, "LEFT", SIDE_PAD + 4, 0) titleFS:SetText("食物药剂百科") titleFS:SetTextColor(theme.gold[1], theme.gold[2], theme.gold[3]) local closeBtn = CreateFrame("Button", nil, header) closeBtn:SetWidth(20) closeBtn:SetHeight(20) closeBtn:SetPoint("TOPRIGHT", header, "TOPRIGHT", -8, -7) 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(theme.dimText[1], theme.dimText[2], theme.dimText[3]) closeBtn:SetScript("OnClick", function() frame:Hide() end) closeBtn:SetScript("OnEnter", function() closeTex:SetVertexColor(1, 0.6, 0.7) end) closeBtn:SetScript("OnLeave", function() closeTex:SetVertexColor(theme.dimText[1], theme.dimText[2], theme.dimText[3]) end) SetDivider(frame, -HEADER_H, 16) local filterY = -(HEADER_H + 6) local searchFrame = CreateFrame("Frame", nil, frame) searchFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", SIDE_PAD, filterY) searchFrame:SetWidth(212) searchFrame:SetHeight(FILTER_H - 4) searchFrame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) searchFrame:SetBackdropColor(0.05, 0.05, 0.08, 0.90) searchFrame:SetBackdropBorderColor(theme.panelBorder[1], theme.panelBorder[2], theme.panelBorder[3], 0.80) local searchBox = CreateFrame("EditBox", "NanamiConsumableSearch", searchFrame) searchBox:SetPoint("TOPLEFT", searchFrame, "TOPLEFT", 4, -3) searchBox:SetPoint("BOTTOMRIGHT", searchFrame, "BOTTOMRIGHT", -4, 3) searchBox:SetFont(font, 11) searchBox:SetAutoFocus(false) searchBox:SetMaxLetters(40) searchBox:EnableMouse(true) S.searchBox = searchBox local hintFS = searchFrame:CreateFontString(nil, "OVERLAY") hintFS:SetFont(font, 10, "OUTLINE") hintFS:SetPoint("LEFT", searchFrame, "LEFT", 6, 0) hintFS:SetText("搜索名称 / 效果 / 类别") hintFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3]) searchBox:SetScript("OnTextChanged", function() local text = this:GetText() or "" S.searchText = text if text == "" then hintFS:Show() else hintFS:Hide() end CUI:Filter() CUI:Render() end) searchBox:SetScript("OnEditFocusGained", function() hintFS:Hide() end) searchBox:SetScript("OnEditFocusLost", function() if (this:GetText() or "") == "" then hintFS:Show() end end) searchBox:SetScript("OnEscapePressed", function() this:ClearFocus() end) local summaryFS = frame:CreateFontString(nil, "OVERLAY") summaryFS:SetFont(font, 10, "OUTLINE") summaryFS:SetPoint("LEFT", searchFrame, "RIGHT", 12, 0) summaryFS:SetPoint("RIGHT", frame, "RIGHT", -36, filterY - 12) summaryFS:SetJustifyH("RIGHT") summaryFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3]) summaryFS:SetText("") S.summaryFS = summaryFS local tabX = SIDE_PAD local tabY = filterY - FILTER_H S.tabBtns = {} for _, roleName in ipairs(BuildRoleList()) do local tabW = Utf8DisplayWidth(roleName) + 14 if tabX + tabW > FRAME_W - SIDE_PAD then tabX = SIDE_PAD tabY = tabY - (ROLE_H - 4) - 2 end local button = CreateFrame("Button", nil, frame) button:SetWidth(tabW) button:SetHeight(ROLE_H - 4) button:SetPoint("TOPLEFT", frame, "TOPLEFT", tabX, tabY) button:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) button:SetBackdropColor(theme.slotBg[1], theme.slotBg[2], theme.slotBg[3], 0.80) button:SetBackdropBorderColor(theme.slotBorder[1], theme.slotBorder[2], theme.slotBorder[3], 0.80) local buttonFS = button:CreateFontString(nil, "OVERLAY") buttonFS:SetFont(font, 10, "OUTLINE") buttonFS:SetPoint("CENTER", button, "CENTER", 0, 0) buttonFS:SetText(roleName) buttonFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3]) button.fs = buttonFS button.roleName = roleName button:SetScript("OnClick", function() S.activeRole = this.roleName RefreshTabs() CUI:Filter() CUI:Render() end) button:SetScript("OnEnter", function() if this.roleName ~= S.activeRole then this:SetBackdropBorderColor(theme.text[1], theme.text[2], theme.text[3], 0.60) end end) button:SetScript("OnLeave", function() if this.roleName ~= S.activeRole then this:SetBackdropBorderColor(theme.slotBorder[1], theme.slotBorder[2], theme.slotBorder[3], 0.80) end end) table.insert(S.tabBtns, button) tabX = tabX + tabW + 3 end RefreshTabs() local listStartY = tabY - ROLE_H + 2 SetDivider(frame, listStartY, 16) local colY = listStartY - 2 local colHeader = CreateFrame("Frame", nil, frame) colHeader:SetPoint("TOPLEFT", frame, "TOPLEFT", SIDE_PAD, colY) colHeader:SetWidth(FRAME_W - SIDE_PAD * 2 - 14) colHeader:SetHeight(COL_H) colHeader:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" }) colHeader:SetBackdropColor(theme.headerBg[1], theme.headerBg[2], theme.headerBg[3], 0.70) BuildColumnLabel(colHeader, "类别", 4, COL_CAT_W) BuildColumnLabel(colHeader, "名称", COL_CAT_W + 4, COL_NAME_W) BuildColumnLabel(colHeader, "效果", COL_CAT_W + COL_NAME_W + 4, COL_EFF_W) BuildColumnLabel(colHeader, "时长", COL_CAT_W + COL_NAME_W + COL_EFF_W + 4, COL_DUR_W) local rowAreaY = colY - COL_H - 1 local rowAreaH = FRAME_H - (-rowAreaY) - 8 local scrollWidth = 10 local scrollBar = CreateFrame("Frame", nil, frame) scrollBar:SetWidth(scrollWidth) scrollBar:SetHeight(rowAreaH) scrollBar:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -4, rowAreaY) scrollBar:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" }) scrollBar:SetBackdropColor(theme.slotBg[1], theme.slotBg[2], theme.slotBg[3], 0.40) local scrollThumb = scrollBar:CreateTexture(nil, "OVERLAY") scrollThumb:SetTexture("Interface\\Buttons\\WHITE8X8") scrollThumb:SetWidth(scrollWidth - 2) scrollThumb:SetHeight(40) scrollThumb:SetPoint("TOP", scrollBar, "TOP", 0, 0) scrollThumb:SetVertexColor(theme.slotSelected[1], theme.slotSelected[2], theme.slotSelected[3], 0.60) scrollBar.thumb = scrollThumb S.scrollBar = scrollBar local rowWidth = FRAME_W - SIDE_PAD * 2 - scrollWidth - 6 for i = 1, MAX_ROWS do local rowY = rowAreaY - (i - 1) * ROW_H local row = CreateFrame("Button", nil, frame) row:SetWidth(rowWidth) row:SetHeight(ROW_H) row:SetPoint("TOPLEFT", frame, "TOPLEFT", SIDE_PAD, rowY) local catFS = row:CreateFontString(nil, "OVERLAY") catFS:SetFont(font, 9, "OUTLINE") catFS:SetPoint("TOPLEFT", row, "TOPLEFT", 6, -3) catFS:SetWidth(COL_CAT_W - 8) catFS:SetJustifyH("LEFT") row.catFS = catFS local nameFS = row:CreateFontString(nil, "OVERLAY") nameFS:SetFont(font, 9, "OUTLINE") nameFS:SetPoint("TOPLEFT", row, "TOPLEFT", COL_CAT_W + 4, -3) nameFS:SetWidth(COL_NAME_W - 4) nameFS:SetJustifyH("LEFT") row.nameFS = nameFS local effFS = row:CreateFontString(nil, "OVERLAY") effFS:SetFont(font, 9, "OUTLINE") effFS:SetPoint("TOPLEFT", row, "TOPLEFT", COL_CAT_W + COL_NAME_W + 4, -3) effFS:SetWidth(COL_EFF_W - 4) effFS:SetJustifyH("LEFT") row.effFS = effFS local durFS = row:CreateFontString(nil, "OVERLAY") durFS:SetFont(font, 9, "OUTLINE") durFS:SetPoint("TOPLEFT", row, "TOPLEFT", COL_CAT_W + COL_NAME_W + COL_EFF_W + 4, -3) durFS:SetWidth(COL_DUR_W - 2) durFS:SetJustifyH("LEFT") row.durFS = durFS local highlight = row:CreateTexture(nil, "HIGHLIGHT") highlight:SetTexture("Interface\\Buttons\\WHITE8X8") highlight:SetAllPoints() highlight:SetVertexColor(1, 1, 1, 0.07) highlight:SetBlendMode("ADD") row:SetScript("OnEnter", function() local entry = this.entry if not entry then return end GameTooltip:SetOwner(this, "ANCHOR_RIGHT") if entry.id and entry.id > 0 then GameTooltip:SetHyperlink("item:" .. entry.id .. ":0:0:0") GameTooltip:AddLine("Shift+点击可发送物品链接", 0.55, 0.55, 0.60) else GameTooltip:SetText(entry.name or "", 1, 0.82, 0.40) if entry.effect and entry.effect ~= "" then GameTooltip:AddLine(entry.effect, 0.80, 0.80, 0.80) end if entry.duration and entry.duration ~= "" then GameTooltip:AddLine("持续时间: " .. entry.duration, 0.55, 0.55, 0.60) end end GameTooltip:Show() end) row:SetScript("OnLeave", function() GameTooltip:Hide() end) row:SetScript("OnClick", function() local entry = this.entry if not entry then return end if IsShiftKeyDown() and entry.id and entry.id > 0 then local _, link = GetItemInfo(entry.id) if link and ChatFrameEditBox then ChatFrameEditBox:Show() ChatFrameEditBox:Insert(link) end end end) row:Hide() S.rows[i] = row end local emptyFS = frame:CreateFontString(nil, "OVERLAY") emptyFS:SetFont(font, 11, "OUTLINE") emptyFS:SetPoint("CENTER", frame, "CENTER", 0, -28) emptyFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3]) emptyFS:SetText("") emptyFS:Hide() S.emptyFS = emptyFS end function CUI:Toggle() if S.frame and S.frame:IsShown() then S.frame:Hide() return end if not S.frame then self:Build() end self:Filter() self:Render() S.frame:Show() end function CUI:Hide() if S.frame then S.frame:Hide() end end