-------------------------------------------------------------------------------- -- S-Frames: In-Game Settings Panel (ConfigUI.lua) -- Pages: -- 1) UI Settings (scrollable) -- 2) Bag Settings -------------------------------------------------------------------------------- SFrames.ConfigUI = SFrames.ConfigUI or {} SFrames.ConfigUI.ready = true local PANEL_WIDTH = 700 local PANEL_HEIGHT = 560 local BAG_DEFAULTS = { enable = true, columns = 10, bagSpacing = 0, scale = 1, sellGrey = true, bankColumns = 12, bankSpacing = 0, bankScale = 1, } local widgetId = 0 local function NextWidgetName(prefix) widgetId = widgetId + 1 return "SFramesConfig" .. prefix .. tostring(widgetId) end local function Clamp(value, minValue, maxValue) if value < minValue then return minValue end if value > maxValue then return maxValue end return value end local function EnsureNumberDefault(tbl, key, value) if type(tbl[key]) ~= "number" then tbl[key] = value end end local function EnsureStringDefault(tbl, key, value) if type(tbl[key]) ~= "string" or tbl[key] == "" then tbl[key] = value end end local SOFT_THEME = SFrames.ActiveTheme local function EnsureSoftBackdrop(frame) if not frame then return end if frame.sfSoftBackdrop then return end 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.sfSoftBackdrop = true end -- Thin 1px border backdrop, suitable for small frames like checkbox boxes local function EnsureThinBackdrop(frame) if not frame then return end if frame.sfSoftBackdrop then return end if SFrames and SFrames.CreateBackdrop then SFrames:CreateBackdrop(frame) elseif frame.SetBackdrop then frame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) end frame.sfSoftBackdrop = true end local function HideTexture(tex) if not tex then return end if tex.SetTexture then tex:SetTexture(nil) end tex:Hide() end local function StyleButton(btn) if not btn or btn.sfSoftStyled then return btn end btn.sfSoftStyled = true -- Apply ESC menu style round backdrop btn: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 }, }) HideTexture(btn.GetNormalTexture and btn:GetNormalTexture()) HideTexture(btn.GetPushedTexture and btn:GetPushedTexture()) HideTexture(btn.GetHighlightTexture and btn:GetHighlightTexture()) HideTexture(btn.GetDisabledTexture and btn:GetDisabledTexture()) local name = btn:GetName() or "" for _, suffix in ipairs({ "Left", "Right", "Middle" }) do local tex = _G[name .. suffix] if tex then tex:SetAlpha(0) tex:Hide() end end local function SetVisual(state) local bg = SOFT_THEME.buttonBg local border = SOFT_THEME.buttonBorder local text = SOFT_THEME.buttonText if btn.sfSoftActive then bg = SOFT_THEME.buttonActiveBg border = SOFT_THEME.buttonActiveBorder text = SOFT_THEME.buttonActiveText elseif state == "hover" then bg = SOFT_THEME.buttonHoverBg border = (SFrames.ActiveTheme and SFrames.ActiveTheme.btnHoverBd) or { 0.80, 0.48, 0.64, 0.98 } text = SOFT_THEME.buttonActiveText elseif state == "down" then bg = SOFT_THEME.buttonDownBg elseif state == "disabled" then bg = SOFT_THEME.buttonDisabledBg text = SOFT_THEME.buttonDisabledText end if btn.SetBackdropColor then btn:SetBackdropColor(bg[1], bg[2], bg[3], bg[4]) end if btn.SetBackdropBorderColor then btn:SetBackdropBorderColor(border[1], border[2], border[3], border[4]) end local fs = btn.GetFontString and btn:GetFontString() if fs then fs:SetTextColor(text[1], text[2], text[3]) end end btn.RefreshVisual = function() if btn.sfSoftActive then SetVisual("active") elseif btn.IsEnabled and not btn:IsEnabled() then SetVisual("disabled") else SetVisual("normal") end end local oldEnter = btn:GetScript("OnEnter") local oldLeave = btn:GetScript("OnLeave") local oldDown = btn:GetScript("OnMouseDown") local oldUp = btn:GetScript("OnMouseUp") btn:SetScript("OnEnter", function() if oldEnter then oldEnter() end if this.IsEnabled and this:IsEnabled() and not this.sfSoftActive then SetVisual("hover") end end) btn:SetScript("OnLeave", function() if oldLeave then oldLeave() end if this.IsEnabled and this:IsEnabled() and not this.sfSoftActive then SetVisual("normal") end end) btn:SetScript("OnMouseDown", function() if oldDown then oldDown() end if this.IsEnabled and this:IsEnabled() and not this.sfSoftActive then SetVisual("down") end end) btn:SetScript("OnMouseUp", function() if oldUp then oldUp() end if this.IsEnabled and this:IsEnabled() and not this.sfSoftActive then SetVisual("hover") end end) btn:RefreshVisual() return btn end local function AddBtnIcon(btn, iconKey, size, align) if not (SFrames and SFrames.CreateIcon) then return end local sz = size or 12 local gap = 3 local ico = SFrames:CreateIcon(btn, iconKey, sz) ico:SetDrawLayer("OVERLAY") btn.nanamiIcon = ico local fs = btn.GetFontString and btn:GetFontString() if fs then fs:ClearAllPoints() if align == "left" then ico:SetPoint("LEFT", btn, "LEFT", 8, 0) fs:SetPoint("LEFT", ico, "RIGHT", gap, 0) fs:SetPoint("RIGHT", btn, "RIGHT", -4, 0) fs:SetJustifyH("LEFT") else fs:SetPoint("CENTER", btn, "CENTER", (sz + gap) / 2, 0) ico:SetPoint("RIGHT", fs, "LEFT", -gap, 0) end else ico:SetPoint("CENTER", btn, "CENTER", 0, 0) end end local function StyleCheckBox(cb) if not cb or cb.sfSoftStyled then return cb end cb.sfSoftStyled = true -- Hide ALL default CheckButton textures including the checked texture HideTexture(cb.GetNormalTexture and cb:GetNormalTexture()) HideTexture(cb.GetPushedTexture and cb:GetPushedTexture()) HideTexture(cb.GetHighlightTexture and cb:GetHighlightTexture()) HideTexture(cb.GetDisabledTexture and cb:GetDisabledTexture()) if cb.SetCheckedTexture then cb:SetCheckedTexture("") end if cb.SetDisabledCheckedTexture then cb:SetDisabledCheckedTexture("") end -- Round-corner box (tooltip border = soft rounded edges) local box = CreateFrame("Frame", nil, cb) box:SetPoint("TOPLEFT", cb, "TOPLEFT", 0, 0) box:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", 0, 0) box:SetFrameLevel(cb:GetFrameLevel() + 1) box:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 10, insets = { left = 2, right = 2, top = 2, bottom = 2 }, }) cb.sfBox = box local function ApplyState(isChecked) if isChecked then box:SetBackdropColor( SOFT_THEME.checkFill[1], SOFT_THEME.checkFill[2], SOFT_THEME.checkFill[3], 0.88) box:SetBackdropBorderColor( SOFT_THEME.checkFill[1], SOFT_THEME.checkFill[2], SOFT_THEME.checkFill[3], 1) else box:SetBackdropColor( SOFT_THEME.checkBg[1], SOFT_THEME.checkBg[2], SOFT_THEME.checkBg[3], SOFT_THEME.checkBg[4]) box:SetBackdropBorderColor( SOFT_THEME.checkBorder[1], SOFT_THEME.checkBorder[2], SOFT_THEME.checkBorder[3], SOFT_THEME.checkBorder[4]) end end cb.sfApplyState = ApplyState -- Text label: push it away from the box with a left margin local label = _G[cb:GetName() .. "Text"] if label then label:ClearAllPoints() label:SetPoint("LEFT", cb, "RIGHT", 4, 0) label:SetFont(SFrames:GetFont(), 11, "OUTLINE") label:SetJustifyH("LEFT") label:SetTextColor(SOFT_THEME.text[1], SOFT_THEME.text[2], SOFT_THEME.text[3]) end -- Hover: brighten border when unchecked cb:SetScript("OnEnter", function() if not this.sfChecked then if this.sfBox then this.sfBox:SetBackdropBorderColor( SOFT_THEME.checkHoverBorder[1], SOFT_THEME.checkHoverBorder[2], SOFT_THEME.checkHoverBorder[3], SOFT_THEME.checkHoverBorder[4]) end end end) cb:SetScript("OnLeave", function() if not this.sfChecked then if this.sfBox then this.sfBox:SetBackdropBorderColor( SOFT_THEME.checkBorder[1], SOFT_THEME.checkBorder[2], SOFT_THEME.checkBorder[3], SOFT_THEME.checkBorder[4]) end end end) ApplyState(false) return cb end local function StyleSlider(slider, low, high, text) if not slider or slider.sfSoftStyled then return slider end slider.sfSoftStyled = true local regions = { slider:GetRegions() } for i = 1, table.getn(regions) do local region = regions[i] if region and region.GetObjectType and region:GetObjectType() == "Texture" then region:SetTexture(nil) end end local track = slider:CreateTexture(nil, "BACKGROUND") track:SetTexture("Interface\\Buttons\\WHITE8X8") track:SetPoint("LEFT", slider, "LEFT", 0, 0) track:SetPoint("RIGHT", slider, "RIGHT", 0, 0) track:SetHeight(4) track:SetVertexColor(SOFT_THEME.sliderTrack[1], SOFT_THEME.sliderTrack[2], SOFT_THEME.sliderTrack[3], SOFT_THEME.sliderTrack[4]) slider.sfTrack = track local fill = slider:CreateTexture(nil, "ARTWORK") fill:SetTexture("Interface\\Buttons\\WHITE8X8") fill:SetPoint("LEFT", track, "LEFT", 0, 0) fill:SetPoint("TOP", track, "TOP", 0, 0) fill:SetPoint("BOTTOM", track, "BOTTOM", 0, 0) fill:SetWidth(1) fill:SetVertexColor(SOFT_THEME.sliderFill[1], SOFT_THEME.sliderFill[2], SOFT_THEME.sliderFill[3], SOFT_THEME.sliderFill[4]) slider.sfFill = fill if slider.SetThumbTexture then slider:SetThumbTexture("Interface\\Buttons\\WHITE8X8") end local thumb = slider.GetThumbTexture and slider:GetThumbTexture() if thumb then thumb:SetWidth(8) thumb:SetHeight(16) thumb:SetVertexColor(SOFT_THEME.sliderThumb[1], SOFT_THEME.sliderThumb[2], SOFT_THEME.sliderThumb[3], SOFT_THEME.sliderThumb[4]) end local function UpdateFill() local minValue, maxValue = slider:GetMinMaxValues() local value = slider:GetValue() or minValue local pct = 0 if maxValue > minValue then pct = (value - minValue) / (maxValue - minValue) end pct = Clamp(pct, 0, 1) local width = math.floor((slider:GetWidth() or 1) * pct + 0.5) if width < 1 then width = 1 end slider.sfFill:SetWidth(width) end local oldChanged = slider:GetScript("OnValueChanged") slider:SetScript("OnValueChanged", function() if oldChanged then oldChanged() end UpdateFill() end) UpdateFill() local font = (SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" if low then low:SetFont(font, 8, "OUTLINE") low:SetTextColor(SOFT_THEME.dimText[1], SOFT_THEME.dimText[2], SOFT_THEME.dimText[3]) low:ClearAllPoints() low:SetWidth(40) low:SetJustifyH("LEFT") low:SetPoint("TOPLEFT", slider, "BOTTOMLEFT", 10, 0) end if high then high:SetFont(font, 8, "OUTLINE") high:SetTextColor(SOFT_THEME.dimText[1], SOFT_THEME.dimText[2], SOFT_THEME.dimText[3]) high:ClearAllPoints() high:SetWidth(40) high:SetJustifyH("RIGHT") high:SetPoint("TOPRIGHT", slider, "BOTTOMRIGHT", -10, 0) end if text then text:SetFont(font, 9, "OUTLINE") text:SetTextColor(SOFT_THEME.text[1], SOFT_THEME.text[2], SOFT_THEME.text[3]) text:ClearAllPoints() text:SetPoint("BOTTOM", slider, "TOP", 0, 2) end return slider end local function StyleScrollBar(slider, sliderName) if not slider or slider.sfSoftStyled then return slider end slider.sfSoftStyled = true slider:SetWidth(12) local regions = { slider:GetRegions() } for i = 1, table.getn(regions) do local region = regions[i] if region and region.GetObjectType and region:GetObjectType() == "Texture" then region:SetTexture(nil) end end local track = slider:CreateTexture(nil, "BACKGROUND") track:SetTexture("Interface\\Buttons\\WHITE8X8") track:SetPoint("TOPLEFT", slider, "TOPLEFT", 3, 0) track:SetPoint("BOTTOMRIGHT", slider, "BOTTOMRIGHT", -3, 0) track:SetVertexColor(0.18, 0.19, 0.22, 0.9) if slider.SetThumbTexture then slider:SetThumbTexture("Interface\\Buttons\\WHITE8X8") end local thumb = slider.GetThumbTexture and slider:GetThumbTexture() if thumb then thumb:SetWidth(8) thumb:SetHeight(20) thumb:SetVertexColor(SOFT_THEME.scrollThumb[1], SOFT_THEME.scrollThumb[2], SOFT_THEME.scrollThumb[3], SOFT_THEME.scrollThumb[4] or 0.95) end local upBtn = _G[sliderName .. "ScrollUpButton"] local downBtn = _G[sliderName .. "ScrollDownButton"] if upBtn then upBtn:Hide() end if downBtn then downBtn:Hide() end return slider end local function EnsureDB() if not SFramesDB then SFramesDB = {} end if SFramesDB.showLevel == nil then SFramesDB.showLevel = true end if SFramesDB.classColorHealth == nil then SFramesDB.classColorHealth = true end if SFramesDB.enableMerchant == nil then SFramesDB.enableMerchant = true end if SFramesDB.enableQuestUI == nil then SFramesDB.enableQuestUI = true end if SFramesDB.enableQuestLogSkin == nil then SFramesDB.enableQuestLogSkin = true end if SFramesDB.enableTrainer == nil then SFramesDB.enableTrainer = true end if SFramesDB.enableSpellBook == nil then SFramesDB.enableSpellBook = true end if SFramesDB.enableTradeSkill == nil then SFramesDB.enableTradeSkill = true end if SFramesDB.spellBookHighestOnly == nil then SFramesDB.spellBookHighestOnly = false end if SFramesDB.spellBookAutoReplace == nil then SFramesDB.spellBookAutoReplace = false end if SFramesDB.enableSocial == nil then SFramesDB.enableSocial = true end if SFramesDB.enableInspect == nil then SFramesDB.enableInspect = true end if SFramesDB.enableFlightMap == nil then SFramesDB.enableFlightMap = true end if SFramesDB.enablePetStable == nil then SFramesDB.enablePetStable = true end if SFramesDB.enableMail == nil then SFramesDB.enableMail = true end if type(SFramesDB.spellBookScale) ~= "number" then SFramesDB.spellBookScale = 1 end if type(SFramesDB.socialScale) ~= "number" then SFramesDB.socialScale = 1 end if type(SFramesDB.playerFrameScale) ~= "number" then SFramesDB.playerFrameScale = 1 end if type(SFramesDB.playerFrameWidth) ~= "number" then SFramesDB.playerFrameWidth = 220 end if type(SFramesDB.playerPortraitWidth) ~= "number" then SFramesDB.playerPortraitWidth = 50 end if type(SFramesDB.playerHealthHeight) ~= "number" then SFramesDB.playerHealthHeight = 38 end if type(SFramesDB.playerPowerHeight) ~= "number" then SFramesDB.playerPowerHeight = 9 end if SFramesDB.playerShowClass == nil then SFramesDB.playerShowClass = false end if SFramesDB.playerShowClassIcon == nil then SFramesDB.playerShowClassIcon = true end if SFramesDB.playerShowPortrait == nil then SFramesDB.playerShowPortrait = true end if type(SFramesDB.playerFrameAlpha) ~= "number" then SFramesDB.playerFrameAlpha = 1 end if type(SFramesDB.playerBgAlpha) ~= "number" then SFramesDB.playerBgAlpha = 0.9 end if type(SFramesDB.playerNameFontSize) ~= "number" then SFramesDB.playerNameFontSize = 10 end if type(SFramesDB.playerValueFontSize) ~= "number" then SFramesDB.playerValueFontSize = 10 end local defaultPlayerPowerWidth = math.max(60, SFramesDB.playerFrameWidth - ((SFramesDB.playerShowPortrait ~= false) and SFramesDB.playerPortraitWidth or 0) - 2) if type(SFramesDB.playerPowerWidth) ~= "number" or math.abs(SFramesDB.playerPowerWidth - SFramesDB.playerFrameWidth) < 0.5 or math.abs(SFramesDB.playerPowerWidth - 168) < 0.5 then SFramesDB.playerPowerWidth = defaultPlayerPowerWidth end if type(SFramesDB.playerPowerOffsetX) ~= "number" then SFramesDB.playerPowerOffsetX = 0 end if type(SFramesDB.playerPowerOffsetY) ~= "number" then SFramesDB.playerPowerOffsetY = 0 end if SFramesDB.playerPowerOnTop == nil then SFramesDB.playerPowerOnTop = false end if SFramesDB.playerShowBorder == nil then SFramesDB.playerShowBorder = false end if type(SFramesDB.playerCornerRadius) ~= "number" then SFramesDB.playerCornerRadius = 0 end if type(SFramesDB.playerPortraitBgAlpha) ~= "number" then SFramesDB.playerPortraitBgAlpha = SFramesDB.playerBgAlpha end if SFramesDB.playerHealthTexture == nil then SFramesDB.playerHealthTexture = "" end if SFramesDB.playerPowerTexture == nil then SFramesDB.playerPowerTexture = "" end if SFramesDB.playerNameFontKey == nil then SFramesDB.playerNameFontKey = "" end if SFramesDB.playerHealthFontKey == nil then SFramesDB.playerHealthFontKey = "" end if SFramesDB.playerPowerFontKey == nil then SFramesDB.playerPowerFontKey = "" end if type(SFramesDB.playerHealthFontSize) ~= "number" then SFramesDB.playerHealthFontSize = SFramesDB.playerValueFontSize end if type(SFramesDB.playerPowerFontSize) ~= "number" then SFramesDB.playerPowerFontSize = SFramesDB.playerValueFontSize end if type(SFramesDB.targetFrameScale) ~= "number" then SFramesDB.targetFrameScale = 1 end if type(SFramesDB.targetFrameWidth) ~= "number" then SFramesDB.targetFrameWidth = 220 end if type(SFramesDB.targetPortraitWidth) ~= "number" then SFramesDB.targetPortraitWidth = 50 end if type(SFramesDB.targetHealthHeight) ~= "number" then SFramesDB.targetHealthHeight = 38 end if type(SFramesDB.targetPowerHeight) ~= "number" then SFramesDB.targetPowerHeight = 9 end if SFramesDB.targetShowClass == nil then SFramesDB.targetShowClass = false end if SFramesDB.targetShowClassIcon == nil then SFramesDB.targetShowClassIcon = true end if SFramesDB.targetShowPortrait == nil then SFramesDB.targetShowPortrait = true end if type(SFramesDB.targetFrameAlpha) ~= "number" then SFramesDB.targetFrameAlpha = 1 end if type(SFramesDB.targetBgAlpha) ~= "number" then SFramesDB.targetBgAlpha = 0.9 end if type(SFramesDB.targetNameFontSize) ~= "number" then SFramesDB.targetNameFontSize = 10 end if type(SFramesDB.targetValueFontSize) ~= "number" then SFramesDB.targetValueFontSize = 10 end local defaultTargetPowerWidth = math.max(60, SFramesDB.targetFrameWidth - ((SFramesDB.targetShowPortrait ~= false) and SFramesDB.targetPortraitWidth or 0) - 2) if type(SFramesDB.targetPowerWidth) ~= "number" or math.abs(SFramesDB.targetPowerWidth - SFramesDB.targetFrameWidth) < 0.5 then SFramesDB.targetPowerWidth = defaultTargetPowerWidth end if type(SFramesDB.targetPowerOffsetX) ~= "number" then SFramesDB.targetPowerOffsetX = 0 end if type(SFramesDB.targetPowerOffsetY) ~= "number" then SFramesDB.targetPowerOffsetY = 0 end if SFramesDB.targetPowerOnTop == nil then SFramesDB.targetPowerOnTop = false end if SFramesDB.targetShowBorder == nil then SFramesDB.targetShowBorder = false end if type(SFramesDB.targetCornerRadius) ~= "number" then SFramesDB.targetCornerRadius = 0 end if type(SFramesDB.targetPortraitBgAlpha) ~= "number" then SFramesDB.targetPortraitBgAlpha = SFramesDB.targetBgAlpha end if SFramesDB.targetHealthTexture == nil then SFramesDB.targetHealthTexture = "" end if SFramesDB.targetPowerTexture == nil then SFramesDB.targetPowerTexture = "" end if SFramesDB.targetNameFontKey == nil then SFramesDB.targetNameFontKey = "" end if SFramesDB.targetHealthFontKey == nil then SFramesDB.targetHealthFontKey = "" end if SFramesDB.targetPowerFontKey == nil then SFramesDB.targetPowerFontKey = "" end if type(SFramesDB.targetHealthFontSize) ~= "number" then SFramesDB.targetHealthFontSize = SFramesDB.targetValueFontSize end if type(SFramesDB.targetPowerFontSize) ~= "number" then SFramesDB.targetPowerFontSize = SFramesDB.targetValueFontSize end if SFramesDB.targetDistanceEnabled == nil then SFramesDB.targetDistanceEnabled = true end if type(SFramesDB.targetDistanceScale) ~= "number" then SFramesDB.targetDistanceScale = 1 end if type(SFramesDB.targetDistanceFontSize) ~= "number" then SFramesDB.targetDistanceFontSize = 14 end if SFramesDB.targetDistanceFontKey == nil then SFramesDB.targetDistanceFontKey = "" end if type(SFramesDB.fontKey) ~= "string" then SFramesDB.fontKey = "default" end -- Focus frame defaults if SFramesDB.focusEnabled == nil then SFramesDB.focusEnabled = true end if type(SFramesDB.focusFrameScale) ~= "number" then SFramesDB.focusFrameScale = 0.9 end if type(SFramesDB.focusFrameWidth) ~= "number" then SFramesDB.focusFrameWidth = 200 end if type(SFramesDB.focusPortraitWidth) ~= "number" then SFramesDB.focusPortraitWidth = 45 end if type(SFramesDB.focusHealthHeight) ~= "number" then SFramesDB.focusHealthHeight = 32 end if type(SFramesDB.focusPowerHeight) ~= "number" then SFramesDB.focusPowerHeight = 10 end if SFramesDB.focusShowPortrait == nil then SFramesDB.focusShowPortrait = true end if SFramesDB.focusShowCastBar == nil then SFramesDB.focusShowCastBar = true end if SFramesDB.focusShowAuras == nil then SFramesDB.focusShowAuras = true end if type(SFramesDB.focusNameFontSize) ~= "number" then SFramesDB.focusNameFontSize = 11 end if type(SFramesDB.focusValueFontSize) ~= "number" then SFramesDB.focusValueFontSize = 10 end if type(SFramesDB.focusBgAlpha) ~= "number" then SFramesDB.focusBgAlpha = 0.9 end local defaultFocusPowerWidth = math.max(60, SFramesDB.focusFrameWidth - ((SFramesDB.focusShowPortrait ~= false) and SFramesDB.focusPortraitWidth or 0) - 2) if type(SFramesDB.focusPowerWidth) ~= "number" or math.abs(SFramesDB.focusPowerWidth - SFramesDB.focusFrameWidth) < 0.5 then SFramesDB.focusPowerWidth = defaultFocusPowerWidth end if type(SFramesDB.focusPowerOffsetX) ~= "number" then SFramesDB.focusPowerOffsetX = 0 end if type(SFramesDB.focusPowerOffsetY) ~= "number" then SFramesDB.focusPowerOffsetY = 0 end if SFramesDB.focusPowerOnTop == nil then SFramesDB.focusPowerOnTop = false end if SFramesDB.focusShowBorder == nil then SFramesDB.focusShowBorder = false end if type(SFramesDB.focusCornerRadius) ~= "number" then SFramesDB.focusCornerRadius = 0 end if type(SFramesDB.focusPortraitBgAlpha) ~= "number" then SFramesDB.focusPortraitBgAlpha = SFramesDB.focusBgAlpha end if SFramesDB.focusHealthTexture == nil then SFramesDB.focusHealthTexture = "" end if SFramesDB.focusPowerTexture == nil then SFramesDB.focusPowerTexture = "" end if SFramesDB.focusNameFontKey == nil then SFramesDB.focusNameFontKey = "" end if SFramesDB.focusHealthFontKey == nil then SFramesDB.focusHealthFontKey = "" end if SFramesDB.focusPowerFontKey == nil then SFramesDB.focusPowerFontKey = "" end if SFramesDB.focusCastFontKey == nil then SFramesDB.focusCastFontKey = "" end if SFramesDB.focusDistanceFontKey == nil then SFramesDB.focusDistanceFontKey = "" end if type(SFramesDB.focusHealthFontSize) ~= "number" then SFramesDB.focusHealthFontSize = SFramesDB.focusValueFontSize end if type(SFramesDB.focusPowerFontSize) ~= "number" then SFramesDB.focusPowerFontSize = SFramesDB.focusValueFontSize end if type(SFramesDB.focusCastFontSize) ~= "number" then SFramesDB.focusCastFontSize = 10 end if type(SFramesDB.focusDistanceFontSize) ~= "number" then SFramesDB.focusDistanceFontSize = 14 end if SFramesDB.showPetFrame == nil then SFramesDB.showPetFrame = true end if type(SFramesDB.petFrameScale) ~= "number" then SFramesDB.petFrameScale = 1 end if SFramesDB.partyLayout ~= "horizontal" and SFramesDB.partyLayout ~= "vertical" then SFramesDB.partyLayout = "vertical" end if type(SFramesDB.partyFrameScale) ~= "number" then SFramesDB.partyFrameScale = 1 end if type(SFramesDB.partyFrameWidth) ~= "number" then SFramesDB.partyFrameWidth = 150 end if type(SFramesDB.partyFrameHeight) ~= "number" then SFramesDB.partyFrameHeight = 35 end if type(SFramesDB.partyPortraitWidth) ~= "number" then SFramesDB.partyPortraitWidth = 33 end if type(SFramesDB.partyHealthHeight) ~= "number" then SFramesDB.partyHealthHeight = 22 end if type(SFramesDB.partyPowerHeight) ~= "number" then SFramesDB.partyPowerHeight = 10 end if type(SFramesDB.partyHorizontalGap) ~= "number" then SFramesDB.partyHorizontalGap = 8 end if type(SFramesDB.partyVerticalGap) ~= "number" then SFramesDB.partyVerticalGap = 30 end if type(SFramesDB.partyNameFontSize) ~= "number" then SFramesDB.partyNameFontSize = 10 end if type(SFramesDB.partyValueFontSize) ~= "number" then SFramesDB.partyValueFontSize = 10 end local defaultPartyPowerWidth = math.max(40, SFramesDB.partyFrameWidth - SFramesDB.partyPortraitWidth - 5) if type(SFramesDB.partyPowerWidth) ~= "number" or math.abs(SFramesDB.partyPowerWidth - SFramesDB.partyFrameWidth) < 0.5 then SFramesDB.partyPowerWidth = defaultPartyPowerWidth end if type(SFramesDB.partyPowerOffsetX) ~= "number" then SFramesDB.partyPowerOffsetX = 0 end if type(SFramesDB.partyPowerOffsetY) ~= "number" then SFramesDB.partyPowerOffsetY = 0 end if SFramesDB.partyPowerOnTop == nil then SFramesDB.partyPowerOnTop = false end if SFramesDB.partyShowBorder == nil then SFramesDB.partyShowBorder = false end if type(SFramesDB.partyCornerRadius) ~= "number" then SFramesDB.partyCornerRadius = 0 end if type(SFramesDB.partyPortraitBgAlpha) ~= "number" then SFramesDB.partyPortraitBgAlpha = SFramesDB.partyBgAlpha end if SFramesDB.partyHealthTexture == nil then SFramesDB.partyHealthTexture = "" end if SFramesDB.partyPowerTexture == nil then SFramesDB.partyPowerTexture = "" end if SFramesDB.partyNameFontKey == nil then SFramesDB.partyNameFontKey = "" end if SFramesDB.partyHealthFontKey == nil then SFramesDB.partyHealthFontKey = "" end if SFramesDB.partyPowerFontKey == nil then SFramesDB.partyPowerFontKey = "" end if type(SFramesDB.partyHealthFontSize) ~= "number" then SFramesDB.partyHealthFontSize = SFramesDB.partyValueFontSize end if type(SFramesDB.partyPowerFontSize) ~= "number" then SFramesDB.partyPowerFontSize = SFramesDB.partyValueFontSize end if SFramesDB.partyShowBuffs == nil then SFramesDB.partyShowBuffs = true end if SFramesDB.partyShowDebuffs == nil then SFramesDB.partyShowDebuffs = true end if SFramesDB.partyPortrait3D == nil then SFramesDB.partyPortrait3D = true end if type(SFramesDB.partyBgAlpha) ~= "number" then SFramesDB.partyBgAlpha = 0.9 end if SFramesDB.totHealthTexture == nil then SFramesDB.totHealthTexture = "" end if SFramesDB.petHealthTexture == nil then SFramesDB.petHealthTexture = "" end if SFramesDB.petPowerTexture == nil then SFramesDB.petPowerTexture = "" end if SFramesDB.raidLayout ~= "horizontal" and SFramesDB.raidLayout ~= "vertical" then SFramesDB.raidLayout = "horizontal" end if type(SFramesDB.raidFrameScale) ~= "number" then SFramesDB.raidFrameScale = 1 end if type(SFramesDB.raidFrameWidth) ~= "number" then SFramesDB.raidFrameWidth = 60 end if type(SFramesDB.raidFrameHeight) ~= "number" then SFramesDB.raidFrameHeight = 40 end if type(SFramesDB.raidHealthHeight) ~= "number" then SFramesDB.raidHealthHeight = 31 end if type(SFramesDB.raidHorizontalGap) ~= "number" then SFramesDB.raidHorizontalGap = 2 end if type(SFramesDB.raidVerticalGap) ~= "number" then SFramesDB.raidVerticalGap = 2 end if type(SFramesDB.raidGroupGap) ~= "number" then SFramesDB.raidGroupGap = 8 end if type(SFramesDB.raidNameFontSize) ~= "number" then SFramesDB.raidNameFontSize = 10 end if type(SFramesDB.raidValueFontSize) ~= "number" then SFramesDB.raidValueFontSize = 9 end local defaultRaidPowerWidth = math.max(20, SFramesDB.raidFrameWidth - 2) if type(SFramesDB.raidPowerWidth) ~= "number" or math.abs(SFramesDB.raidPowerWidth - SFramesDB.raidFrameWidth) < 0.5 then SFramesDB.raidPowerWidth = defaultRaidPowerWidth end if type(SFramesDB.raidPowerOffsetX) ~= "number" then SFramesDB.raidPowerOffsetX = 0 end if type(SFramesDB.raidPowerOffsetY) ~= "number" then SFramesDB.raidPowerOffsetY = 0 end if SFramesDB.raidPowerOnTop == nil then SFramesDB.raidPowerOnTop = false end if SFramesDB.raidShowBorder == nil then SFramesDB.raidShowBorder = false end if type(SFramesDB.raidCornerRadius) ~= "number" then SFramesDB.raidCornerRadius = 0 end if SFramesDB.raidHealthTexture == nil then SFramesDB.raidHealthTexture = "" end if SFramesDB.raidPowerTexture == nil then SFramesDB.raidPowerTexture = "" end if SFramesDB.raidNameFontKey == nil then SFramesDB.raidNameFontKey = "" end if SFramesDB.raidHealthFontKey == nil then SFramesDB.raidHealthFontKey = "" end if SFramesDB.raidPowerFontKey == nil then SFramesDB.raidPowerFontKey = "" end if type(SFramesDB.raidHealthFontSize) ~= "number" then SFramesDB.raidHealthFontSize = SFramesDB.raidValueFontSize end if type(SFramesDB.raidPowerFontSize) ~= "number" then SFramesDB.raidPowerFontSize = SFramesDB.raidValueFontSize end if SFramesDB.raidShowPower == nil then SFramesDB.raidShowPower = true end if SFramesDB.enableRaidFrames == nil then SFramesDB.enableRaidFrames = true end if SFramesDB.raidHealthFormat == nil then SFramesDB.raidHealthFormat = "compact" end if SFramesDB.raidShowGroupLabel == nil then SFramesDB.raidShowGroupLabel = true end if type(SFramesDB.raidBgAlpha) ~= "number" then SFramesDB.raidBgAlpha = 0.9 end if SFramesDB.charPanelEnable == nil then SFramesDB.charPanelEnable = true end if SFramesDB.afkEnabled == nil then SFramesDB.afkEnabled = true end if type(SFramesDB.afkDelay) ~= "number" then SFramesDB.afkDelay = 5 end if SFramesDB.afkOutsideRest == nil then SFramesDB.afkOutsideRest = false end if SFramesDB.smoothBars == nil then SFramesDB.smoothBars = true end if SFramesDB.mobRealHealth == nil then SFramesDB.mobRealHealth = true end if SFramesDB.showItemLevel == nil then SFramesDB.showItemLevel = true end if SFramesDB.showTooltipIDs == nil then SFramesDB.showTooltipIDs = true end if SFramesDB.itemCompare == nil then SFramesDB.itemCompare = true end if SFramesDB.gearScore == nil then SFramesDB.gearScore = true end if type(SFramesDB.Bags) ~= "table" then SFramesDB.Bags = {} end local defaults = BAG_DEFAULTS if SFrames and SFrames.Config and type(SFrames.Config.Bags) == "table" then defaults = SFrames.Config.Bags end for key, value in pairs(defaults) do if SFramesDB.Bags[key] == nil then SFramesDB.Bags[key] = value end end if type(SFramesDB.Bags.alpha) ~= "number" then SFramesDB.Bags.alpha = 1 end if type(SFramesDB.Bags.bankAlpha) ~= "number" then SFramesDB.Bags.bankAlpha = 1 end if type(SFramesDB.Bags.bgAlpha) ~= "number" then SFramesDB.Bags.bgAlpha = 0.95 end if type(SFramesDB.Minimap) ~= "table" then SFramesDB.Minimap = {} end if SFramesDB.Minimap.enabled == nil then SFramesDB.Minimap.enabled = true end if type(SFramesDB.Minimap.scale) ~= "number" then SFramesDB.Minimap.scale = 1.0 end if SFramesDB.Minimap.showClock == nil then SFramesDB.Minimap.showClock = true end if SFramesDB.Minimap.showCoords == nil then SFramesDB.Minimap.showCoords = true end if SFramesDB.Minimap.mapStyle == nil then SFramesDB.Minimap.mapStyle = "auto" end if SFramesDB.Minimap.mapShape == nil then SFramesDB.Minimap.mapShape = "square1" end if type(SFramesDB.Minimap.posX) ~= "number" then SFramesDB.Minimap.posX = -5 end if type(SFramesDB.Minimap.posY) ~= "number" then SFramesDB.Minimap.posY = -5 end if type(SFramesDB.MapReveal) ~= "table" then SFramesDB.MapReveal = {} end if SFramesDB.MapReveal.enabled == nil then SFramesDB.MapReveal.enabled = true end if type(SFramesDB.MapReveal.unexploredAlpha) ~= "number" then SFramesDB.MapReveal.unexploredAlpha = 0.7 end if type(SFramesDB.Tweaks) ~= "table" then SFramesDB.Tweaks = {} end if SFramesDB.Tweaks.autoStance == nil then SFramesDB.Tweaks.autoStance = true end if SFramesDB.Tweaks.superWoW == nil then SFramesDB.Tweaks.superWoW = true end if SFramesDB.Tweaks.turtleCompat == nil then SFramesDB.Tweaks.turtleCompat = true end if SFramesDB.Tweaks.cooldownNumbers == nil then SFramesDB.Tweaks.cooldownNumbers = true end if SFramesDB.Tweaks.combatNotify == nil then SFramesDB.Tweaks.combatNotify = true end if SFramesDB.Tweaks.darkUI == nil then SFramesDB.Tweaks.darkUI = false end if SFramesDB.Tweaks.worldMapWindow == nil then SFramesDB.Tweaks.worldMapWindow = false end if SFramesDB.Tweaks.mouseoverCast == nil then SFramesDB.Tweaks.mouseoverCast = false end if type(SFramesDB.WorldMap) ~= "table" then SFramesDB.WorldMap = {} end if SFramesDB.WorldMap.enabled == nil then SFramesDB.WorldMap.enabled = true end if type(SFramesDB.WorldMap.scale) ~= "number" then SFramesDB.WorldMap.scale = 0.85 end if type(SFramesDB.WorldMap.nav) ~= "table" then SFramesDB.WorldMap.nav = { enabled = false, width = 350, alpha = 0.70, locked = false } end if type(SFramesDB.MinimapBuffs) ~= "table" then SFramesDB.MinimapBuffs = {} end if SFramesDB.MinimapBuffs.enabled == nil then SFramesDB.MinimapBuffs.enabled = true end if type(SFramesDB.MinimapBuffs.iconSize) ~= "number" then SFramesDB.MinimapBuffs.iconSize = 30 end if type(SFramesDB.MinimapBuffs.iconsPerRow) ~= "number" then SFramesDB.MinimapBuffs.iconsPerRow = 8 end if type(SFramesDB.MinimapBuffs.spacing) ~= "number" then SFramesDB.MinimapBuffs.spacing = 2 end if SFramesDB.MinimapBuffs.growDirection ~= "LEFT" and SFramesDB.MinimapBuffs.growDirection ~= "RIGHT" then SFramesDB.MinimapBuffs.growDirection = "LEFT" end if SFramesDB.MinimapBuffs.position ~= "TOPRIGHT" and SFramesDB.MinimapBuffs.position ~= "TOPLEFT" and SFramesDB.MinimapBuffs.position ~= "BOTTOMRIGHT" and SFramesDB.MinimapBuffs.position ~= "BOTTOMLEFT" then SFramesDB.MinimapBuffs.position = "TOPRIGHT" end if type(SFramesDB.MinimapBuffs.offsetX) ~= "number" then SFramesDB.MinimapBuffs.offsetX = 0 end if type(SFramesDB.MinimapBuffs.offsetY) ~= "number" then SFramesDB.MinimapBuffs.offsetY = 0 end if SFramesDB.MinimapBuffs.showTimer == nil then SFramesDB.MinimapBuffs.showTimer = true end if SFramesDB.MinimapBuffs.showDebuffs == nil then SFramesDB.MinimapBuffs.showDebuffs = true end if type(SFramesDB.MinimapBuffs.debuffIconSize) ~= "number" then SFramesDB.MinimapBuffs.debuffIconSize = 30 end if type(SFramesDB.MinimapBuffs.bgAlpha) ~= "number" then SFramesDB.MinimapBuffs.bgAlpha = 0.92 end if type(SFramesDB.ActionBars) ~= "table" then SFramesDB.ActionBars = {} end local abDef = { enable = true, buttonSize = 36, buttonGap = 2, smallBarSize = 27, scale = 1.0, barCount = 3, showHotkey = true, showMacroName = false, rangeColoring = true, showPetBar = true, showStanceBar = true, showRightBars = true, buttonRounded = false, buttonInnerShadow = false, alpha = 1.0, bgAlpha = 0.9, rightBar1PerRow = 1, rightBar2PerRow = 1, bottomBar1PerRow = 12, bottomBar2PerRow = 12, bottomBar3PerRow = 12, } for k, v in pairs(abDef) do if SFramesDB.ActionBars[k] == nil then SFramesDB.ActionBars[k] = v end end if type(SFramesDB.ExtraBar) ~= "table" then SFramesDB.ExtraBar = {} end local ebDef = { enable = false, buttonCount = 12, perRow = 12, buttonSize = 36, buttonGap = 2, align = "center", startSlot = 73, alpha = 1.0, showHotkey = true, showCount = true, buttonRounded = false, buttonInnerShadow = false, } for k, v in pairs(ebDef) do if SFramesDB.ExtraBar[k] == nil then SFramesDB.ExtraBar[k] = v end end if SFramesDB.enableUnitFrames == nil then SFramesDB.enableUnitFrames = true end if SFramesDB.enablePlayerFrame == nil then SFramesDB.enablePlayerFrame = true end if SFramesDB.enableTargetFrame == nil then SFramesDB.enableTargetFrame = true end if SFramesDB.enablePartyFrame == nil then SFramesDB.enablePartyFrame = true end if SFramesDB.enableChat == nil then SFramesDB.enableChat = true end if type(SFramesDB.chatBgAlpha) ~= "number" then SFramesDB.chatBgAlpha = 0.9 end if type(SFramesDB.Theme) ~= "table" then SFramesDB.Theme = {} end if SFramesDB.Theme.preset == nil then SFramesDB.Theme.preset = "pink" end if SFramesDB.Theme.useClassTheme == nil then SFramesDB.Theme.useClassTheme = false end if SFramesDB.Theme.iconSet == nil or SFramesDB.Theme.iconSet == "default" then SFramesDB.Theme.iconSet = "icon" end end local function ApplyBgAlphaToFrame(frame, alpha) if not frame or not frame.SetBackdropColor then return end local A = SFrames.ActiveTheme if A and A.panelBg then frame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], alpha) else frame:SetBackdropColor(0.1, 0.1, 0.1, alpha) end end local function CreateSection(parent, title, x, y, width, height, font) local section = CreateFrame("Frame", NextWidgetName("Section"), parent) section:SetWidth(width) section:SetHeight(height) section:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) EnsureSoftBackdrop(section) if section.SetBackdropColor then section:SetBackdropColor(SOFT_THEME.sectionBg[1], SOFT_THEME.sectionBg[2], SOFT_THEME.sectionBg[3], SOFT_THEME.sectionBg[4]) end if section.SetBackdropBorderColor then section:SetBackdropBorderColor(SOFT_THEME.sectionBorder[1], SOFT_THEME.sectionBorder[2], SOFT_THEME.sectionBorder[3], SOFT_THEME.sectionBorder[4]) end local titleFS = section:CreateFontString(nil, "OVERLAY") titleFS:SetFont(font, 11, "OUTLINE") titleFS:SetPoint("TOPLEFT", section, "TOPLEFT", 10, -9) titleFS:SetText(title) titleFS:SetTextColor(SOFT_THEME.title[1], SOFT_THEME.title[2], SOFT_THEME.title[3]) -- Divider line under title local div = section:CreateTexture(nil, "ARTWORK") div:SetTexture("Interface\\Buttons\\WHITE8X8") div:SetHeight(1) div:SetPoint("TOPLEFT", section, "TOPLEFT", 8, -24) div:SetPoint("TOPRIGHT", section, "TOPRIGHT", -8, -24) div:SetVertexColor(SOFT_THEME.sectionBorder[1], SOFT_THEME.sectionBorder[2], SOFT_THEME.sectionBorder[3], 0.6) return section end local function CreateLabel(parent, text, x, y, font, size, r, g, b) local fs = parent:CreateFontString(nil, "OVERLAY") fs:SetFont(font, size or 11, "OUTLINE") fs:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) fs:SetText(text) fs:SetTextColor(r or 1, g or 1, b or 1) return fs end local function CreateDesc(parent, text, x, y, font, maxWidth) local fs = parent:CreateFontString(nil, "OVERLAY") fs:SetFont(font, 9, "OUTLINE") fs:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) if maxWidth then fs:SetWidth(maxWidth) fs:SetJustifyH("LEFT") end fs:SetText(text) fs:SetTextColor(0.65, 0.58, 0.62) return fs end local function CreateCheckBox(parent, label, x, y, getter, setter, onValueChanged) local cb = CreateFrame("CheckButton", NextWidgetName("Check"), parent, "UICheckButtonTemplate") cb:SetWidth(18) cb:SetHeight(18) cb:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) StyleCheckBox(cb) local text = _G[cb:GetName() .. "Text"] if text then text:SetText(label) local parentWidth = parent:GetWidth() or 520 local textWidth = parentWidth - x - 24 if textWidth > 240 then textWidth = 240 end text:SetWidth(textWidth) end local internalRefresh = false cb:SetScript("OnClick", function() if internalRefresh then return end local v = this:GetChecked() local checkedBool = (v == 1 or v == true) this.sfChecked = checkedBool if this.sfApplyState then this.sfApplyState(checkedBool) end setter(checkedBool) if onValueChanged then onValueChanged(checkedBool) end PlaySound(checkedBool and "igMainMenuOptionCheckBoxOn" or "igMainMenuOptionCheckBoxOff") end) cb.Refresh = function() internalRefresh = true local isChecked = getter() and true or false cb:SetChecked(isChecked and 1 or 0) cb.sfChecked = isChecked if cb.sfApplyState then cb.sfApplyState(isChecked) end internalRefresh = false end cb:Refresh() return cb end local function CreateSlider(parent, label, x, y, width, minValue, maxValue, step, getter, setter, formatter, onValueChanged) local sliderName = NextWidgetName("Slider") local slider = CreateFrame("Slider", sliderName, parent, "OptionsSliderTemplate") slider:SetWidth(width) slider:SetHeight(26) slider:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) slider:SetMinMaxValues(minValue, maxValue) slider:SetValueStep(step) if slider.SetObeyStepOnDrag then slider:SetObeyStepOnDrag(true) end local low = _G[sliderName .. "Low"] local high = _G[sliderName .. "High"] local text = _G[sliderName .. "Text"] if low then low:SetText(tostring(minValue)) end if high then high:SetText(tostring(maxValue)) end local internalRefresh = false local function UpdateLabel(value) local display = formatter and formatter(value) or value if text then text:SetText(label .. ": " .. tostring(display)) end end slider:SetScript("OnValueChanged", function() if internalRefresh then return end local raw = this:GetValue() or minValue or 0 local safeStep = step or 1 local value if safeStep >= 1 then value = math.floor(raw + 0.5) else if safeStep == 0 then safeStep = 1 end local scaled = raw / safeStep value = math.floor(scaled + 0.5) * safeStep end value = Clamp(value, minValue, maxValue) UpdateLabel(value) setter(value) if onValueChanged then onValueChanged(value) end end) slider.Refresh = function() local value = tonumber(getter()) or minValue value = Clamp(value, minValue, maxValue) internalRefresh = true slider:SetValue(value) internalRefresh = false UpdateLabel(value) end StyleSlider(slider, low, high, text) slider:Refresh() return slider end local function CreateButton(parent, text, x, y, width, height, onClick) local btn = CreateFrame("Button", NextWidgetName("Button"), parent, "UIPanelButtonTemplate") btn:SetWidth(width) btn:SetHeight(height) btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) btn:SetText(text) btn:SetScript("OnClick", onClick) StyleButton(btn) return btn end local function CreateOptionGrid(parent, title, x, y, width, options, getter, setter, previewKind, previewSize, onValueChanged) local font = SFrames:GetFont() local controls = {} local cols = previewKind == "font" and 3 or 5 local btnW = previewKind == "font" and 150 or 96 local btnH = previewKind == "font" and 22 or 48 local gapX = previewKind == "font" and 8 or 6 local gapY = 6 CreateLabel(parent, title, x, y, font, 10, 0.85, 0.75, 0.80) local buttons = {} for idx, entry in ipairs(options) do local col = math.mod(idx - 1, cols) local row = math.floor((idx - 1) / cols) local bx = x + col * (btnW + gapX) local by = y - 18 - row * (btnH + gapY) local btn = CreateFrame("Button", NextWidgetName("OptionGrid"), parent) btn:SetWidth(btnW) btn:SetHeight(btnH) btn:SetPoint("TOPLEFT", parent, "TOPLEFT", bx, by) btn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) btn:SetBackdropColor(0.08, 0.08, 0.10, 0.9) btn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) btn.optionKey = entry.key btn.optionLabel = entry.label if previewKind == "bar" then local preview = CreateFrame("StatusBar", NextWidgetName("OptionGridBar"), btn) preview:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2) preview:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 14) preview:SetStatusBarTexture(entry.path) preview:SetStatusBarColor(0.2, 0.85, 0.3, 1) preview:SetMinMaxValues(0, 1) preview:SetValue(1) local label = btn:CreateFontString(nil, "OVERLAY") label:SetFont(font, 9, "OUTLINE") label:SetPoint("BOTTOM", btn, "BOTTOM", 0, 3) label:SetText(entry.label) label:SetTextColor(0.8, 0.8, 0.8) else local label = btn:CreateFontString(nil, "OVERLAY") local ok = pcall(label.SetFont, label, entry.path, previewSize or 11, "OUTLINE") if not ok then label:SetFont(font, previewSize or 11, "OUTLINE") end label:SetPoint("CENTER", btn, "CENTER", 0, 0) label:SetText(entry.label) label:SetTextColor(0.9, 0.9, 0.9) end local selectBorder = btn:CreateTexture(nil, "OVERLAY") selectBorder:SetTexture("Interface\\Buttons\\WHITE8X8") selectBorder:SetPoint("TOPLEFT", btn, "TOPLEFT", -1, 1) selectBorder:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 1, -1) selectBorder:SetVertexColor(1, 0.85, 0.4, 0.8) selectBorder:Hide() btn.selectBorder = selectBorder btn:SetScript("OnClick", function() setter(this.optionKey) for _, b in ipairs(buttons) do local active = b.optionKey == this.optionKey b:SetBackdropBorderColor(active and 1 or 0.3, active and 0.85 or 0.3, active and 0.6 or 0.35, 1) if b.selectBorder then if active then b.selectBorder:Show() else b.selectBorder:Hide() end end end if onValueChanged then onValueChanged(this.optionKey) end end) btn:SetScript("OnEnter", function() if this.optionKey ~= getter() then this:SetBackdropBorderColor(0.7, 0.7, 0.8, 1) end GameTooltip:SetOwner(this, "ANCHOR_TOP") GameTooltip:SetText(this.optionLabel) GameTooltip:Show() end) btn:SetScript("OnLeave", function() local active = this.optionKey == getter() this:SetBackdropBorderColor(active and 1 or 0.3, active and 0.85 or 0.3, active and 0.6 or 0.35, 1) GameTooltip:Hide() end) btn.Refresh = function() local active = btn.optionKey == getter() btn:SetBackdropBorderColor(active and 1 or 0.3, active and 0.85 or 0.3, active and 0.6 or 0.35, 1) if btn.selectBorder then if active then btn.selectBorder:Show() else btn.selectBorder:Hide() end end end btn:Refresh() table.insert(buttons, btn) table.insert(controls, btn) end local rows = math.floor((table.getn(options) + cols - 1) / cols) return controls, (rows * btnH) + ((rows - 1) * gapY) + 18 end local function CreateScrollArea(parent, x, y, width, height, childHeight) local holder = CreateFrame("Frame", NextWidgetName("ScrollHolder"), parent) holder:SetWidth(width) holder:SetHeight(height) holder:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) local scroll = CreateFrame("ScrollFrame", NextWidgetName("ScrollFrame"), holder) scroll:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, 0) scroll:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -18, 0) local child = CreateFrame("Frame", NextWidgetName("ScrollChild"), scroll) child:SetWidth(width - 22) child:SetHeight(childHeight) scroll:SetScrollChild(child) local sliderName = NextWidgetName("ScrollBar") local slider = CreateFrame("Slider", sliderName, holder, "UIPanelScrollBarTemplate") slider:SetPoint("TOPRIGHT", holder, "TOPRIGHT", 0, -16) slider:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", 0, 16) slider:SetScript("OnValueChanged", function() if scroll and scroll.SetVerticalScroll then scroll:SetVerticalScroll(this:GetValue()) end end) slider:SetMinMaxValues(0, 0) slider:SetValueStep(20) slider:SetValue(0) local upBtn = _G[sliderName .. "ScrollUpButton"] if upBtn then upBtn:SetScript("OnClick", function() local value = slider:GetValue() - 24 if value < 0 then value = 0 end slider:SetValue(value) end) end local downBtn = _G[sliderName .. "ScrollDownButton"] if downBtn then downBtn:SetScript("OnClick", function() local _, maxValue = slider:GetMinMaxValues() local value = slider:GetValue() + 24 if value > maxValue then value = maxValue end slider:SetValue(value) end) end StyleScrollBar(slider, sliderName) local function UpdateRange() local maxScroll = child:GetHeight() - scroll:GetHeight() if maxScroll < 0 then maxScroll = 0 end slider:SetMinMaxValues(0, maxScroll) slider:SetValueStep(20) local current = slider:GetValue() if current > maxScroll then current = maxScroll slider:SetValue(current) end if scroll and scroll.SetVerticalScroll then scroll:SetVerticalScroll(current) end if maxScroll <= 0 then slider:Hide() else slider:Show() end end local function ScrollBy(delta) local _, maxValue = slider:GetMinMaxValues() local value = slider:GetValue() - delta * 28 if value < 0 then value = 0 end if value > maxValue then value = maxValue end slider:SetValue(value) end if scroll.EnableMouseWheel then scroll:EnableMouseWheel(true) scroll:SetScript("OnMouseWheel", function() ScrollBy(arg1) end) end if child.EnableMouseWheel then child:EnableMouseWheel(true) child:SetScript("OnMouseWheel", function() ScrollBy(arg1) end) end return { holder = holder, scroll = scroll, child = child, slider = slider, UpdateRange = UpdateRange, Reset = function() slider:SetValue(0) if scroll and scroll.SetVerticalScroll then scroll:SetVerticalScroll(0) end UpdateRange() end, } end function SFrames.ConfigUI:RefreshControls(controlList) if not controlList then return end for _, control in ipairs(controlList) do if control and control.Refresh then control:Refresh() end end end function SFrames.ConfigUI:BuildUIPage() local font = SFrames:GetFont() local page = self.uiPage local controls = {} local function RefreshPlayer() if SFrames.Player and SFrames.Player.ApplyConfig then SFrames.Player:ApplyConfig() return end if SFrames.Player and SFrames.Player.UpdateAll then SFrames.Player:UpdateAll() end end local function RefreshTarget() if SFrames.Target and SFrames.Target.ApplyConfig then SFrames.Target:ApplyConfig() return end if SFrames.Target and SFrames.Target.UpdateAll then SFrames.Target:UpdateAll() end end local function RefreshParty() if SFrames.Party and SFrames.Party.ApplyConfig then SFrames.Party:ApplyConfig() return end if SFrames.Party and SFrames.Party.UpdateAll then SFrames.Party:UpdateAll() end end -- Section 内容从 y=-32 开始(标题24 + 分隔线 + 8px 间距) -- 每个选项行占 26px,描述文字占 16px local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 1090) local root = uiScroll.child -- ── 初始化向导 ────────────────────────────────────────────── local wizSection = CreateSection(root, "初始化向导", 8, -8, 520, 62, font) CreateDesc(wizSection, "重新打开首次配置向导,可重新选择各项功能开关", 14, -30, font, 380) CreateButton(wizSection, "重新运行初始化向导", 370, -30, 140, 24, function() if SFrames.ConfigUI.frame then SFrames.ConfigUI.frame:Hide() end if SFrames.SetupWizard and SFrames.SetupWizard.Show then SFrames.SetupWizard:Show(nil, "rerun") end end) -- ── 布局模式 ────────────────────────────────────────────────── local layoutSection = CreateSection(root, "布局模式", 8, -78, 520, 62, font) CreateDesc(layoutSection, "进入布局模式,拖拽调整所有 UI 元素的位置", 14, -30, font, 380) CreateButton(layoutSection, "进入布局模式", 370, -30, 140, 24, function() if SFrames.ConfigUI.frame then SFrames.ConfigUI.frame:Hide() end if SFrames.Movers and SFrames.Movers.ToggleLayoutMode then SFrames.Movers:ToggleLayoutMode() end end) -- ── UI 缩放 ────────────────────────────────────────────────── local scaleSection = CreateSection(root, "UI 缩放", 8, -148, 520, 82, font) local function ApplyUIScaleKeepPos(newScale) UIParent:SetScale(newScale) local panel = SFrames.ConfigUI.frame if panel and panel:IsShown() then panel:ClearAllPoints() panel:SetPoint("CENTER", UIParent, "CENTER", 0, 0) end end table.insert(controls, CreateCheckBox(scaleSection, "启用自定义 UI 缩放", 14, -34, function() return GetCVar("useUiScale") == "1" end, function(checked) SetCVar("useUiScale", checked and "1" or "0") if checked then local s = tonumber(GetCVar("uiScale")) or 1.0 ApplyUIScaleKeepPos(s) else ApplyUIScaleKeepPos(1.0) end end )) CreateDesc(scaleSection, "调用系统 UI 缩放,对所有界面元素生效", 36, -50, font) local uiScaleSlider = CreateSlider(scaleSection, "缩放比例", 270, -34, 200, 0.64, 1.00, 0.01, function() return tonumber(GetCVar("uiScale")) or 1.0 end, function(value) SetCVar("uiScale", tostring(value)) end, function(v) return string.format("%.0f%%", v * 100) end ) table.insert(controls, uiScaleSlider) uiScaleSlider:SetScript("OnMouseUp", function() if GetCVar("useUiScale") == "1" then local s = tonumber(GetCVar("uiScale")) or 1.0 ApplyUIScaleKeepPos(s) end end) -- ── 全局 ────────────────────────────────────────────────────── local globalSection = CreateSection(root, "全局", 8, -238, 520, 288, font) table.insert(controls, CreateCheckBox(globalSection, "显示玩家/目标等级文本", 14, -34, function() return SFramesDB.showLevel ~= false end, function(checked) SFramesDB.showLevel = checked end, function() RefreshPlayer() RefreshTarget() RefreshParty() end )) CreateDesc(globalSection, "在玩家和目标框体中显示角色等级数字", 36, -50, font) table.insert(controls, CreateCheckBox(globalSection, "玩家/目标生命条使用职业颜色", 14, -70, function() return SFramesDB.classColorHealth ~= false end, function(checked) SFramesDB.classColorHealth = checked end, function() RefreshPlayer() RefreshTarget() RefreshParty() end )) CreateDesc(globalSection, "血条颜色跟随职业(关闭则统一绿色)", 36, -86, font) table.insert(controls, CreateCheckBox(globalSection, "启用全新商人购买界面", 14, -106, function() return SFramesDB.enableMerchant ~= false end, function(checked) SFramesDB.enableMerchant = checked end )) CreateDesc(globalSection, "替换默认商人窗口为自定义界面", 36, -122, font) table.insert(controls, CreateCheckBox(globalSection, "启用全新任务/NPC对话界面", 14, -142, function() return SFramesDB.enableQuestUI ~= false end, function(checked) SFramesDB.enableQuestUI = checked end )) CreateDesc(globalSection, "替换默认任务和NPC对话窗口(需重载UI)", 36, -158, font) table.insert(controls, CreateCheckBox(globalSection, "启用任务日志美化皮肤", 14, -178, function() return SFramesDB.enableQuestLogSkin ~= false end, function(checked) SFramesDB.enableQuestLogSkin = checked end )) CreateDesc(globalSection, "美化任务日志界面,兼容 pfQuest(需重载UI)", 36, -194, font) table.insert(controls, CreateCheckBox(globalSection, "启用全新训练师界面", 270, -34, function() return SFramesDB.enableTrainer ~= false end, function(checked) SFramesDB.enableTrainer = checked end )) CreateDesc(globalSection, "替换默认职业/专业训练师窗口(需重载UI)", 292, -50, font) table.insert(controls, CreateCheckBox(globalSection, "启用全新法术书界面", 270, -70, function() return SFramesDB.enableSpellBook ~= false end, function(checked) SFramesDB.enableSpellBook = checked end )) CreateDesc(globalSection, "替换默认法术书窗口(需重载UI)", 292, -86, font) table.insert(controls, CreateCheckBox(globalSection, "启用全新社交界面", 270, -106, function() return SFramesDB.enableSocial ~= false end, function(checked) SFramesDB.enableSocial = checked end )) CreateDesc(globalSection, "替换好友/查询/工会/团队窗口(需重载UI)", 292, -122, font) table.insert(controls, CreateCheckBox(globalSection, "NPC单选项自动跳过", 270, -142, function() return SFramesDB.autoGossip ~= false end, function(checked) SFramesDB.autoGossip = checked end )) CreateDesc(globalSection, "只有一个对话选项时自动确认(按 Shift 临时禁用)", 292, -158, font) table.insert(controls, CreateCheckBox(globalSection, "启用兽栏皮肤", 270, -178, function() return SFramesDB.enablePetStable ~= false end, function(checked) SFramesDB.enablePetStable = checked end )) CreateDesc(globalSection, "美化宠物兽栏界面(需重载UI)", 292, -194, font) table.insert(controls, CreateCheckBox(globalSection, "启用全新专业技能界面", 14, -214, function() return SFramesDB.enableTradeSkill ~= false end, function(checked) SFramesDB.enableTradeSkill = checked end )) CreateDesc(globalSection, "替换默认专业技能窗口(需重载UI)", 36, -230, font) table.insert(controls, CreateCheckBox(globalSection, "启用全新飞行地图界面", 270, -214, function() return SFramesDB.enableFlightMap ~= false end, function(checked) SFramesDB.enableFlightMap = checked end )) CreateDesc(globalSection, "美化飞行管理员地图,增加目的地列表(需重载UI)", 292, -230, font) table.insert(controls, CreateCheckBox(globalSection, "启用全新邮箱界面", 14, -250, function() return SFramesDB.enableMail ~= false end, function(checked) SFramesDB.enableMail = checked end )) CreateDesc(globalSection, "替换默认邮箱窗口,支持批量收取和多物品发送(需重载UI)", 36, -266, font) table.insert(controls, CreateCheckBox(globalSection, "启用自定义拾取界面", 270, -250, function() if type(SFramesDB.lootDisplay) ~= "table" then return true end return SFramesDB.lootDisplay.enable ~= false end, function(checked) if type(SFramesDB.lootDisplay) ~= "table" then SFramesDB.lootDisplay = {} end SFramesDB.lootDisplay.enable = checked end )) CreateDesc(globalSection, "替换原生拾取窗口,品质染色+拾取提示(需重载UI)", 292, -266, font) -- ── 增强功能(Libs 库集成)────────────────────────────────── local enhSection = CreateSection(root, "增强功能(需安装 !Libs 插件)", 8, -536, 520, 204, font) table.insert(controls, CreateCheckBox(enhSection, "血条平滑动画(需 /reload)", 14, -34, function() return SFramesDB.smoothBars ~= false end, function(checked) SFramesDB.smoothBars = checked end )) CreateDesc(enhSection, "血量/法力变化时丝滑过渡,需要 LibSmoothStatusBar", 36, -50, font, 218) table.insert(controls, CreateCheckBox(enhSection, "目标真实血量", 270, -34, function() return SFramesDB.mobRealHealth ~= false end, function(checked) SFramesDB.mobRealHealth = checked end )) CreateDesc(enhSection, "怪物血条显示实际HP数值,需要 LibMobHealthCache", 292, -50, font, 218) table.insert(controls, CreateCheckBox(enhSection, "物品等级显示", 14, -80, function() return SFramesDB.showItemLevel ~= false end, function(checked) SFramesDB.showItemLevel = checked end, function() if SFrames.CharacterPanel and SFrames.CharacterPanel.UpdateCurrentTab then SFrames.CharacterPanel:UpdateCurrentTab() end end )) CreateDesc(enhSection, "装备栏/背包角标显示 iLvl + 面板平均装等,需要 LibItem-Level", 36, -96, font, 218) table.insert(controls, CreateCheckBox(enhSection, "装备属性对比", 270, -80, function() return SFramesDB.itemCompare ~= false end, function(checked) SFramesDB.itemCompare = checked end )) CreateDesc(enhSection, "背包物品 tooltip 显示与已装备的属性差异,需要 ItemBonusLib", 292, -96, font, 218) table.insert(controls, CreateCheckBox(enhSection, "装备评分 (EP)", 14, -126, function() return SFramesDB.gearScore ~= false end, function(checked) SFramesDB.gearScore = checked end )) CreateDesc(enhSection, "Tooltip 显示当前职业各天赋EP评分及硬核评分,需要 ItemBonusLib", 36, -142, font, 480) table.insert(controls, CreateCheckBox(enhSection, "显示物品/法术ID", 270, -126, function() return SFramesDB.showTooltipIDs ~= false end, function(checked) SFramesDB.showTooltipIDs = checked end )) CreateDesc(enhSection, "在 Tooltip 中显示物品ID和法术ID", 292, -142, font, 218) CreateLabel(enhSection, "提示:以上功能需要安装对应的 !Libs 库才能生效。", 14, -172, font, 10, 0.6, 0.6, 0.65) -- ── ShaguTweaks 功能移植 ────────────────────────────────────── local tweaksSection = CreateSection(root, "ShaguTweaks 功能移植(需 /reload 生效)", 8, -750, 520, 260, font) table.insert(controls, CreateCheckBox(tweaksSection, "自动切换姿态/形态", 14, -34, function() return SFramesDB.Tweaks.autoStance ~= false end, function(checked) SFramesDB.Tweaks.autoStance = checked end )) CreateDesc(tweaksSection, "施法需要特定姿态时自动切换(如人形按熊掌→自动变熊)", 36, -50, font, 218) table.insert(controls, CreateCheckBox(tweaksSection, "乌龟服兼容修改", 270, -34, function() return SFramesDB.Tweaks.turtleCompat ~= false end, function(checked) SFramesDB.Tweaks.turtleCompat = checked end )) CreateDesc(tweaksSection, "隐藏乌龟服自带目标血量文字,修复地图窗口标题偏移", 36, -96, font, 218) table.insert(controls, CreateCheckBox(tweaksSection, "SuperWoW 客户端兼容", 270, -80, function() return SFramesDB.Tweaks.superWoW ~= false end, function(checked) SFramesDB.Tweaks.superWoW = checked end )) CreateDesc(tweaksSection, "为 SuperWoW 提供 GUID 施法数据支持(需安装 SuperWoW)", 292, -96, font, 218) table.insert(controls, CreateCheckBox(tweaksSection, "冷却倒计时", 14, -126, function() return SFramesDB.Tweaks.cooldownNumbers ~= false end, function(checked) SFramesDB.Tweaks.cooldownNumbers = checked end )) CreateDesc(tweaksSection, "在技能/物品冷却图标上显示剩余时间文字(2秒以上)", 36, -142, font, 218) table.insert(controls, CreateCheckBox(tweaksSection, "暗色界面风格", 270, -126, function() return SFramesDB.Tweaks.darkUI == true end, function(checked) SFramesDB.Tweaks.darkUI = checked end )) CreateDesc(tweaksSection, "将整个游戏界面调暗为深色主题(默认关闭)", 292, -142, font, 218) table.insert(controls, CreateCheckBox(tweaksSection, "进战斗后台通知", 14, -172, function() return SFramesDB.Tweaks.combatNotify ~= false end, function(checked) SFramesDB.Tweaks.combatNotify = checked end )) CreateDesc(tweaksSection, "进入战斗时闪烁任务栏图标(需安装 UnitXP SP3)", 36, -188, font, 218) table.insert(controls, CreateCheckBox(tweaksSection, "鼠标指向施法", 14, -218, function() return SFramesDB.Tweaks.mouseoverCast == true end, function(checked) SFramesDB.Tweaks.mouseoverCast = checked end )) CreateDesc(tweaksSection, "鼠标悬停单位框体时施法直接作用于该单位,不切换目标(需 SuperWoW)", 36, -234, font, 480) CreateLabel(tweaksSection, "提示:以上所有选项修改后需要 /reload 才能生效。", 14, -264, font, 10, 0.6, 0.6, 0.65) -- ── 聊天背景透明度 ────────────────────────────────────── local chatBgSection = CreateSection(root, "聊天窗口", 8, -1018, 520, 120, font) table.insert(controls, CreateSlider(chatBgSection, "背景透明度", 14, -38, 220, 0, 1.0, 0.05, function() local chatDB = SFramesDB and SFramesDB.Chat return (chatDB and type(chatDB.bgAlpha) == "number") and chatDB.bgAlpha or 0.45 end, function(value) if not SFramesDB.Chat then SFramesDB.Chat = {} end SFramesDB.Chat.bgAlpha = value end, function(v) return string.format("%.0f%%", v * 100) end, function() if SFrames.Chat and SFrames.Chat.ApplyConfig then SFrames.Chat:ApplyConfig() end end )) table.insert(controls, CreateCheckBox(chatBgSection, "悬停显示背景", 260, -36, function() local chatDB = SFramesDB and SFramesDB.Chat return not chatDB or chatDB.hoverTransparent ~= false end, function(checked) if not SFramesDB.Chat then SFramesDB.Chat = {} end SFramesDB.Chat.hoverTransparent = (checked == true) if SFrames.Chat and SFrames.Chat.ApplyConfig then SFrames.Chat:ApplyConfig() end end )) CreateDesc(chatBgSection, "调整聊天窗口背景的透明度(仅影响背景,不影响文字)。勾选[悬停显示背景]后,鼠标离开时背景自动透明。", 14, -68, font, 480) uiScroll:UpdateRange() self.uiControls = controls self.uiScroll = uiScroll end local function AddFrameAdvancedSections(root, controls, spec) local font = SFrames:GetFont() local y = spec.startY local apply = spec.apply local prefix = spec.prefix local showPowerHint = spec.showPowerHint local powerSectionHeight = showPowerHint and 154 or 132 local powerSection = CreateSection(root, spec.powerTitle or "能量条", 8, y, 520, powerSectionHeight, font) table.insert(controls, CreateSlider(powerSection, "宽度", 14, -42, 150, 40, 420, 1, function() return SFramesDB[prefix .. "PowerWidth"] end, function(value) SFramesDB[prefix .. "PowerWidth"] = value end, function(v) return tostring(math.floor(v + 0.5)) end, apply )) table.insert(controls, CreateSlider(powerSection, "高度", 170, -42, 150, 4, 40, 1, function() return SFramesDB[prefix .. "PowerHeight"] end, function(value) SFramesDB[prefix .. "PowerHeight"] = value end, function(v) return tostring(math.floor(v + 0.5)) end, apply )) table.insert(controls, CreateSlider(powerSection, "X 偏移", 326, -42, 130, -120, 120, 1, function() return SFramesDB[prefix .. "PowerOffsetX"] end, function(value) SFramesDB[prefix .. "PowerOffsetX"] = value end, function(v) return tostring(math.floor(v + 0.5)) end, apply )) table.insert(controls, CreateSlider(powerSection, "Y 偏移", 14, -94, 150, -80, 80, 1, function() return SFramesDB[prefix .. "PowerOffsetY"] end, function(value) SFramesDB[prefix .. "PowerOffsetY"] = value end, function(v) return tostring(math.floor(v + 0.5)) end, apply )) table.insert(controls, CreateCheckBox(powerSection, "能量条在上层", 170, -92, function() return SFramesDB[prefix .. "PowerOnTop"] == true end, function(checked) SFramesDB[prefix .. "PowerOnTop"] = checked end, apply )) if showPowerHint then CreateDesc(powerSection, showPowerHint, 14, -118, font, 470) end y = y - (powerSectionHeight + 12) local textureSection = CreateSection(root, "条材质", 8, y, 520, 182, font) local texControls, texHeight = CreateOptionGrid(textureSection, "血条材质", 14, -34, 490, SFrames.BarTextures or {}, function() return SFramesDB[prefix .. "HealthTexture"] or "" end, function(value) SFramesDB[prefix .. "HealthTexture"] = value end, "bar", 0, apply) for _, control in ipairs(texControls) do table.insert(controls, control) end local texControls2 = CreateOptionGrid(textureSection, "能量条材质", 14, -(34 + texHeight + 12), 490, SFrames.BarTextures or {}, function() return SFramesDB[prefix .. "PowerTexture"] or "" end, function(value) SFramesDB[prefix .. "PowerTexture"] = value end, "bar", 0, apply) for _, control in ipairs(texControls2) do table.insert(controls, control) end y = y - 194 local fontSection = CreateSection(root, "字体", 8, y, 520, spec.fontSectionHeight or 286, font) local blockY = -34 local function AddFontBlock(title, keyName, sizeKey) local optControls, optHeight = CreateOptionGrid(fontSection, title, 14, blockY, 490, SFrames.FontChoices or {}, function() return SFramesDB[keyName] or "" end, function(value) SFramesDB[keyName] = value end, "font", 11, apply) for _, control in ipairs(optControls) do table.insert(controls, control) end table.insert(controls, CreateSlider(fontSection, title .. "字号", 14, blockY - optHeight - 26, 180, 8, 24, 1, function() return SFramesDB[sizeKey] end, function(value) SFramesDB[sizeKey] = value end, function(v) return tostring(math.floor(v + 0.5)) end, apply )) blockY = blockY - optHeight - 74 end AddFontBlock("姓名字体", prefix .. "NameFontKey", prefix .. "NameFontSize") AddFontBlock("生命值字体", prefix .. "HealthFontKey", prefix .. "HealthFontSize") AddFontBlock("能量值字体", prefix .. "PowerFontKey", prefix .. "PowerFontSize") if spec.extraFontBlocks then for _, extra in ipairs(spec.extraFontBlocks) do AddFontBlock(extra.title, extra.keyName, extra.sizeKey) end end return y - (spec.fontSectionHeight or 286) - 12 end local FRAME_DEFAULT_BUILDERS = { player = function() local portraitHeight = (SFrames.Config and SFrames.Config.height or 45) - 2 return { enablePlayerFrame = true, playerFrameScale = 1, playerFrameWidth = 220, playerPortraitWidth = 50, playerPortraitHeight = portraitHeight, playerPortraitOffsetX = 0, playerPortraitOffsetY = 0, playerPortraitMode = "head", playerHealthHeight = 38, playerPowerHeight = 9, playerPowerWidth = 166, playerPowerOffsetX = 0, playerPowerOffsetY = 0, playerPowerOnTop = false, playerShowClass = false, playerShowClassIcon = true, playerShowPortrait = true, playerFrameAlpha = 1, playerBgAlpha = 0.9, playerPortraitBgAlpha = 0.9, playerShowBorder = false, playerCornerRadius = 0, playerNameFontSize = 10, playerValueFontSize = 10, playerHealthFontSize = 10, playerPowerFontSize = 10, playerHealthTexture = "", playerPowerTexture = "", playerNameFontKey = "", playerHealthFontKey = "", playerPowerFontKey = "", castbarStandalone = true, castbarRainbow = true, castbarWidth = 280, castbarHeight = 20, castbarAlpha = 1, powerRainbow = false, showPetFrame = true, petFrameScale = 1, playerLeaderIconOffsetX = 0, playerLeaderIconOffsetY = 0, playerRaidIconOffsetX = 0, playerRaidIconOffsetY = 0, petHealthTexture = "", petPowerTexture = "", } end, target = function() local portraitHeight = (SFrames.Config and SFrames.Config.height or 45) - 2 return { enableTargetFrame = true, targetFrameScale = 1, targetFrameWidth = 220, targetPortraitWidth = 50, targetPortraitHeight = portraitHeight, targetPortraitOffsetX = 0, targetPortraitOffsetY = 0, targetPortraitMode = "head", targetHealthHeight = 38, targetPowerHeight = 9, targetPowerWidth = 168, targetPowerOffsetX = 0, targetPowerOffsetY = 0, targetPowerOnTop = false, targetShowClass = false, targetShowClassIcon = true, targetShowPortrait = true, targetFrameAlpha = 1, targetBgAlpha = 0.9, targetPortraitBgAlpha = 0.9, targetShowBorder = false, targetCornerRadius = 0, targetNameFontSize = 10, targetValueFontSize = 10, targetHealthFontSize = 10, targetPowerFontSize = 10, targetHealthTexture = "", targetPowerTexture = "", targetNameFontKey = "", targetHealthFontKey = "", targetPowerFontKey = "", targetDistanceEnabled = true, targetDistanceOnFrame = true, targetDistanceScale = 1, targetDistanceFontSize = 14, targetDistanceFontKey = "", totHealthTexture = "", } end, focus = function() return { focusEnabled = true, focusFrameScale = 0.9, focusFrameWidth = 200, focusPortraitWidth = 45, focusPortraitHeight = 43, focusPortraitOffsetX = 0, focusPortraitOffsetY = 0, focusPortraitMode = "head", focusHealthHeight = 32, focusPowerHeight = 10, focusPowerWidth = 153, focusPowerOffsetX = 0, focusPowerOffsetY = 0, focusPowerOnTop = false, focusShowPortrait = true, focusShowCastBar = true, focusShowAuras = true, focusBgAlpha = 0.9, focusPortraitBgAlpha = 0.9, focusShowBorder = false, focusCornerRadius = 0, focusNameFontSize = 11, focusValueFontSize = 10, focusHealthFontSize = 10, focusPowerFontSize = 10, focusCastFontSize = 10, focusDistanceFontSize = 14, focusHealthTexture = "", focusPowerTexture = "", focusNameFontKey = "", focusHealthFontKey = "", focusPowerFontKey = "", focusCastFontKey = "", focusDistanceFontKey = "", } end, party = function() return { enablePartyFrame = true, partyLayout = "vertical", partyFrameScale = 1, partyFrameWidth = 150, partyFrameHeight = 35, partyPortraitWidth = 33, partyPortraitHeight = 33, partyPortraitOffsetX = 0, partyPortraitOffsetY = 0, partyPortraitMode = "head", partyHealthHeight = 22, partyPowerHeight = 10, partyPowerWidth = 112, partyPowerOffsetX = 0, partyPowerOffsetY = 0, partyPowerOnTop = false, partyHorizontalGap = 8, partyVerticalGap = 30, partyBgAlpha = 0.9, partyPortraitBgAlpha = 0.9, partyShowBorder = false, partyCornerRadius = 0, partyNameFontSize = 10, partyValueFontSize = 10, partyHealthFontSize = 10, partyPowerFontSize = 10, partyHealthTexture = "", partyPowerTexture = "", partyNameFontKey = "", partyHealthFontKey = "", partyPowerFontKey = "", partyShowBuffs = true, partyShowDebuffs = true, partyPortrait3D = true, } end, raid = function() return { enableRaidFrames = true, raidLayout = "horizontal", raidFrameScale = 1, raidFrameWidth = 60, raidFrameHeight = 40, raidHealthHeight = 31, raidPowerHeight = 6, raidPowerWidth = 58, raidPowerOffsetX = 0, raidPowerOffsetY = 0, raidPowerOnTop = false, raidHorizontalGap = 2, raidVerticalGap = 2, raidGroupGap = 8, raidBgAlpha = 0.9, raidShowBorder = false, raidCornerRadius = 0, raidNameFontSize = 10, raidValueFontSize = 9, raidHealthFontSize = 9, raidPowerFontSize = 9, raidHealthTexture = "", raidPowerTexture = "", raidNameFontKey = "", raidHealthFontKey = "", raidPowerFontKey = "", raidShowPower = true, raidHealthFormat = "compact", raidShowGroupLabel = true, } end, } local activePreviewDropdown local function FindOptionEntry(options, key) if type(options) ~= "table" then return nil end for _, entry in ipairs(options) do if entry.key == key then return entry end end return options[1] end local function ClosePreviewDropdown() if activePreviewDropdown and activePreviewDropdown.Hide then activePreviewDropdown:Hide() end activePreviewDropdown = nil end local function CreatePreviewDropdown(parent, label, x, y, width, options, getter, setter, previewKind, previewSize, onValueChanged) local font = SFrames:GetFont() CreateLabel(parent, label, x, y, font, 10, 0.85, 0.75, 0.80) local button = CreateFrame("Button", NextWidgetName("PreviewDropdown"), parent) button:SetWidth(width) button:SetHeight(24) button:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y - 16) button:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } }) button:SetBackdropColor(0.08, 0.08, 0.10, 0.92) button:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) local preview = button:CreateFontString(nil, "OVERLAY") preview:SetPoint("LEFT", button, "LEFT", 8, 0) preview:SetPoint("RIGHT", button, "RIGHT", -24, 0) preview:SetJustifyH("LEFT") preview:SetTextColor(0.92, 0.92, 0.92) local arrow = button:CreateFontString(nil, "OVERLAY") arrow:SetFont(font, 10, "OUTLINE") arrow:SetPoint("RIGHT", button, "RIGHT", -8, 0) arrow:SetText("▼") arrow:SetTextColor(0.8, 0.75, 0.8) local menu = CreateFrame("Frame", NextWidgetName("PreviewDropdownMenu"), UIParent) menu:SetWidth(width) menu:SetHeight((table.getn(options or {}) * 22) + 8) menu:SetFrameStrata("DIALOG") menu:SetClampedToScreen(true) EnsureSoftBackdrop(menu) menu:SetBackdropColor(0.05, 0.05, 0.07, 0.96) menu:SetBackdropBorderColor(0.45, 0.38, 0.46, 1) menu:Hide() button.menu = menu for index, entry in ipairs(options or {}) do local item = CreateFrame("Button", NextWidgetName("PreviewDropdownItem"), menu) item:SetWidth(width - 10) item:SetHeight(20) item:SetPoint("TOPLEFT", menu, "TOPLEFT", 5, -5 - ((index - 1) * 22)) item.optionKey = entry.key local itemText = item:CreateFontString(nil, "OVERLAY") itemText:SetPoint("LEFT", item, "LEFT", 6, 0) itemText:SetPoint("RIGHT", item, "RIGHT", -6, 0) itemText:SetJustifyH("LEFT") itemText:SetText(entry.label) if previewKind == "font" then local ok = pcall(itemText.SetFont, itemText, entry.path, previewSize or 11, "OUTLINE") if not ok then itemText:SetFont(font, previewSize or 11, "OUTLINE") end else itemText:SetFont(font, 10, "OUTLINE") end itemText:SetTextColor(0.9, 0.9, 0.9) item.text = itemText item:SetScript("OnEnter", function() this.text:SetTextColor(1, 0.88, 0.55) end) item:SetScript("OnLeave", function() this.text:SetTextColor(0.9, 0.9, 0.9) end) item:SetScript("OnClick", function() setter(this.optionKey) ClosePreviewDropdown() if button.Refresh then button:Refresh() end if onValueChanged then onValueChanged(this.optionKey) end end) end button:SetScript("OnHide", function() if this.menu then this.menu:Hide() end end) button:SetScript("OnClick", function() if this.menu:IsShown() then ClosePreviewDropdown() return end ClosePreviewDropdown() this.menu:ClearAllPoints() this.menu:SetPoint("TOPLEFT", this, "BOTTOMLEFT", 0, -2) this.menu:Show() activePreviewDropdown = this.menu end) button.Refresh = function() local entry = FindOptionEntry(options, getter() or "") if not entry then return end preview:SetText(entry.label) if previewKind == "font" then local ok = pcall(preview.SetFont, preview, entry.path, previewSize or 11, "OUTLINE") if not ok then preview:SetFont(font, previewSize or 11, "OUTLINE") end else preview:SetFont(font, 10, "OUTLINE") end end button:Refresh() return button end local function ResetFrameDefaults(kind) local builder = FRAME_DEFAULT_BUILDERS[kind] if not builder then return end for key, value in pairs(builder()) do SFramesDB[key] = value end end local function CreateResetButton(parent, x, y, frameKind, refreshFn, controls) return CreateButton(parent, "恢复默认", x, y, 100, 22, function() ClosePreviewDropdown() ResetFrameDefaults(frameKind) EnsureDB() if controls then SFrames.ConfigUI:RefreshControls(controls) end if refreshFn then refreshFn() end end) end local function AddFontControl(parent, controls, label, keyName, sizeKey, x, y, apply, sizeMin, sizeMax) table.insert(controls, CreatePreviewDropdown(parent, label .. "字体", x, y, 220, SFrames.FontChoices or {}, function() return SFramesDB[keyName] or "" end, function(value) SFramesDB[keyName] = value end, "font", 11, apply)) table.insert(controls, CreateSlider(parent, label .. "字号", x + 244, y - 4, 180, sizeMin or 8, sizeMax or 24, 1, function() return SFramesDB[sizeKey] end, function(value) SFramesDB[sizeKey] = value end, function(v) return tostring(math.floor(v + 0.5)) end, apply)) end local function AddFontControl(parent, controls, label, keyName, sizeKey, x, y, apply, sizeMin, sizeMax) CreateLabel(parent, label .. "字体", x, y, SFrames:GetFont(), 10, 0.85, 0.75, 0.80) local choices = SFrames.FontChoices or {} local previewBtn = CreateFrame("Button", NextWidgetName("FontPickerSafe"), parent) previewBtn:SetWidth(220) previewBtn:SetHeight(24) previewBtn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y - 16) previewBtn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) previewBtn:SetBackdropColor(0.08, 0.08, 0.10, 0.92) previewBtn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) local previewText = previewBtn:CreateFontString(nil, "OVERLAY") previewText:SetPoint("LEFT", previewBtn, "LEFT", 8, 0) previewText:SetPoint("RIGHT", previewBtn, "RIGHT", -8, 0) previewText:SetJustifyH("LEFT") previewText:SetTextColor(0.92, 0.92, 0.92) local function FindFontIndex() local current = SFramesDB[keyName] or "" for idx, entry in ipairs(choices) do if entry.key == current then return idx end end return 1 end local function ApplyFontIndex(index) local entry = choices[index] or choices[1] if not entry then return end SFramesDB[keyName] = entry.key previewText:SetText(entry.label) local ok = pcall(previewText.SetFont, previewText, entry.path, 11, "OUTLINE") if not ok then previewText:SetFont(SFrames:GetFont(), 11, "OUTLINE") end if apply then apply() end end previewBtn.Refresh = function() ApplyFontIndex(FindFontIndex()) end previewBtn:SetScript("OnClick", function() local idx = FindFontIndex() + 1 if idx > table.getn(choices) then idx = 1 end ApplyFontIndex(idx) end) previewBtn:Refresh() table.insert(controls, previewBtn) table.insert(controls, CreateButton(parent, "<", x + 224, y - 16, 24, 24, function() local idx = FindFontIndex() - 1 if idx < 1 then idx = table.getn(choices) end ApplyFontIndex(idx) end)) table.insert(controls, CreateButton(parent, ">", x + 252, y - 16, 24, 24, function() local idx = FindFontIndex() + 1 if idx > table.getn(choices) then idx = 1 end ApplyFontIndex(idx) end)) table.insert(controls, CreateSlider(parent, label .. "字号", x + 284, y - 4, 140, sizeMin or 8, sizeMax or 24, 1, function() return SFramesDB[sizeKey] end, function(value) SFramesDB[sizeKey] = value end, function(v) return tostring(math.floor(v + 0.5)) end, apply)) end function SFrames.ConfigUI:BuildPlayerPage() local font = SFrames:GetFont() local page = self.playerPage local controls = {} local playerScroll = CreateScrollArea(page, 4, -4, 552, 420, 1960) local root = playerScroll.child local function RefreshPlayer() if SFrames.Player and SFrames.Player.ApplyConfig then SFrames.Player:ApplyConfig() return end if SFrames.Player and SFrames.Player.UpdateAll then SFrames.Player:UpdateAll() end end local playerSection = CreateSection(root, "玩家框体", 8, -8, 520, 402, font) table.insert(controls, CreateCheckBox(playerSection, "启用 Nanami 玩家框体(需 /reload)", 12, -28, function() return SFramesDB.enablePlayerFrame ~= false end, function(checked) SFramesDB.enablePlayerFrame = checked end )) table.insert(controls, CreateResetButton(playerSection, 356, -24, "player", RefreshPlayer, controls)) table.insert(controls, CreateSlider(playerSection, "缩放", 14, -72, 150, 0.7, 1.8, 0.05, function() return SFramesDB.playerFrameScale end, function(value) SFramesDB.playerFrameScale = value end, function(v) return string.format("%.2f", v) end, function() RefreshPlayer() end )) table.insert(controls, CreateSlider(playerSection, "框体宽度", 170, -72, 150, 170, 420, 1, function() return SFramesDB.playerFrameWidth end, function(value) SFramesDB.playerFrameWidth = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshPlayer() end )) table.insert(controls, CreateSlider(playerSection, "头像宽度", 326, -72, 130, 32, 95, 1, function() return SFramesDB.playerPortraitWidth end, function(value) SFramesDB.playerPortraitWidth = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshPlayer() end )) table.insert(controls, CreateSlider(playerSection, "生命条高度", 14, -134, 150, 14, 80, 1, function() return SFramesDB.playerHealthHeight end, function(value) SFramesDB.playerHealthHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshPlayer() end )) table.insert(controls, CreateSlider(playerSection, "能量条高度", 170, -134, 150, 6, 40, 1, function() return SFramesDB.playerPowerHeight end, function(value) SFramesDB.playerPowerHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshPlayer() end )) table.insert(controls, CreateCheckBox(playerSection, "玩家姓名显示职业", 326, -132, function() return SFramesDB.playerShowClass ~= false end, function(checked) SFramesDB.playerShowClass = checked end, function() RefreshPlayer() end )) table.insert(controls, CreateCheckBox(playerSection, "玩家显示职业图标", 326, -158, function() return SFramesDB.playerShowClassIcon ~= false end, function(checked) SFramesDB.playerShowClassIcon = checked end, function() RefreshPlayer() end )) table.insert(controls, CreateCheckBox(playerSection, "启用3D头像", 12, -188, function() return SFramesDB.playerShowPortrait ~= false end, function(checked) SFramesDB.playerShowPortrait = checked end, function() RefreshPlayer() end )) table.insert(controls, CreateSlider(playerSection, "姓名字号", 14, -226, 150, 8, 18, 1, function() return SFramesDB.playerNameFontSize end, function(value) SFramesDB.playerNameFontSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshPlayer() end )) table.insert(controls, CreateSlider(playerSection, "数值字号", 170, -226, 150, 8, 18, 1, function() return SFramesDB.playerValueFontSize end, function(value) SFramesDB.playerValueFontSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshPlayer() end )) table.insert(controls, CreateSlider(playerSection, "透明度", 326, -226, 130, 0.1, 1.0, 0.05, function() return SFramesDB.playerFrameAlpha end, function(value) SFramesDB.playerFrameAlpha = value end, function(v) return string.format("%.0f%%", v * 100) end, function(value) if SFrames.Player and SFrames.Player.frame then SFrames.Player.frame:SetAlpha(value) end end )) table.insert(controls, CreateSlider(playerSection, "背景透明度", 14, -288, 150, 0, 1.0, 0.05, function() return SFramesDB.playerBgAlpha end, function(value) SFramesDB.playerBgAlpha = value end, function(v) return string.format("%.0f%%", v * 100) end, function(value) if SFrames.Player and SFrames.Player.frame then local f = SFrames.Player.frame ApplyBgAlphaToFrame(f, value) if f.healthBGFrame then ApplyBgAlphaToFrame(f.healthBGFrame, value) end if f.powerBGFrame then ApplyBgAlphaToFrame(f.powerBGFrame, value) end if f.portraitBG then ApplyBgAlphaToFrame(f.portraitBG, value) end end end )) CreateLabel(playerSection, "提示:关闭头像后职业图标移到框体最左侧,框体仅显示血条。", 14, -355, font, 10, 0.9, 0.9, 0.9) -- ── 施法条 ────────────────────────────────────────────────── local cbSection = CreateSection(root, "施法条", 8, -418, 520, 250, font) table.insert(controls, CreateCheckBox(cbSection, "独立施法条(显示在屏幕下方)", 14, -34, function() return SFramesDB.castbarStandalone == true end, function(checked) SFramesDB.castbarStandalone = checked end, function() if SFrames.Player and SFrames.Player.ApplyCastbarPosition then SFrames.Player:ApplyCastbarPosition() end end )) CreateDesc(cbSection, "关闭时施法条依附玩家框体上方,开启后独立显示在屏幕下方中央", 36, -50, font) table.insert(controls, CreateCheckBox(cbSection, "启用彩虹色施法条", 14, -74, function() return SFramesDB.castbarRainbow == true end, function(checked) SFramesDB.castbarRainbow = checked end )) CreateDesc(cbSection, "施法时条颜色会随时间流转变化", 36, -90, font) table.insert(controls, CreateSlider(cbSection, "独立施法条宽度", 14, -120, 200, 100, 500, 5, function() return SFramesDB.castbarWidth or 280 end, function(value) SFramesDB.castbarWidth = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() if SFrames.Player and SFrames.Player.ApplyCastbarPosition then SFrames.Player:ApplyCastbarPosition() end end )) table.insert(controls, CreateSlider(cbSection, "独立施法条高度", 270, -120, 200, 10, 60, 1, function() return SFramesDB.castbarHeight or 20 end, function(value) SFramesDB.castbarHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() if SFrames.Player and SFrames.Player.ApplyCastbarPosition then SFrames.Player:ApplyCastbarPosition() end end )) table.insert(controls, CreateSlider(cbSection, "施法条透明度", 14, -160, 200, 0.1, 1.0, 0.05, function() return SFramesDB.castbarAlpha or 1 end, function(value) SFramesDB.castbarAlpha = value end, function(v) return string.format("%.0f%%", v * 100) end, function(value) local cb = SFrames.Player and SFrames.Player.frame and SFrames.Player.frame.castbar if cb and cb:IsShown() then cb:SetAlpha(value) if cb.cbbg then cb.cbbg:SetAlpha(value) end end end )) -- ── 宠物框体 ────────────────────────────────────────────────── local petSection = CreateSection(root, "宠物框体", 8, -676, 520, 98, font) table.insert(controls, CreateCheckBox(petSection, "显示宠物框体", 14, -34, function() return SFramesDB.showPetFrame ~= false end, function(checked) SFramesDB.showPetFrame = checked end, function() if SFrames.Pet and SFrames.Pet.UpdateAll then SFrames.Pet:UpdateAll() end end )) CreateDesc(petSection, "在玩家框体附近显示当前宠物的血量条", 36, -50, font) table.insert(controls, CreateSlider(petSection, "缩放", 270, -52, 200, 0.7, 1.8, 0.05, function() return SFramesDB.petFrameScale or 1 end, function(value) SFramesDB.petFrameScale = value end, function(v) return string.format("%.2f", v) end, function(value) if SFrames.Pet and SFrames.Pet.frame then SFrames.Pet.frame:SetScale(value) end end )) -- ── 图标位置 ────────────────────────────────────────────────── local iconSection = CreateSection(root, "图标位置", 8, -754, 520, 142, font) table.insert(controls, CreateSlider(iconSection, "队长图标 X", 14, -44, 150, -80, 80, 1, function() return SFramesDB.playerLeaderIconOffsetX or 0 end, function(value) SFramesDB.playerLeaderIconOffsetX = value end, function(v) return tostring(math.floor(v + 0.5)) end, RefreshPlayer )) table.insert(controls, CreateSlider(iconSection, "队长图标 Y", 170, -44, 150, -80, 80, 1, function() return SFramesDB.playerLeaderIconOffsetY or 0 end, function(value) SFramesDB.playerLeaderIconOffsetY = value end, function(v) return tostring(math.floor(v + 0.5)) end, RefreshPlayer )) table.insert(controls, CreateSlider(iconSection, "战斗标记 X", 14, -104, 150, -120, 120, 1, function() return SFramesDB.playerRaidIconOffsetX or 0 end, function(value) SFramesDB.playerRaidIconOffsetX = value end, function(v) return tostring(math.floor(v + 0.5)) end, RefreshPlayer )) table.insert(controls, CreateSlider(iconSection, "战斗标记 Y", 170, -104, 150, -80, 80, 1, function() return SFramesDB.playerRaidIconOffsetY or 0 end, function(value) SFramesDB.playerRaidIconOffsetY = value end, function(v) return tostring(math.floor(v + 0.5)) end, RefreshPlayer )) AddFrameAdvancedSections(root, controls, { prefix = "player", startY = -944, apply = RefreshPlayer, }) local function RefreshPet() if SFrames.Pet and SFrames.Pet.ApplyConfig then SFrames.Pet:ApplyConfig() end if SFrames.Pet and SFrames.Pet.UpdateAll then SFrames.Pet:UpdateAll() end end local petTexSection = CreateSection(root, "宠物框体 条材质", 8, -1800, 520, 182, font) local petTexControls, petTexH = CreateOptionGrid(petTexSection, "血条材质", 14, -34, 490, SFrames.BarTextures or {}, function() return SFramesDB.petHealthTexture or "" end, function(value) SFramesDB.petHealthTexture = value end, "bar", 0, RefreshPet) for _, c in ipairs(petTexControls) do table.insert(controls, c) end local petTexControls2 = CreateOptionGrid(petTexSection, "能量条材质", 14, -(34 + petTexH + 12), 490, SFrames.BarTextures or {}, function() return SFramesDB.petPowerTexture or "" end, function(value) SFramesDB.petPowerTexture = value end, "bar", 0, RefreshPet) for _, c in ipairs(petTexControls2) do table.insert(controls, c) end playerScroll:UpdateRange() self.playerControls = controls self.playerScroll = playerScroll end function SFrames.ConfigUI:BuildTargetPage() local font = SFrames:GetFont() local page = self.targetPage local controls = {} local function RefreshTarget() if SFrames.Target and SFrames.Target.ApplyConfig then SFrames.Target:ApplyConfig() return end if SFrames.Target and SFrames.Target.UpdateAll then SFrames.Target:UpdateAll() end end local targetScroll = CreateScrollArea(page, 4, -4, 548, 420, 1780) local targetRoot = targetScroll.child local targetSection = CreateSection(targetRoot, "目标框体", 8, -8, 520, 460, font) table.insert(controls, CreateCheckBox(targetSection, "启用 Nanami 目标框体(需 /reload)", 12, -28, function() return SFramesDB.enableTargetFrame ~= false end, function(checked) SFramesDB.enableTargetFrame = checked end )) table.insert(controls, CreateResetButton(targetSection, 356, -24, "target", RefreshTarget, controls)) table.insert(controls, CreateSlider(targetSection, "缩放", 14, -72, 150, 0.7, 1.8, 0.05, function() return SFramesDB.targetFrameScale end, function(value) SFramesDB.targetFrameScale = value end, function(v) return string.format("%.2f", v) end, function() RefreshTarget() end )) table.insert(controls, CreateSlider(targetSection, "框体宽度", 170, -72, 150, 170, 420, 1, function() return SFramesDB.targetFrameWidth end, function(value) SFramesDB.targetFrameWidth = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshTarget() end )) table.insert(controls, CreateSlider(targetSection, "头像宽度", 326, -72, 130, 32, 95, 1, function() return SFramesDB.targetPortraitWidth end, function(value) SFramesDB.targetPortraitWidth = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshTarget() end )) table.insert(controls, CreateSlider(targetSection, "生命条高度", 14, -134, 150, 14, 80, 1, function() return SFramesDB.targetHealthHeight end, function(value) SFramesDB.targetHealthHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshTarget() end )) table.insert(controls, CreateSlider(targetSection, "能量条高度", 170, -134, 150, 6, 40, 1, function() return SFramesDB.targetPowerHeight end, function(value) SFramesDB.targetPowerHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshTarget() end )) table.insert(controls, CreateCheckBox(targetSection, "目标姓名显示职业", 326, -132, function() return SFramesDB.targetShowClass ~= false end, function(checked) SFramesDB.targetShowClass = checked end, function() RefreshTarget() end )) table.insert(controls, CreateCheckBox(targetSection, "目标显示职业图标", 326, -158, function() return SFramesDB.targetShowClassIcon ~= false end, function(checked) SFramesDB.targetShowClassIcon = checked end, function() RefreshTarget() end )) table.insert(controls, CreateCheckBox(targetSection, "启用3D头像", 12, -188, function() return SFramesDB.targetShowPortrait ~= false end, function(checked) SFramesDB.targetShowPortrait = checked end, function() RefreshTarget() end )) table.insert(controls, CreateSlider(targetSection, "姓名字号", 14, -226, 150, 8, 18, 1, function() return SFramesDB.targetNameFontSize end, function(value) SFramesDB.targetNameFontSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshTarget() end )) table.insert(controls, CreateSlider(targetSection, "数值字号", 170, -226, 150, 8, 18, 1, function() return SFramesDB.targetValueFontSize end, function(value) SFramesDB.targetValueFontSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshTarget() end )) table.insert(controls, CreateSlider(targetSection, "透明度", 326, -226, 130, 0.1, 1.0, 0.05, function() return SFramesDB.targetFrameAlpha end, function(value) SFramesDB.targetFrameAlpha = value end, function(v) return string.format("%.0f%%", v * 100) end, function(value) if SFrames.Target and SFrames.Target.frame then SFrames.Target.frame:SetAlpha(value) end end )) table.insert(controls, CreateSlider(targetSection, "背景透明度", 170, -288, 150, 0, 1.0, 0.05, function() return SFramesDB.targetBgAlpha end, function(value) SFramesDB.targetBgAlpha = value end, function(v) return string.format("%.0f%%", v * 100) end, function(value) if SFrames.Target and SFrames.Target.frame then local f = SFrames.Target.frame ApplyBgAlphaToFrame(f, value) if f.healthBGFrame then ApplyBgAlphaToFrame(f.healthBGFrame, value) end if f.powerBGFrame then ApplyBgAlphaToFrame(f.powerBGFrame, value) end if f.portraitBG then ApplyBgAlphaToFrame(f.portraitBG, value) end end end )) table.insert(controls, CreateCheckBox(targetSection, "启用目标距离文本", 326, -326, function() return SFramesDB.targetDistanceEnabled ~= false end, function(checked) SFramesDB.targetDistanceEnabled = checked end, function(checked) if SFrames.Target and SFrames.Target.distanceFrame then if checked and UnitExists("target") then SFrames.Target.distanceFrame:Show() elseif not checked then SFrames.Target.distanceFrame:Hide() end end if SFrames.Target and SFrames.Target.frame and SFrames.Target.frame.distText then if not checked then SFrames.Target.frame.distText:Hide() end end end )) table.insert(controls, CreateCheckBox(targetSection, "距离显示在框体上", 326, -354, function() return SFramesDB.targetDistanceOnFrame ~= false end, function(checked) SFramesDB.targetDistanceOnFrame = checked end, function() if SFrames.Target then SFrames.Target:OnTargetChanged() end end )) table.insert(controls, CreateSlider(targetSection, "距离文本缩放(独立框体)", 14, -406, 150, 0.7, 1.8, 0.05, function() return SFramesDB.targetDistanceScale end, function(value) SFramesDB.targetDistanceScale = value end, function(v) return string.format("%.2f", v) end, function(value) if SFrames.Target and SFrames.Target.ApplyDistanceScale then SFrames.Target:ApplyDistanceScale(value) end end )) table.insert(controls, CreateSlider(targetSection, "距离文字大小", 200, -406, 150, 8, 24, 1, function() return SFramesDB.targetDistanceFontSize or 14 end, function(value) SFramesDB.targetDistanceFontSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function(value) if SFrames.Target and SFrames.Target.ApplyDistanceScale then SFrames.Target:ApplyDistanceScale(SFramesDB.targetDistanceScale or 1) end if SFrames.Target and SFrames.Target.ApplyConfig then SFrames.Target:ApplyConfig() end end )) AddFrameAdvancedSections(targetRoot, controls, { prefix = "target", startY = -516, apply = RefreshTarget, extraFontBlocks = { { title = "距离文字字体", keyName = "targetDistanceFontKey", sizeKey = "targetDistanceFontSize" }, }, fontSectionHeight = 372, }) local function RefreshToT() if SFrames.ToT and SFrames.ToT.ApplyConfig then SFrames.ToT:ApplyConfig() end end local totTexSection = CreateSection(targetRoot, "目标的目标 条材质", 8, -1560, 520, 100, font) local totTexControls, totTexH = CreateOptionGrid(totTexSection, "血条材质", 14, -34, 490, SFrames.BarTextures or {}, function() return SFramesDB.totHealthTexture or "" end, function(value) SFramesDB.totHealthTexture = value end, "bar", 0, RefreshToT) for _, c in ipairs(totTexControls) do table.insert(controls, c) end targetScroll:UpdateRange() self.targetControls = controls self.targetScroll = targetScroll end function SFrames.ConfigUI:BuildFocusPage() local font = SFrames:GetFont() local page = self.focusPage local controls = {} local focusScroll = CreateScrollArea(page, 4, -4, 548, 420, 1860) local root = focusScroll.child local focusSection = CreateSection(root, "焦点框体", 8, -8, 520, 390, font) local function FocusApply() if SFrames.Focus and SFrames.Focus.ApplySettings then SFrames.Focus:ApplySettings() end end table.insert(controls, CreateCheckBox(focusSection, "启用焦点框体(需 /reload)", 12, -28, function() return SFramesDB.focusEnabled ~= false end, function(checked) SFramesDB.focusEnabled = checked end )) table.insert(controls, CreateResetButton(focusSection, 356, -24, "focus", FocusApply, controls)) table.insert(controls, CreateSlider(focusSection, "缩放", 14, -72, 150, 0.5, 1.8, 0.05, function() return SFramesDB.focusFrameScale end, function(v) SFramesDB.focusFrameScale = v; FocusApply() end, function(v) return string.format("%.0f%%", v * 100) end )) table.insert(controls, CreateSlider(focusSection, "宽度", 280, -72, 150, 140, 320, 5, function() return SFramesDB.focusFrameWidth end, function(v) SFramesDB.focusFrameWidth = v; FocusApply() end, function(v) return string.format("%.0f", v) end )) table.insert(controls, CreateSlider(focusSection, "血条高度", 14, -126, 150, 16, 60, 1, function() return SFramesDB.focusHealthHeight end, function(v) SFramesDB.focusHealthHeight = v; FocusApply() end, function(v) return string.format("%.0f", v) end )) table.insert(controls, CreateSlider(focusSection, "蓝条高度", 280, -126, 150, 4, 20, 1, function() return SFramesDB.focusPowerHeight end, function(v) SFramesDB.focusPowerHeight = v; FocusApply() end, function(v) return string.format("%.0f", v) end )) table.insert(controls, CreateSlider(focusSection, "头像宽度", 14, -180, 150, 0, 80, 1, function() return SFramesDB.focusPortraitWidth end, function(v) SFramesDB.focusPortraitWidth = v; FocusApply() end, function(v) return string.format("%.0f", v) end )) table.insert(controls, CreateSlider(focusSection, "背景透明度", 280, -180, 150, 0, 1, 0.05, function() return SFramesDB.focusBgAlpha end, function(v) SFramesDB.focusBgAlpha = v; FocusApply() end, function(v) return string.format("%.0f%%", v * 100) end )) table.insert(controls, CreateCheckBox(focusSection, "显示 3D 头像", 12, -230, function() return SFramesDB.focusShowPortrait ~= false end, function(checked) SFramesDB.focusShowPortrait = checked; FocusApply() end )) table.insert(controls, CreateCheckBox(focusSection, "显示施法条", 270, -230, function() return SFramesDB.focusShowCastBar ~= false end, function(checked) SFramesDB.focusShowCastBar = checked; FocusApply() end )) table.insert(controls, CreateCheckBox(focusSection, "显示 Buff/Debuff", 12, -260, function() return SFramesDB.focusShowAuras ~= false end, function(checked) SFramesDB.focusShowAuras = checked; FocusApply() end )) table.insert(controls, CreateSlider(focusSection, "姓名字号", 14, -304, 150, 8, 16, 1, function() return SFramesDB.focusNameFontSize end, function(v) SFramesDB.focusNameFontSize = v; FocusApply() end, function(v) return string.format("%.0f", v) end )) table.insert(controls, CreateSlider(focusSection, "数值字号", 280, -304, 150, 7, 14, 1, function() return SFramesDB.focusValueFontSize end, function(v) SFramesDB.focusValueFontSize = v; FocusApply() end, function(v) return string.format("%.0f", v) end )) CreateLabel(focusSection, "操作: Shift+左键目标框体 = 设为焦点 | 右键目标框体菜单 = 设为/取消焦点", 14, -348, font, 9, 0.65, 0.58, 0.62) CreateLabel(focusSection, "命令: /nui focus (设焦点) | /nui unfocus (取消) | /nui focustarget (选中焦点)", 14, -364, font, 9, 0.65, 0.58, 0.62) CreateLabel(focusSection, "提示: 启用/禁用焦点框体需要 /reload,其余设置实时生效。", 14, -384, font, 10, 0.6, 0.6, 0.65) AddFrameAdvancedSections(root, controls, { prefix = "focus", startY = -418, apply = FocusApply, extraFontBlocks = { { title = "施法条字体", keyName = "focusCastFontKey", sizeKey = "focusCastFontSize" }, { title = "距离文字字体", keyName = "focusDistanceFontKey", sizeKey = "focusDistanceFontSize" }, }, fontSectionHeight = 458, }) focusScroll:UpdateRange() self.focusControls = controls self.focusScroll = focusScroll end function SFrames.ConfigUI:BuildPartyPage() local font = SFrames:GetFont() local page = self.partyPage local controls = {} local function RefreshParty() if SFrames.Party and SFrames.Party.ApplyConfig then SFrames.Party:ApplyConfig() end if SFrames.Party and SFrames.Party.UpdateAll then SFrames.Party:UpdateAll() end end local uiScroll = CreateScrollArea(page, 4, -4, 548, 420, 1720) local root = uiScroll.child local partySection = CreateSection(root, "小队", 8, -8, 520, 460, font) table.insert(controls, CreateCheckBox(partySection, "启用 Nanami 小队框体(需 /reload)", 12, -28, function() return SFramesDB.enablePartyFrame ~= false end, function(checked) SFramesDB.enablePartyFrame = checked end )) table.insert(controls, CreateResetButton(partySection, 356, -24, "party", RefreshParty, controls)) CreateButton(partySection, "解锁小队框架", 14, -52, 130, 22, function() if SFrames and SFrames.UnlockFrames then SFrames:UnlockFrames() end end) CreateButton(partySection, "锁定小队框架", 154, -52, 130, 22, function() if SFrames and SFrames.LockFrames then SFrames:LockFrames() end end) table.insert(controls, CreateCheckBox(partySection, "横向布局(关闭为竖向)", 12, -86, function() return SFramesDB.partyLayout == "horizontal" end, function(checked) if checked then SFramesDB.partyLayout = "horizontal" else SFramesDB.partyLayout = "vertical" end end, function(checked) if SFrames.Party and SFrames.Party.SetLayout then if checked then SFrames.Party:SetLayout("horizontal") else SFrames.Party:SetLayout("vertical") end end RefreshParty() end )) table.insert(controls, CreateSlider(partySection, "缩放", 14, -134, 150, 0.7, 1.8, 0.05, function() return SFramesDB.partyFrameScale end, function(value) SFramesDB.partyFrameScale = value end, function(v) return string.format("%.2f", v) end, function() RefreshParty() end )) table.insert(controls, CreateSlider(partySection, "框体宽度", 170, -134, 150, 120, 320, 1, function() return SFramesDB.partyFrameWidth end, function(value) SFramesDB.partyFrameWidth = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) table.insert(controls, CreateSlider(partySection, "框体高度", 326, -134, 130, 28, 80, 1, function() return SFramesDB.partyFrameHeight end, function(value) SFramesDB.partyFrameHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) table.insert(controls, CreateSlider(partySection, "头像宽度", 14, -196, 150, 24, 64, 1, function() return SFramesDB.partyPortraitWidth end, function(value) SFramesDB.partyPortraitWidth = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) table.insert(controls, CreateSlider(partySection, "生命条高度", 170, -196, 150, 10, 60, 1, function() return SFramesDB.partyHealthHeight end, function(value) SFramesDB.partyHealthHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) table.insert(controls, CreateSlider(partySection, "能量条高度", 326, -196, 130, 6, 30, 1, function() return SFramesDB.partyPowerHeight end, function(value) SFramesDB.partyPowerHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) table.insert(controls, CreateSlider(partySection, "横向间距", 14, -258, 150, 0, 40, 1, function() return SFramesDB.partyHorizontalGap end, function(value) SFramesDB.partyHorizontalGap = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) table.insert(controls, CreateSlider(partySection, "纵向间距", 170, -258, 150, 0, 80, 1, function() return SFramesDB.partyVerticalGap end, function(value) SFramesDB.partyVerticalGap = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) table.insert(controls, CreateSlider(partySection, "姓名字号", 326, -258, 130, 8, 18, 1, function() return SFramesDB.partyNameFontSize end, function(value) SFramesDB.partyNameFontSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) table.insert(controls, CreateSlider(partySection, "数值字号", 14, -320, 150, 8, 18, 1, function() return SFramesDB.partyValueFontSize end, function(value) SFramesDB.partyValueFontSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) table.insert(controls, CreateSlider(partySection, "背景透明度", 170, -320, 150, 0, 1.0, 0.05, function() return SFramesDB.partyBgAlpha end, function(value) SFramesDB.partyBgAlpha = value end, function(v) return string.format("%.0f%%", v * 100) end, function(value) if SFrames.Party and SFrames.Party.frames then for _, f in ipairs(SFrames.Party.frames) do if f then if f.pbg then ApplyBgAlphaToFrame(f.pbg, value) end if f.healthBGFrame then ApplyBgAlphaToFrame(f.healthBGFrame, value) end end end end end )) table.insert(controls, CreateCheckBox(partySection, "显示小队增益", 14, -380, function() return SFramesDB.partyShowBuffs ~= false end, function(checked) SFramesDB.partyShowBuffs = checked end, function() RefreshParty() end )) table.insert(controls, CreateCheckBox(partySection, "显示小队减益", 170, -380, function() return SFramesDB.partyShowDebuffs ~= false end, function(checked) SFramesDB.partyShowDebuffs = checked end, function() RefreshParty() end )) table.insert(controls, CreateCheckBox(partySection, "启用3D头像", 326, -380, function() return SFramesDB.partyPortrait3D ~= false end, function(checked) SFramesDB.partyPortrait3D = checked end, function() RefreshParty() end )) CreateButton(partySection, "小队测试模式", 326, -410, 130, 22, function() if SFrames.Party and SFrames.Party.TestMode then SFrames.Party:TestMode() end end) AddFrameAdvancedSections(root, controls, { prefix = "party", startY = -486, apply = RefreshParty, }) uiScroll:UpdateRange() self.partyControls = controls self.partyScroll = uiScroll end function SFrames.ConfigUI:BuildBagPage() local font = SFrames:GetFont() local page = self.bagPage local controls = {} local bagScroll = CreateScrollArea(page, 4, -4, 548, 458, 700) local root = bagScroll.child local bagSection = CreateSection(root, "背包", 8, -8, 502, 300, font) table.insert(controls, CreateCheckBox(bagSection, "启用 Nanami 背包(需 /reload)", 12, -28, function() return SFramesDB.Bags.enable ~= false end, function(checked) SFramesDB.Bags.enable = checked end )) table.insert(controls, CreateCheckBox(bagSection, "自动出售灰色物品", 12, -52, function() return SFramesDB.Bags.sellGrey ~= false end, function(checked) SFramesDB.Bags.sellGrey = checked end )) table.insert(controls, CreateSlider(bagSection, "背包列数", 16, -90, 220, 4, 20, 1, function() return SFramesDB.Bags.columns end, function(value) SFramesDB.Bags.columns = value end, function(value) return tostring(math.floor(value + 0.5)) end, function() if SFrames and SFrames.Bags and SFrames.Bags.Container and SFrames.Bags.Container.UpdateLayout then SFrames.Bags.Container:UpdateLayout() end end )) table.insert(controls, CreateSlider(bagSection, "背包间距", 258, -90, 220, 0, 12, 1, function() return SFramesDB.Bags.bagSpacing end, function(value) SFramesDB.Bags.bagSpacing = value end, function(value) return tostring(math.floor(value + 0.5)) end, function() if SFrames and SFrames.Bags and SFrames.Bags.Container and SFrames.Bags.Container.UpdateLayout then SFrames.Bags.Container:UpdateLayout() end end )) table.insert(controls, CreateSlider(bagSection, "背包缩放", 16, -150, 220, 0.50, 2.00, 0.05, function() return SFramesDB.Bags.scale end, function(value) SFramesDB.Bags.scale = value end, function(value) return string.format("%.2f", value) end, function(value) if SFramesBagFrame then SFramesBagFrame:SetScale(value) end end )) table.insert(controls, CreateSlider(bagSection, "背包透明度", 258, -150, 220, 0.1, 1.0, 0.05, function() return SFramesDB.Bags.alpha or 1 end, function(value) SFramesDB.Bags.alpha = value end, function(value) return string.format("%.0f%%", value * 100) end, function(value) if SFramesBagFrame then SFramesBagFrame:SetAlpha(value) end end )) table.insert(controls, CreateSlider(bagSection, "背包背景透明度", 16, -196, 220, 0, 1.0, 0.05, function() return SFramesDB.Bags.bgAlpha or 0.95 end, function(value) SFramesDB.Bags.bgAlpha = value end, function(value) return string.format("%.0f%%", value * 100) end, function(value) if SFramesBagFrame then ApplyBgAlphaToFrame(SFramesBagFrame, value) end end )) CreateButton(bagSection, "打开背包", 258, -196, 220, 24, function() if SFrames and SFrames.Bags and SFrames.Bags.Container and SFrames.Bags.Container.Open then SFrames.Bags.Container:Open() end end) local bankSection = CreateSection(root, "银行", 8, -316, 502, 215, font) table.insert(controls, CreateSlider(bankSection, "银行列数", 16, -50, 220, 4, 24, 1, function() return SFramesDB.Bags.bankColumns end, function(value) SFramesDB.Bags.bankColumns = value end, function(value) return tostring(math.floor(value + 0.5)) end, function() if SFrames and SFrames.Bags and SFrames.Bags.Bank and SFrames.Bags.Bank.UpdateLayout then SFrames.Bags.Bank:UpdateLayout() end end )) table.insert(controls, CreateSlider(bankSection, "银行间距", 258, -50, 220, 0, 12, 1, function() return SFramesDB.Bags.bankSpacing end, function(value) SFramesDB.Bags.bankSpacing = value end, function(value) return tostring(math.floor(value + 0.5)) end, function() if SFrames and SFrames.Bags and SFrames.Bags.Bank and SFrames.Bags.Bank.UpdateLayout then SFrames.Bags.Bank:UpdateLayout() end end )) table.insert(controls, CreateSlider(bankSection, "银行缩放", 16, -110, 220, 0.50, 2.00, 0.05, function() return SFramesDB.Bags.bankScale end, function(value) SFramesDB.Bags.bankScale = value end, function(value) return string.format("%.2f", value) end, function(value) if SFramesBankFrame then SFramesBankFrame:SetScale(value) end end )) table.insert(controls, CreateSlider(bankSection, "银行透明度", 258, -110, 220, 0.1, 1.0, 0.05, function() return SFramesDB.Bags.bankAlpha or 1 end, function(value) SFramesDB.Bags.bankAlpha = value end, function(value) return string.format("%.0f%%", value * 100) end, function(value) if SFramesBankFrame then SFramesBankFrame:SetAlpha(value) end end )) CreateButton(bankSection, "打开离线银行", 16, -170, 220, 24, function() if SFrames and SFrames.Bags and SFrames.Bags.Bank and SFrames.Bags.Bank.OpenOffline then SFrames.Bags.Bank:OpenOffline() end end) --------------------------------------------------------------------------- -- Sell Price Database Section --------------------------------------------------------------------------- local priceSection = CreateSection(root, "售价数据库", 8, -481, 502, 126, font) local function CountCacheEntries() local n = 0 if SFramesGlobalDB and SFramesGlobalDB.sellPriceCache then for _ in pairs(SFramesGlobalDB.sellPriceCache) do n = n + 1 end end return n end local function CountDBEntries() return 0 -- No longer using embedded DB file end local dbCount = CountDBEntries() local statusLabel = CreateLabel(priceSection, string.format("已学习: %d 条 (API模式, 无内置数据库)", CountCacheEntries()), 14, -30, font, 10, 0.8, 0.8, 0.7) local scanResultLabel = CreateLabel(priceSection, "", 14, -104, font, 10, 0.45, 0.9, 0.45) CreateButton(priceSection, "扫描全部物品售价", 16, -48, 228, 24, function() if not SFramesGlobalDB then SFramesGlobalDB = {} end if not SFramesGlobalDB.sellPriceCache then SFramesGlobalDB.sellPriceCache = {} end local cache = SFramesGlobalDB.sellPriceCache local scanned, learned = 0, 0 local function TryScan(link) if not link then return end local _, _, sid = string.find(link, "item:(%d+)") if not sid then return end local id = tonumber(sid) if not id then return end scanned = scanned + 1 if cache[id] and cache[id] > 0 then return end local price if ShaguTweaks and ShaguTweaks.SellValueDB and ShaguTweaks.SellValueDB[id] and ShaguTweaks.SellValueDB[id] > 0 then price = ShaguTweaks.SellValueDB[id] elseif aux and aux.account_data and aux.account_data.merchant_sell and aux.account_data.merchant_sell[id] and aux.account_data.merchant_sell[id] > 0 then price = aux.account_data.merchant_sell[id] else local _,_,_,_,_,_,_,_,_,_,sp = GetItemInfo(link) if sp and type(sp) == "number" and sp > 0 then price = sp end end if price then cache[id] = price learned = learned + 1 end end for bag = 0, 4 do for slot = 1, (GetContainerNumSlots(bag) or 0) do TryScan(GetContainerItemLink(bag, slot)) end end for bag = 5, 10 do pcall(function() for slot = 1, (GetContainerNumSlots(bag) or 0) do TryScan(GetContainerItemLink(bag, slot)) end end) end pcall(function() for slot = 1, (GetContainerNumSlots(-1) or 0) do TryScan(GetContainerItemLink(-1, slot)) end end) for slot = 1, 19 do TryScan(GetInventoryItemLink("player", slot)) end statusLabel:SetText(string.format("已学习: %d 条 (API模式)", CountCacheEntries())) scanResultLabel:SetText(string.format("完成! 检查 %d 件, 新增 %d 条售价", scanned, learned)) end) CreateButton(priceSection, "清空学习缓存", 258, -48, 228, 24, function() if SFramesGlobalDB then SFramesGlobalDB.sellPriceCache = {} end statusLabel:SetText("已学习: 0 条 (API模式)") scanResultLabel:SetText("学习缓存已清空") end) CreateDesc(priceSection, "扫描背包/银行/装备中所有物品, 缓存售价到离线数据库 (随存档保存)", 14, -76, font, 480) CreateDesc(priceSection, "多次游玩后数据自动丰富, 其他角色也能共享离线数据", 14, -90, font, 480) bagScroll:UpdateRange() self.bagScroll = bagScroll self.bagControls = controls end function SFrames.ConfigUI:BuildRaidPage() local font = SFrames:GetFont() local page = self.raidPage local controls = {} local function RefreshRaid() if SFrames.Raid and SFrames.Raid.ApplyLayout then if not SFrames.Raid._framesBuilt then return end SFrames.Raid:ApplyLayout() SFrames.Raid:UpdateAll() for i = 1, 40 do local f = SFrames.Raid.frames[i] and SFrames.Raid.frames[i].frame if f then SFrames.Raid:ApplyFrameStyle(f, SFrames.Raid:GetMetrics()) end end end end local uiScroll = CreateScrollArea(page, 4, -4, 548, 420, 1620) local root = uiScroll.child local raidSection = CreateSection(root, "团队框架设置", 8, -8, 520, 570, font) table.insert(controls, CreateCheckBox(raidSection, "启用 Nanami 团队框架(默认启用,需 /reload)", 12, -28, function() return SFramesDB.enableRaidFrames ~= false end, function(checked) SFramesDB.enableRaidFrames = checked end )) table.insert(controls, CreateResetButton(raidSection, 356, -24, "raid", RefreshRaid, controls)) CreateButton(raidSection, "解锁团队框架", 14, -56, 130, 22, function() if SFrames and SFrames.UnlockFrames then SFrames:UnlockFrames() end end) CreateButton(raidSection, "锁定团队框架", 154, -56, 130, 22, function() if SFrames and SFrames.LockFrames then SFrames:LockFrames() end end) table.insert(controls, CreateCheckBox(raidSection, "横向小队排列(关闭为竖向叠放)", 12, -88, function() return SFramesDB.raidLayout == "horizontal" end, function(checked) SFramesDB.raidLayout = checked and "horizontal" or "vertical" end, function() RefreshRaid() end )) table.insert(controls, CreateSlider(raidSection, "缩放", 14, -150, 150, 0.5, 2.0, 0.05, function() return SFramesDB.raidFrameScale end, function(value) SFramesDB.raidFrameScale = value end, function(v) return string.format("%.2f", v) end, function(val) if SFrames.Raid and SFrames.Raid.parent then SFrames.Raid.parent:SetScale(val) end end )) table.insert(controls, CreateSlider(raidSection, "框体宽度", 170, -150, 150, 30, 150, 1, function() return SFramesDB.raidFrameWidth end, function(value) SFramesDB.raidFrameWidth = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshRaid() end )) table.insert(controls, CreateSlider(raidSection, "框体高度", 326, -150, 130, 20, 80, 1, function() return SFramesDB.raidFrameHeight end, function(value) SFramesDB.raidFrameHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshRaid() end )) table.insert(controls, CreateSlider(raidSection, "生命条高度", 14, -212, 150, 10, 78, 1, function() return SFramesDB.raidHealthHeight end, function(value) SFramesDB.raidHealthHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshRaid() end )) table.insert(controls, CreateCheckBox(raidSection, "显示能量条", 170, -210, function() return SFramesDB.raidShowPower ~= false end, function(checked) SFramesDB.raidShowPower = checked end, function() RefreshRaid() end )) table.insert(controls, CreateCheckBox(raidSection, "显示小队标题(横向=顶部,竖向=左侧)", 14, -244, function() return SFramesDB.raidShowGroupLabel ~= false end, function(checked) SFramesDB.raidShowGroupLabel = checked end, function() RefreshRaid() end )) table.insert(controls, CreateSlider(raidSection, "横向间距", 14, -274, 150, 0, 40, 1, function() return SFramesDB.raidHorizontalGap end, function(value) SFramesDB.raidHorizontalGap = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshRaid() end )) table.insert(controls, CreateSlider(raidSection, "纵向间距", 170, -274, 150, 0, 40, 1, function() return SFramesDB.raidVerticalGap end, function(value) SFramesDB.raidVerticalGap = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshRaid() end )) table.insert(controls, CreateSlider(raidSection, "小队间隔", 326, -274, 130, 0, 60, 1, function() return SFramesDB.raidGroupGap end, function(value) SFramesDB.raidGroupGap = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshRaid() end )) table.insert(controls, CreateSlider(raidSection, "姓名字号", 14, -336, 150, 8, 18, 1, function() return SFramesDB.raidNameFontSize end, function(value) SFramesDB.raidNameFontSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshRaid() end )) table.insert(controls, CreateSlider(raidSection, "数值字号", 170, -336, 150, 8, 18, 1, function() return SFramesDB.raidValueFontSize end, function(value) SFramesDB.raidValueFontSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshRaid() end )) table.insert(controls, CreateSlider(raidSection, "背景透明度", 326, -336, 130, 0, 1.0, 0.05, function() return SFramesDB.raidBgAlpha end, function(value) SFramesDB.raidBgAlpha = value end, function(v) return string.format("%.0f%%", v * 100) end, function(value) if SFrames.Raid and SFrames.Raid.frames then for i = 1, 40 do local entry = SFrames.Raid.frames[i] local f = entry and entry.frame if f then if f.healthBGFrame then ApplyBgAlphaToFrame(f.healthBGFrame, value) end if f.powerBGFrame then ApplyBgAlphaToFrame(f.powerBGFrame, value) end end end end end )) table.insert(controls, CreateCheckBox(raidSection, "血量百分比模式 (关=紧凑数值/-缺口)", 14, -390, function() return SFramesDB.raidHealthFormat == "percent" end, function(checked) SFramesDB.raidHealthFormat = checked and "percent" or "compact" end, function() RefreshRaid() end )) CreateButton(raidSection, "测试模式(假团队)", 14, -450, 150, 24, function() if SFrames.Raid then if SFrames.Raid.testing then SFrames.Raid.testing = false SFrames.Raid:UpdateAll() else SFrames.Raid.testing = true SFrames.Raid:EnsureFrames() SFrames.Raid.parent:Show() for i = 1, 40 do local f = SFrames.Raid.frames[i].frame f:Show() f.nameText:SetText("玩家"..i) f.healthText:SetText("100%") f.health:SetMinMaxValues(0, 100) f.health:SetValue(100) f.health:SetStatusBarColor(0, 1, 0) end end end end) AddFrameAdvancedSections(root, controls, { prefix = "raid", startY = -592, apply = RefreshRaid, hasPortraitBg = false, showPowerHint = "关闭团队能量条后,这些设置会保留。", }) uiScroll:UpdateRange() self.raidControls = controls self.raidScroll = uiScroll end function SFrames.ConfigUI:BuildCharPage() local font = SFrames:GetFont() local page = self.charPage local controls = {} local uiScroll = CreateScrollArea(page, 4, -4, 552, PANEL_HEIGHT - 110, 720) local root = uiScroll.child local charSection = CreateSection(root, "人物面板", 8, -8, 524, 430, font) table.insert(controls, CreateCheckBox(charSection, "启用 Nanami 人物面板(需 /reload)", 12, -34, function() return SFramesDB.charPanelEnable ~= false end, function(checked) SFramesDB.charPanelEnable = checked end )) CreateDesc(charSection, "关闭后按 C 键将打开原版人物面板", 36, -50, font) table.insert(controls, CreateSlider(charSection, "面板缩放", 14, -72, 240, 0.6, 1.5, 0.05, function() return SFramesDB.charPanelScale or 1.0 end, function(v) SFramesDB.charPanelScale = v end, function(v) return string.format("%.0f%%", v * 100) end, function() if SFramesCharacterPanel then SFramesCharacterPanel:SetScale(SFramesDB.charPanelScale or 1.0) end end )) table.insert(controls, CreateCheckBox(charSection, "显示PVP称号", 12, -118, function() return SFramesDB.charShowTitle ~= false end, function(checked) SFramesDB.charShowTitle = checked end, function() if SFrames.CharacterPanel and SFrames.CharacterPanel.UpdateCurrentTab then SFrames.CharacterPanel:UpdateCurrentTab() end end )) table.insert(controls, CreateCheckBox(charSection, "显示公会名", 12, -146, function() return SFramesDB.charShowGuild ~= false end, function(checked) SFramesDB.charShowGuild = checked end, function() if SFrames.CharacterPanel and SFrames.CharacterPanel.UpdateCurrentTab then SFrames.CharacterPanel:UpdateCurrentTab() end end )) table.insert(controls, CreateCheckBox(charSection, "显示装备品质光晕", 12, -174, function() return SFramesDB.charShowQualityGlow ~= false end, function(checked) SFramesDB.charShowQualityGlow = checked end, function() if SFrames.CharacterPanel and SFrames.CharacterPanel.UpdateCurrentTab then SFrames.CharacterPanel:UpdateCurrentTab() end end )) table.insert(controls, CreateCheckBox(charSection, "显示3D角色模型", 12, -202, function() return SFramesDB.charShowModel ~= false end, function(checked) SFramesDB.charShowModel = checked end, function() if SFrames.CharacterPanel and SFrames.CharacterPanel.UpdateCurrentTab then SFrames.CharacterPanel:UpdateCurrentTab() end end )) table.insert(controls, CreateCheckBox(charSection, "启用 Nanami 观察面板(需 /reload)", 12, -230, function() return SFramesDB.enableInspect ~= false end, function(checked) SFramesDB.enableInspect = checked end )) CreateDesc(charSection, "关闭后观察他人将使用原版界面", 36, -246, font) CreateLabel(charSection, "提示: 面板使用 C 键打开/关闭,按 ESC 关闭。", 14, -274, font, 10, 0.6, 0.6, 0.65) CreateLabel(charSection, "装备页下方可滚动查看全部角色属性。", 14, -292, font, 10, 0.6, 0.6, 0.65) -- Spell Book section local sbSection = CreateSection(root, "法术书", 8, -446, 524, 200, font) table.insert(controls, CreateSlider(sbSection, "法术书缩放", 14, -34, 240, 0.6, 1.5, 0.05, function() return SFramesDB.spellBookScale or 1.0 end, function(v) SFramesDB.spellBookScale = v end, function(v) return string.format("%.0f%%", v * 100) end, function() if SFramesSpellBookUI then SFramesSpellBookUI:SetScale(SFramesDB.spellBookScale or 1.0) end end )) table.insert(controls, CreateCheckBox(sbSection, "只显示最高等级法术", 14, -80, function() return SFramesDB.spellBookHighestOnly == true end, function(checked) SFramesDB.spellBookHighestOnly = checked end )) CreateDesc(sbSection, "隐藏同名法术的低等级版本,只保留最高等级", 36, -96, font) table.insert(controls, CreateCheckBox(sbSection, "学习新等级后自动替换动作条", 14, -116, function() return SFramesDB.spellBookAutoReplace == true end, function(checked) SFramesDB.spellBookAutoReplace = checked end )) CreateDesc(sbSection, "学习高等级法术时,自动替换动作条上的低等级版本", 36, -132, font) CreateLabel(sbSection, "提示: 法术书使用 P 键打开/关闭,支持鼠标滚轮翻页。", 14, -160, font, 10, 0.6, 0.6, 0.65) -- Social section local socialSection = CreateSection(root, "社交面板", 8, -660, 524, 120, font) table.insert(controls, CreateSlider(socialSection, "社交面板缩放", 14, -34, 240, 0.6, 1.5, 0.05, function() return SFramesDB.socialScale or 1.0 end, function(v) SFramesDB.socialScale = v end, function(v) return string.format("%.0f%%", v * 100) end, function() if SFramesSocialUI then SFramesSocialUI:SetScale(SFramesDB.socialScale or 1.0) end end )) CreateLabel(socialSection, "提示: 社交面板使用 O 键打开/关闭。", 14, -80, font, 10, 0.6, 0.6, 0.65) uiScroll:UpdateRange() self.charControls = controls self.charScroll = uiScroll end function SFrames.ConfigUI:BuildActionBarPage() local font = SFrames:GetFont() local page = self.actionBarPage local controls = {} local function RefreshAB() if SFrames.ActionBars and SFrames.ActionBars.ApplyConfig then SFrames.ActionBars:ApplyConfig() end end local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 1400) local root = uiScroll.child local abSection = CreateSection(root, "动作条", 8, -8, 520, 1040, font) table.insert(controls, CreateCheckBox(abSection, "启用动作条接管(需 /reload 生效)", 12, -28, function() return SFramesDB.ActionBars.enable ~= false end, function(checked) SFramesDB.ActionBars.enable = checked end )) CreateLabel(abSection, "按键绑定:", 14, -56, font, 11, 0.85, 0.75, 0.80) CreateButton(abSection, "进入按键绑定模式", 14, -76, 160, 24, function() if SFrames.ActionBars and SFrames.ActionBars.EnterKeyBindMode then if self.frame then self.frame:Hide() end SFrames.ActionBars:EnterKeyBindMode() end end) CreateDesc(abSection, "悬停动作条/姿态栏/宠物栏按钮,按下键盘键/鼠标键(3-5)/滚轮绑定。右键清除。ESC 退出。也可用 /nui bind", 14, -100, font, 480) table.insert(controls, CreateSlider(abSection, "按钮大小", 14, -130, 150, 24, 48, 1, function() return SFramesDB.ActionBars.buttonSize end, function(value) SFramesDB.ActionBars.buttonSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshAB() end )) table.insert(controls, CreateSlider(abSection, "按钮间距", 180, -130, 150, 0, 8, 1, function() return SFramesDB.ActionBars.buttonGap end, function(value) SFramesDB.ActionBars.buttonGap = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshAB() end )) table.insert(controls, CreateSlider(abSection, "整体缩放", 346, -130, 150, 0.5, 2.0, 0.05, function() return SFramesDB.ActionBars.scale end, function(value) SFramesDB.ActionBars.scale = value end, function(v) return string.format("%.2f", v) end, function() RefreshAB() end )) table.insert(controls, CreateSlider(abSection, "显示行数", 14, -192, 150, 1, 3, 1, function() return SFramesDB.ActionBars.barCount end, function(value) SFramesDB.ActionBars.barCount = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshAB() end )) table.insert(controls, CreateSlider(abSection, "姿态/宠物栏按钮大小", 180, -192, 150, 16, 40, 1, function() return SFramesDB.ActionBars.smallBarSize end, function(value) SFramesDB.ActionBars.smallBarSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshAB() end )) table.insert(controls, CreateSlider(abSection, "透明度", 14, -242, 150, 0.1, 1.0, 0.05, function() return SFramesDB.ActionBars.alpha or 1 end, function(value) SFramesDB.ActionBars.alpha = value end, function(v) return string.format("%.0f%%", v * 100) end, function(value) if SFrames.ActionBars then if SFrames.ActionBars.anchor then SFrames.ActionBars.anchor:SetAlpha(value) end if SFrames.ActionBars.row2Anchor then SFrames.ActionBars.row2Anchor:SetAlpha(value) end if SFrames.ActionBars.row3Anchor then SFrames.ActionBars.row3Anchor:SetAlpha(value) end if SFrames.ActionBars.rightBar1Holder then SFrames.ActionBars.rightBar1Holder:SetAlpha(value) end if SFrames.ActionBars.rightBar2Holder then SFrames.ActionBars.rightBar2Holder:SetAlpha(value) end if SFrames.ActionBars.stanceHolder then SFrames.ActionBars.stanceHolder:SetAlpha(value) end if SFrames.ActionBars.petHolder then SFrames.ActionBars.petHolder:SetAlpha(value) end end end )) table.insert(controls, CreateSlider(abSection, "背景透明度", 170, -242, 150, 0, 1.0, 0.05, function() return SFramesDB.ActionBars.bgAlpha or 0.9 end, function(value) SFramesDB.ActionBars.bgAlpha = value end, function(v) return string.format("%.0f%%", v * 100) end, function(value) if SFrames.ActionBars then if SFrames.ActionBars.anchor then ApplyBgAlphaToFrame(SFrames.ActionBars.anchor, value) end if SFrames.ActionBars.row2Anchor then ApplyBgAlphaToFrame(SFrames.ActionBars.row2Anchor, value) end if SFrames.ActionBars.row3Anchor then ApplyBgAlphaToFrame(SFrames.ActionBars.row3Anchor, value) end if SFrames.ActionBars.rightBar1Holder then ApplyBgAlphaToFrame(SFrames.ActionBars.rightBar1Holder, value) end if SFrames.ActionBars.rightBar2Holder then ApplyBgAlphaToFrame(SFrames.ActionBars.rightBar2Holder, value) end if SFrames.ActionBars.stanceHolder then ApplyBgAlphaToFrame(SFrames.ActionBars.stanceHolder, value) end if SFrames.ActionBars.petHolder then ApplyBgAlphaToFrame(SFrames.ActionBars.petHolder, value) end end end )) table.insert(controls, CreateCheckBox(abSection, "显示快捷键文字", 12, -300, function() return SFramesDB.ActionBars.showHotkey ~= false end, function(checked) SFramesDB.ActionBars.showHotkey = checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, "显示宏名称", 200, -300, function() return SFramesDB.ActionBars.showMacroName == true end, function(checked) SFramesDB.ActionBars.showMacroName = checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, "超距红色着色(技能超出射程时按钮变红)", 12, -328, function() return SFramesDB.ActionBars.rangeColoring ~= false end, function(checked) SFramesDB.ActionBars.rangeColoring = checked end )) table.insert(controls, CreateCheckBox(abSection, "显示宠物动作条", 12, -356, function() return SFramesDB.ActionBars.showPetBar ~= false end, function(checked) SFramesDB.ActionBars.showPetBar = checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, "显示姿态栏", 200, -356, function() return SFramesDB.ActionBars.showStanceBar ~= false end, function(checked) SFramesDB.ActionBars.showStanceBar = checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, "显示右侧动作条", 12, -384, function() return SFramesDB.ActionBars.showRightBars ~= false end, function(checked) SFramesDB.ActionBars.showRightBars = checked end, function() RefreshAB() end )) table.insert(controls, CreateSlider(abSection, "右侧栏1每行按钮数", 14, -416, 150, 1, 12, 1, function() return SFramesDB.ActionBars.rightBar1PerRow or 1 end, function(value) SFramesDB.ActionBars.rightBar1PerRow = value end, function(v) local n = math.floor(v + 0.5); if n == 1 then return "1 (竖)" elseif n == 12 then return "12 (横)" else return tostring(n) end end, function() RefreshAB() end )) table.insert(controls, CreateSlider(abSection, "右侧栏2每行按钮数", 180, -416, 150, 1, 12, 1, function() return SFramesDB.ActionBars.rightBar2PerRow or 1 end, function(value) SFramesDB.ActionBars.rightBar2PerRow = value end, function(v) local n = math.floor(v + 0.5); if n == 1 then return "1 (竖)" elseif n == 12 then return "12 (横)" else return tostring(n) end end, function() RefreshAB() end )) table.insert(controls, CreateSlider(abSection, "底部栏1每行", 12, -456, 110, 1, 12, 1, function() return SFramesDB.ActionBars.bottomBar1PerRow or 12 end, function(value) SFramesDB.ActionBars.bottomBar1PerRow = value end, function(v) local n = math.floor(v + 0.5); if n == 12 then return "12 (满)" else return tostring(n) end end, function() RefreshAB() end )) table.insert(controls, CreateSlider(abSection, "底部栏2每行", 134, -456, 110, 1, 12, 1, function() return SFramesDB.ActionBars.bottomBar2PerRow or 12 end, function(value) SFramesDB.ActionBars.bottomBar2PerRow = value end, function(v) local n = math.floor(v + 0.5); if n == 12 then return "12 (满)" else return tostring(n) end end, function() RefreshAB() end )) table.insert(controls, CreateSlider(abSection, "底部栏3每行", 256, -456, 110, 1, 12, 1, function() return SFramesDB.ActionBars.bottomBar3PerRow or 12 end, function(value) SFramesDB.ActionBars.bottomBar3PerRow = value end, function(v) local n = math.floor(v + 0.5); if n == 12 then return "12 (满)" else return tostring(n) end end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, "始终显示动作条(空格子也显示背景框)", 12, -514, function() return SFramesDB.ActionBars.alwaysShowGrid == true end, function(checked) SFramesDB.ActionBars.alwaysShowGrid = checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, "按钮圆角", 12, -542, function() return SFramesDB.ActionBars.buttonRounded == true end, function(checked) SFramesDB.ActionBars.buttonRounded = checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, "按钮内阴影", 200, -542, function() return SFramesDB.ActionBars.buttonInnerShadow == true end, function(checked) SFramesDB.ActionBars.buttonInnerShadow = checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, "显示动作条狮鹫(在底部动作条两侧显示装饰狮鹫)", 12, -570, function() return SFramesDB.ActionBars.hideGryphon == false end, function(checked) SFramesDB.ActionBars.hideGryphon = not checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, "狮鹫置于动作条之上(否则在动作条之下)", 12, -598, function() return SFramesDB.ActionBars.gryphonOnTop == true end, function(checked) SFramesDB.ActionBars.gryphonOnTop = checked end, function() RefreshAB() end )) table.insert(controls, CreateSlider(abSection, "狮鹫宽度", 14, -652, 150, 24, 200, 2, function() return SFramesDB.ActionBars.gryphonWidth or 64 end, function(value) SFramesDB.ActionBars.gryphonWidth = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshAB() end )) table.insert(controls, CreateSlider(abSection, "狮鹫高度", 180, -652, 150, 24, 200, 2, function() return SFramesDB.ActionBars.gryphonHeight or 64 end, function(value) SFramesDB.ActionBars.gryphonHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshAB() end )) CreateDesc(abSection, "使用布局模式 (/nui layout) 调整狮鹫位置", 14, -714, font, 480) -- 狮鹫样式选择器(带图例预览) CreateLabel(abSection, "狮鹫样式:", 14, -766, font, 11, 0.85, 0.75, 0.80) local GRYPHON_STYLES_UI = { { key = "dragonflight", label = "巨龙时代", tex = "Interface\\AddOns\\Nanami-UI\\img\\df-gryphon" }, { key = "dragonflight_beta", label = "巨龙时代(Beta)", tex = "Interface\\AddOns\\Nanami-UI\\img\\df-gryphon-beta" }, { key = "classic", label = "经典", tex = "Interface\\MainMenuBar\\UI-MainMenuBar-EndCap-Human" }, { key = "cat", label = "猫", tex = "Interface\\AddOns\\Nanami-UI\\img\\cat" }, } local styleBorders = {} local styleStartX = 14 local styleY = -786 for idx, style in ipairs(GRYPHON_STYLES_UI) do local xOff = styleStartX + (idx - 1) * 125 -- 预览框 local preview = CreateFrame("Frame", nil, abSection) preview:SetWidth(64) preview:SetHeight(64) preview:SetPoint("TOPLEFT", abSection, "TOPLEFT", xOff, styleY) -- 预览背景 local bg = preview:CreateTexture(nil, "BACKGROUND") bg:SetAllPoints() bg:SetTexture(0, 0, 0, 0.4) -- 纹理预览 local tex = preview:CreateTexture(nil, "ARTWORK") tex:SetAllPoints() tex:SetTexture(style.tex) -- 选中边框 local border = CreateFrame("Frame", nil, preview) border:SetPoint("TOPLEFT", preview, "TOPLEFT", -2, 2) border:SetPoint("BOTTOMRIGHT", preview, "BOTTOMRIGHT", 2, -2) border:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 2, }) border:Hide() styleBorders[idx] = border -- 样式名标签 local label = preview:CreateFontString(nil, "OVERLAY") label:SetFont(font, 10, "OUTLINE") label:SetPoint("TOP", preview, "BOTTOM", 0, -4) label:SetText(style.label) label:SetTextColor(0.7, 0.65, 0.7) -- 点击选择 preview:EnableMouse(true) preview._sfStyleKey = style.key preview._sfIdx = idx preview:SetScript("OnMouseUp", function() SFramesDB.ActionBars.gryphonStyle = this._sfStyleKey for i, bd in ipairs(styleBorders) do if i == this._sfIdx then bd:Show() bd:SetBackdropBorderColor(1, 0.78, 0.2, 1) else bd:Hide() end end RefreshAB() end) -- 悬停高亮 preview:SetScript("OnEnter", function() local bd = styleBorders[this._sfIdx] if bd and not bd:IsShown() then bd:Show() bd:SetBackdropBorderColor(SOFT_THEME.panelBorder[1], SOFT_THEME.panelBorder[2], SOFT_THEME.panelBorder[3], 0.8) end end) preview:SetScript("OnLeave", function() local bd = styleBorders[this._sfIdx] local current = SFramesDB.ActionBars.gryphonStyle or "dragonflight" if bd and this._sfStyleKey ~= current then bd:Hide() end end) end -- 初始化选中边框 local currentStyle = SFramesDB.ActionBars.gryphonStyle or "dragonflight" for idx, style in ipairs(GRYPHON_STYLES_UI) do if style.key == currentStyle then styleBorders[idx]:Show() styleBorders[idx]:SetBackdropBorderColor(1, 0.78, 0.2, 1) end end -- Layout presets CreateLabel(abSection, "布局预设:", 14, -908, font, 11, 0.85, 0.75, 0.80) CreateDesc(abSection, "快速切换动作条排列方式。选择预设会重置动作条位置并应用对应的排列。", 14, -926, font, 480) local presetDefs = (SFrames.ActionBars and SFrames.ActionBars.PRESETS) or { { id = 1, name = "经典", desc = "底部堆叠 + 右侧竖栏" }, { id = 2, name = "宽屏", desc = "左4x3 + 底部堆叠 + 右4x3" }, { id = 3, name = "堆叠", desc = "全部堆叠于底部中央" }, } local presetBtnW = 155 local presetBtnGap = 8 for pi = 1, 3 do local pdef = presetDefs[pi] local pLabel = pdef and pdef.name or ("方案" .. pi) local pId = pi local px = 14 + (pi - 1) * (presetBtnW + presetBtnGap) CreateButton(abSection, pLabel, px, -946, presetBtnW, 24, function() if SFrames.ActionBars and SFrames.ActionBars.ApplyPreset then SFrames.ActionBars:ApplyPreset(pId) RefreshAB() end end) end for pi = 1, 3 do local pdef = presetDefs[pi] local pdesc = pdef and pdef.desc or "" local px = 14 + (pi - 1) * (presetBtnW + presetBtnGap) CreateDesc(abSection, pdesc, px, -976, font, presetBtnW) end CreateLabel(abSection, "动作条位置:", 14, -1000, font, 11, 0.85, 0.75, 0.80) CreateDesc(abSection, "使用 /nui layout 或右键聊天框 Nanami 标题进入布局模式调整动作条位置", 14, -1018, font, 480) CreateLabel(abSection, "提示:启用/禁用动作条接管需要 /reload 才能生效。", 14, -1038, font, 10, 1, 0.92, 0.38) -- ── 额外动作条 ────────────────────────────────────────────── local function RefreshEB() if SFrames.ExtraBar then local edb = SFrames.ExtraBar:GetDB() if edb.enable then if not SFrames.ExtraBar.holder then SFrames.ExtraBar:Enable() else SFrames.ExtraBar:ApplyConfig() end else SFrames.ExtraBar:Disable() end end end local ebSection = CreateSection(root, "额外动作条", 8, -1056, 520, 310, font) table.insert(controls, CreateCheckBox(ebSection, "启用额外动作条(独立面板,使用空闲动作槽位)", 12, -30, function() return SFramesDB.ExtraBar.enable == true end, function(checked) SFramesDB.ExtraBar.enable = checked if checked then if SFrames.ExtraBar then SFrames.ExtraBar:Enable() end else if SFrames.ExtraBar then SFrames.ExtraBar:Disable() end end end, function() end )) table.insert(controls, CreateSlider(ebSection, "按钮总数", 14, -62, 150, 1, 48, 1, function() return SFramesDB.ExtraBar.buttonCount or 12 end, function(value) SFramesDB.ExtraBar.buttonCount = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshEB() end )) table.insert(controls, CreateSlider(ebSection, "每行按钮数", 180, -62, 150, 1, 12, 1, function() return SFramesDB.ExtraBar.perRow or 12 end, function(value) SFramesDB.ExtraBar.perRow = value end, function(v) local n = math.floor(v + 0.5); if n == 12 then return "12 (满)" else return tostring(n) end end, function() RefreshEB() end )) table.insert(controls, CreateSlider(ebSection, "按钮尺寸", 14, -102, 150, 20, 60, 1, function() return SFramesDB.ExtraBar.buttonSize or 36 end, function(value) SFramesDB.ExtraBar.buttonSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshEB() end )) table.insert(controls, CreateSlider(ebSection, "按钮间距", 180, -102, 150, 0, 10, 1, function() return SFramesDB.ExtraBar.buttonGap or 2 end, function(value) SFramesDB.ExtraBar.buttonGap = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshEB() end )) table.insert(controls, CreateSlider(ebSection, "对齐方式", 14, -142, 150, 1, 3, 1, function() local a = SFramesDB.ExtraBar.align or "center" if a == "left" then return 1 elseif a == "right" then return 3 else return 2 end end, function(value) local n = math.floor(value + 0.5) if n == 1 then SFramesDB.ExtraBar.align = "left" elseif n == 3 then SFramesDB.ExtraBar.align = "right" else SFramesDB.ExtraBar.align = "center" end end, function(v) local n = math.floor(v + 0.5); if n == 1 then return "左对齐" elseif n == 3 then return "右对齐" else return "居中" end end, function() RefreshEB() end )) table.insert(controls, CreateSlider(ebSection, "透明度", 180, -142, 150, 0.1, 1.0, 0.05, function() return SFramesDB.ExtraBar.alpha or 1.0 end, function(value) SFramesDB.ExtraBar.alpha = value end, function(v) return string.format("%.0f%%", v * 100) end, function() RefreshEB() end )) table.insert(controls, CreateSlider(ebSection, "起始槽位(页7=73)", 14, -182, 320, 73, 109, 1, function() return SFramesDB.ExtraBar.startSlot or 73 end, function(value) SFramesDB.ExtraBar.startSlot = value end, function(v) local n = math.floor(v + 0.5); local pg = math.floor((n - 1) / 12) + 1; local idx = math.fmod(n - 1, 12) + 1; return n .. " (页" .. pg .. "-" .. idx .. ")" end, function() RefreshEB() end )) table.insert(controls, CreateCheckBox(ebSection, "显示快捷键", 12, -220, function() return SFramesDB.ExtraBar.showHotkey ~= false end, function(checked) SFramesDB.ExtraBar.showHotkey = checked end, function() RefreshEB() end )) table.insert(controls, CreateCheckBox(ebSection, "显示堆叠数", 200, -220, function() return SFramesDB.ExtraBar.showCount ~= false end, function(checked) SFramesDB.ExtraBar.showCount = checked end, function() RefreshEB() end )) table.insert(controls, CreateCheckBox(ebSection, "按钮圆角", 12, -248, function() return SFramesDB.ExtraBar.buttonRounded == true end, function(checked) SFramesDB.ExtraBar.buttonRounded = checked end, function() RefreshEB() end )) table.insert(controls, CreateCheckBox(ebSection, "按钮内阴影", 200, -248, function() return SFramesDB.ExtraBar.buttonInnerShadow == true end, function(checked) SFramesDB.ExtraBar.buttonInnerShadow = checked end, function() RefreshEB() end )) CreateDesc(ebSection, "使用布局模式 (/nui layout) 调整额外动作条位置。默认槽位 73-84 (页7) 对所有职业安全。", 14, -280, font, 480) uiScroll:UpdateRange() self.actionBarControls = controls self.actionBarScroll = uiScroll end function SFrames.ConfigUI:BuildKeybindsPage() local font = SFrames:GetFont() local page = self.keybindsPage local controls = {} local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 660) local root = uiScroll.child -- Quick keybind mode entry local bindSection = CreateSection(root, "动作条按键绑定", 8, -8, 520, 80, font) CreateButton(bindSection, "进入按键绑定模式", 14, -30, 160, 24, function() if SFrames.ActionBars and SFrames.ActionBars.EnterKeyBindMode then if self.frame then self.frame:Hide() end SFrames.ActionBars:EnterKeyBindMode() end end) CreateDesc(bindSection, "悬停动作条/姿态栏/宠物栏按钮,按下键盘键/鼠标键(3-5)/滚轮绑定。右键清除。ESC 退出。", 184, -32, font, 320) -- Profile management local kbSection = CreateSection(root, "按键绑定方案管理", 8, -98, 520, 520, font) CreateDesc(kbSection, "保存/加载所有按键绑定(包含动作条、移动、聊天、目标选择、菜单等全部设定),可在角色间共享方案。", 14, -28, font, 490) -- Profile list container local listHolder = CreateFrame("Frame", "SFramesKBMListHolder", kbSection) listHolder:SetWidth(496) listHolder:SetHeight(192) listHolder:SetPoint("TOPLEFT", kbSection, "TOPLEFT", 12, -56) listHolder:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 10, insets = { left = 3, right = 3, top = 3, bottom = 3 }, }) listHolder:SetBackdropColor(0.08, 0.06, 0.1, 0.85) listHolder:SetBackdropBorderColor(0.4, 0.35, 0.45, 0.8) local ROW_HEIGHT = 24 local MAX_VISIBLE = 8 local selectedProfile = nil local profileRows = {} local listScroll = CreateFrame("ScrollFrame", "SFramesKBMListScroll", listHolder) listScroll:SetPoint("TOPLEFT", listHolder, "TOPLEFT", 4, -4) listScroll:SetPoint("BOTTOMRIGHT", listHolder, "BOTTOMRIGHT", -4, 4) local listChild = CreateFrame("Frame", "SFramesKBMListChild", listScroll) listChild:SetWidth(488) listChild:SetHeight(ROW_HEIGHT * MAX_VISIBLE) listScroll:SetScrollChild(listChild) listScroll:EnableMouseWheel(true) listScroll:SetScript("OnMouseWheel", function() local cur = listScroll:GetVerticalScroll() or 0 local maxS = listChild:GetHeight() - listScroll:GetHeight() if maxS < 0 then maxS = 0 end local newVal = cur - arg1 * ROW_HEIGHT if newVal < 0 then newVal = 0 end if newVal > maxS then newVal = maxS end listScroll:SetVerticalScroll(newVal) end) local emptyLabel = listChild:CreateFontString(nil, "OVERLAY") emptyLabel:SetFont(font, 11, "OUTLINE") emptyLabel:SetPoint("CENTER", listChild, "CENTER", 0, 0) emptyLabel:SetText("暂无保存的方案") emptyLabel:SetTextColor(0.5, 0.5, 0.5) local function RefreshProfileList() local KBM = SFrames.KeyBindManager if not KBM then return end local list = KBM:GetProfileList() for _, row in ipairs(profileRows) do row:Hide() end if table.getn(list) == 0 then emptyLabel:Show() listChild:SetHeight(ROW_HEIGHT * MAX_VISIBLE) return end emptyLabel:Hide() local totalH = table.getn(list) * ROW_HEIGHT if totalH < ROW_HEIGHT * MAX_VISIBLE then totalH = ROW_HEIGHT * MAX_VISIBLE end listChild:SetHeight(totalH) for idx, name in ipairs(list) do local row = profileRows[idx] if not row then row = CreateFrame("Button", nil, listChild) row:SetWidth(480) row:SetHeight(ROW_HEIGHT) row:EnableMouse(true) row.bg = row:CreateTexture(nil, "BACKGROUND") row.bg:SetAllPoints() row.bg:SetTexture("Interface\\Buttons\\WHITE8X8") row.bg:SetVertexColor(0.15, 0.12, 0.18, 0) row.nameText = row:CreateFontString(nil, "OVERLAY") row.nameText:SetFont(font, 11, "OUTLINE") row.nameText:SetPoint("LEFT", row, "LEFT", 8, 0) row.nameText:SetWidth(200) row.nameText:SetJustifyH("LEFT") row.infoText = row:CreateFontString(nil, "OVERLAY") row.infoText:SetFont(font, 9, "OUTLINE") row.infoText:SetPoint("RIGHT", row, "RIGHT", -8, 0) row.infoText:SetWidth(240) row.infoText:SetJustifyH("RIGHT") row.infoText:SetTextColor(0.6, 0.6, 0.65) row.highlight = row:CreateTexture(nil, "HIGHLIGHT") row.highlight:SetAllPoints() row.highlight:SetTexture("Interface\\Buttons\\WHITE8X8") row.highlight:SetVertexColor(0.4, 0.35, 0.5, 0.2) row:SetScript("OnClick", function() selectedProfile = this.profileName RefreshProfileList() end) profileRows[idx] = row end row:SetPoint("TOPLEFT", listChild, "TOPLEFT", 0, -(idx - 1) * ROW_HEIGHT) row.profileName = name local info = KBM:GetProfileInfo(name) row.nameText:SetText(name) local infoStr = "" if info then infoStr = info.charName .. " | " .. info.count .. " 条绑定" if info.timestamp and info.timestamp > 0 then local d = date and date("%m/%d %H:%M", info.timestamp) or "" if d ~= "" then infoStr = infoStr .. " | " .. d end end end row.infoText:SetText(infoStr) if selectedProfile == name then row.bg:SetVertexColor(0.3, 0.25, 0.45, 0.6) row.nameText:SetTextColor(1, 0.85, 0.4) else row.bg:SetVertexColor(0.15, 0.12, 0.18, (math.mod(idx, 2) == 0) and 0.25 or 0) row.nameText:SetTextColor(0.9, 0.88, 0.92) end row:Show() end end -- Buttons row 1: Save / Load / Delete / Rename local btnY1 = -256 CreateButton(kbSection, "保存当前绑定", 12, btnY1, 120, 24, function() local KBM = SFrames.KeyBindManager if not KBM then return end KBM.ShowInputDialog("|cffffcc00保存按键绑定方案|r", "", function(name) KBM:SaveProfile(name) RefreshProfileList() end) end) CreateButton(kbSection, "加载方案", 142, btnY1, 100, 24, function() local KBM = SFrames.KeyBindManager if not KBM or not selectedProfile then SFrames:Print("请先在列表中选择一个方案") return end KBM.ShowConfirmDialog( "确定要加载方案 |cffffd100" .. selectedProfile .. "|r 吗?\n当前所有按键绑定将被替换。", function() KBM:LoadProfile(selectedProfile) RefreshProfileList() end) end) CreateButton(kbSection, "删除", 252, btnY1, 80, 24, function() local KBM = SFrames.KeyBindManager if not KBM or not selectedProfile then SFrames:Print("请先在列表中选择一个方案") return end KBM.ShowConfirmDialog( "确定要删除方案 |cffffd100" .. selectedProfile .. "|r 吗?", function() KBM:DeleteProfile(selectedProfile) selectedProfile = nil RefreshProfileList() end) end) CreateButton(kbSection, "重命名", 342, btnY1, 90, 24, function() local KBM = SFrames.KeyBindManager if not KBM or not selectedProfile then SFrames:Print("请先在列表中选择一个方案") return end local old = selectedProfile KBM.ShowInputDialog("|cffffcc00重命名方案|r", old, function(newName) local ok, err = KBM:RenameProfile(old, newName) if ok then selectedProfile = newName else SFrames:Print("|cffff4444重命名失败: " .. (err or "") .. "|r") end RefreshProfileList() end) end) -- Buttons row 2: Export / Import local btnY2 = -288 CreateButton(kbSection, "导出当前绑定", 12, btnY2, 130, 24, function() local KBM = SFrames.KeyBindManager if KBM then KBM:ShowExportDialog() end end) CreateButton(kbSection, "导入绑定", 152, btnY2, 130, 24, function() local KBM = SFrames.KeyBindManager if KBM then KBM:ShowImportDialog() end end) CreateButton(kbSection, "导出选中方案", 292, btnY2, 130, 24, function() local KBM = SFrames.KeyBindManager if not KBM or not selectedProfile then SFrames:Print("请先在列表中选择一个方案") return end if not SFramesGlobalDB then SFramesGlobalDB = {} end if not SFramesGlobalDB.KeyBindProfiles then return end local profile = SFramesGlobalDB.KeyBindProfiles[selectedProfile] if profile and profile.bindings then local text = KBM:SerializeBindings(profile.bindings) KBM:ShowExportDialog() local ef = _G["SFramesKBMExport"] if ef and ef.edit then ef.edit:SetText(text) ef.edit:HighlightText() end end end) -- Info section CreateLabel(kbSection, "涵盖范围:", 14, -326, font, 10, 0.85, 0.75, 0.80) CreateDesc(kbSection, "移动按键 | 聊天按键 | 动作条快捷键 | 目标选择 | 界面面板 |\n宠物/姿态栏 | 镜头控制 | 多动作条 | 所有系统按键绑定", 14, -340, font, 490) CreateLabel(kbSection, "使用说明:", 14, -380, font, 10, 0.85, 0.75, 0.80) CreateDesc(kbSection, "1. 点击「保存当前绑定」将当前所有按键设定存为方案\n2. 在列表中选中方案后点击「加载方案」恢复\n3. 导出会将绑定编码为字符串,可安全复制分享给其他玩家\n4. 方案存储在全局变量中,所有角色共享", 14, -394, font, 490) CreateLabel(kbSection, "命令行:", 14, -452, font, 10, 0.85, 0.75, 0.80) CreateDesc(kbSection, "/nui keybinds save <名称> | /nui keybinds load <名称> | /nui keybinds list", 14, -466, font, 490) RefreshProfileList() self.RefreshKBMList = RefreshProfileList uiScroll:UpdateRange() self.keybindsControls = controls self.keybindsScroll = uiScroll end function SFrames.ConfigUI:BuildMinimapPage() local font = SFrames:GetFont() local page = self.minimapPage local controls = {} local function RefreshMinimap() if SFrames.Minimap and SFrames.Minimap.Refresh then SFrames.Minimap:Refresh() end end local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 800) local root = uiScroll.child -- ══════════════════════════════════════════════════════════════ -- 世界地图 & 迷雾揭示 (合并 · 置顶) -- ══════════════════════════════════════════════════════════════ local wmSection = CreateSection(root, "世界地图", 8, -8, 520, 260, font) table.insert(controls, CreateCheckBox(wmSection, "启用全新世界地图界面", 14, -34, function() return SFramesDB.WorldMap.enabled == true end, function(checked) SFramesDB.WorldMap.enabled = checked if checked then SFramesDB.Tweaks.worldMapWindow = false end end )) CreateDesc(wmSection, "Nanami主题窗口化地图,隐藏原生装饰(需重载UI)", 36, -50, font) table.insert(controls, CreateCheckBox(wmSection, "启用导航地图", 270, -34, function() return SFramesDB.WorldMap.nav and SFramesDB.WorldMap.nav.enabled == true end, function(checked) if type(SFramesDB.WorldMap.nav) ~= "table" then SFramesDB.WorldMap.nav = { enabled = false, width = 350, alpha = 0.70, locked = false } end SFramesDB.WorldMap.nav.enabled = checked if SFrames.WorldMap and SFrames.WorldMap.ToggleNav then if (checked and not (NanamiNavMap and NanamiNavMap:IsVisible())) or (not checked and NanamiNavMap and NanamiNavMap:IsVisible()) then SFrames.WorldMap:ToggleNav() end end end )) CreateDesc(wmSection, "实时导航地图,/nui nav | 滚轮缩放 | Ctrl+滚轮透明度", 292, -50, font) -- 迷雾揭示 (合并在同一 section) CreateLabel(wmSection, "── 迷雾揭示 ──", 14, -76, font, 11, 0.70, 0.60, 0.65) table.insert(controls, CreateCheckBox(wmSection, "启用地图迷雾揭示", 14, -96, function() return SFramesDB.MapReveal.enabled ~= false end, function(checked) SFramesDB.MapReveal.enabled = checked if SFrames.MapReveal and SFrames.MapReveal.Refresh then SFrames.MapReveal:Refresh() end end )) CreateDesc(wmSection, "在世界地图上显示未探索区域(变暗显示),需要 LibMapOverlayData", 36, -112, font) table.insert(controls, CreateSlider(wmSection, "未探索区域亮度", 14, -140, 220, 0.2, 1.0, 0.05, function() return SFramesDB.MapReveal.unexploredAlpha or 0.7 end, function(value) SFramesDB.MapReveal.unexploredAlpha = value end, function(v) return string.format("%.0f%%", v * 100) end, function(value) if SFrames.MapReveal and SFrames.MapReveal.SetAlpha then SFrames.MapReveal:SetAlpha(value) end end )) CreateButton(wmSection, "扫描所有地图", 270, -148, 120, 24, function() if SFrames.MapReveal and SFrames.MapReveal.ScanAllMaps then SFrames.MapReveal:ScanAllMaps() else SFrames:Print("MapReveal 模块不可用") end end) CreateDesc(wmSection, "遍历所有大陆区域,发现新地图并自动补充迷雾数据", 292, -174, font) CreateButton(wmSection, "查看统计", 400, -148, 80, 24, function() if SFrames.MapReveal and SFrames.MapReveal.ShowStats then SFrames.MapReveal:ShowStats() else SFrames:Print("MapReveal 模块不可用") end end) CreateButton(wmSection, "导出数据", 488, -148, 80, 24, function() if SFrames.MapReveal and SFrames.MapReveal.ExportScannedData then SFrames.MapReveal:ExportScannedData() else SFrames:Print("MapReveal 模块不可用") end end) CreateLabel(wmSection, "浏览世界地图时自动发现并持久化覆盖层数据,扫描可批量发现已探索区域。", 14, -200, font, 10, 0.6, 0.6, 0.65) CreateDesc(wmSection, "命令: /nui mapscan | /nui mapstats | /nui mapreveal 切换", 14, -216, font) -- ══════════════════════════════════════════════════════════════ -- 小地图 (移至下方) -- ══════════════════════════════════════════════════════════════ local mmSection = CreateSection(root, "小地图", 8, -278, 520, 480, font) table.insert(controls, CreateCheckBox(mmSection, "启用 Nanami 小地图皮肤", 14, -36, function() return SFramesDB.Minimap.enabled ~= false end, function(checked) SFramesDB.Minimap.enabled = checked end )) CreateDesc(mmSection, "关闭后恢复默认小地图,需要 /reload 生效", 36, -52, font) table.insert(controls, CreateSlider(mmSection, "缩放", 14, -80, 220, 0.6, 2.0, 0.05, function() return SFramesDB.Minimap.scale or 1.0 end, function(value) SFramesDB.Minimap.scale = value end, function(v) return string.format("%.0f%%", v * 100) end, function() RefreshMinimap() end )) table.insert(controls, CreateCheckBox(mmSection, "显示游戏时钟", 14, -134, function() return SFramesDB.Minimap.showClock ~= false end, function(checked) SFramesDB.Minimap.showClock = checked end )) CreateDesc(mmSection, "在小地图下方显示服务器时间", 36, -150, font) table.insert(controls, CreateCheckBox(mmSection, "显示玩家坐标", 270, -134, function() return SFramesDB.Minimap.showCoords ~= false end, function(checked) SFramesDB.Minimap.showCoords = checked end )) CreateDesc(mmSection, "在小地图圆内底部显示当前坐标", 292, -150, font) CreateDesc(mmSection, "使用 /nui layout 或右键聊天框 Nanami 标题进入布局模式调整位置", 14, -182, font, 480) -- ── Shape selector (方形 / 圆形) ────────────────────────────── CreateLabel(mmSection, "地图形状:", 14, -210, font, 11, 0.85, 0.75, 0.80) local shapeKeys = {"square1", "square2", "circle"} local shapeNames = {"方形·金", "方形·暗", "圆形"} local shapeBtns = {} local shapeLbls = {} for i = 1, 3 do local btn = CreateFrame("Button", nil, mmSection) btn:SetWidth(72) btn:SetHeight(22) btn:SetPoint("TOPLEFT", mmSection, "TOPLEFT", 100 + (i - 1) * 80, -208) btn:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 8, insets = { left = 2, right = 2, top = 2, bottom = 2 }, }) local lbl = btn:CreateFontString(nil, "OVERLAY") lbl:SetFont(font, 10, "OUTLINE") lbl:SetPoint("CENTER", btn, "CENTER", 0, 0) lbl:SetText(shapeNames[i]) btn._sfShapeKey = shapeKeys[i] shapeBtns[i] = btn shapeLbls[i] = lbl end CreateLabel(mmSection, "提示: 缩放、位置和样式修改后实时生效。", 14, -238, font, 10, 0.6, 0.6, 0.65) -- ── Circular style container (only visible when shape == "circle") ── local circleStyleFrame = CreateFrame("Frame", nil, mmSection) circleStyleFrame:SetPoint("TOPLEFT", mmSection, "TOPLEFT", 0, -256) circleStyleFrame:SetWidth(520) circleStyleFrame:SetHeight(220) CreateLabel(circleStyleFrame, "圆形边框样式:", 14, -4, font, 11, 0.85, 0.75, 0.80) local styles = SFrames.Minimap and SFrames.Minimap.MAP_STYLES or {} local autoBtn = CreateFrame("Button", nil, circleStyleFrame) autoBtn:SetWidth(100) autoBtn:SetHeight(18) autoBtn:SetPoint("TOPLEFT", circleStyleFrame, "TOPLEFT", 130, -2) autoBtn:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 8, insets = { left = 2, right = 2, top = 2, bottom = 2 }, }) local autoLabel = autoBtn:CreateFontString(nil, "OVERLAY") autoLabel:SetFont(font, 10, "OUTLINE") autoLabel:SetPoint("CENTER", autoBtn, "CENTER", 0, 0) autoLabel:SetText("自动(匹配职业)") local function UpdateAutoBtn(isAuto) if isAuto then autoBtn:SetBackdropColor(0.2, 0.4, 0.2, 0.9) autoBtn:SetBackdropBorderColor(0.4, 0.8, 0.3, 1) autoLabel:SetTextColor(0.5, 1, 0.5) else autoBtn:SetBackdropColor(0.15, 0.15, 0.15, 0.7) autoBtn:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.6) autoLabel:SetTextColor(0.6, 0.6, 0.6) end end local styleBorders = {} local cols = 5 local cellW = 56 local cellH = 56 local gapX = 10 local gapY = 18 local styleStartX = 14 local styleY = -26 local function HighlightSelection() local current = SFramesDB.Minimap.mapStyle or "auto" local isAuto = (current == "auto") UpdateAutoBtn(isAuto) for i, bd in ipairs(styleBorders) do if (not isAuto) and styles[i] and styles[i].key == current then bd:Show() bd:SetBackdropBorderColor(1, 0.78, 0.2, 1) else bd:Hide() end end end for idx, style in ipairs(styles) do local col = math.mod(idx - 1, cols) local row = math.floor((idx - 1) / cols) local xOff = styleStartX + col * (cellW + gapX) local yOff = styleY - row * (cellH + gapY) local preview = CreateFrame("Frame", nil, circleStyleFrame) preview:SetWidth(cellW) preview:SetHeight(cellH) preview:SetPoint("TOPLEFT", circleStyleFrame, "TOPLEFT", xOff, yOff) local bg = preview:CreateTexture(nil, "BACKGROUND") bg:SetAllPoints() bg:SetTexture(0, 0, 0, 0.4) local ptex = preview:CreateTexture(nil, "ARTWORK") ptex:SetAllPoints() ptex:SetTexture(style.tex) local border = CreateFrame("Frame", nil, preview) border:SetPoint("TOPLEFT", preview, "TOPLEFT", -2, 2) border:SetPoint("BOTTOMRIGHT", preview, "BOTTOMRIGHT", 2, -2) border:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 2 }) border:Hide() styleBorders[idx] = border local label = preview:CreateFontString(nil, "OVERLAY") label:SetFont(font, 9, "OUTLINE") label:SetPoint("TOP", preview, "BOTTOM", 0, -2) label:SetText(style.label) label:SetTextColor(0.7, 0.65, 0.7) preview:EnableMouse(true) preview._sfStyleKey = style.key preview._sfIdx = idx preview:SetScript("OnMouseUp", function() SFramesDB.Minimap.mapStyle = this._sfStyleKey HighlightSelection() RefreshMinimap() end) preview:SetScript("OnEnter", function() local bd = styleBorders[this._sfIdx] if bd and not bd:IsShown() then bd:Show() bd:SetBackdropBorderColor(SOFT_THEME.panelBorder[1], SOFT_THEME.panelBorder[2], SOFT_THEME.panelBorder[3], 0.8) end end) preview:SetScript("OnLeave", function() local bd = styleBorders[this._sfIdx] local current = SFramesDB.Minimap.mapStyle or "auto" if bd and this._sfStyleKey ~= current then bd:Hide() end end) end autoBtn:SetScript("OnClick", function() SFramesDB.Minimap.mapStyle = "auto" HighlightSelection() RefreshMinimap() end) HighlightSelection() -- Shape UI update: highlight active shape button, toggle circular style visibility local function UpdateShapeUI() local current = SFramesDB.Minimap.mapShape or "square1" for i = 1, 3 do if shapeKeys[i] == current then shapeBtns[i]:SetBackdropColor(0.2, 0.4, 0.2, 0.9) shapeBtns[i]:SetBackdropBorderColor(0.4, 0.8, 0.3, 1) shapeLbls[i]:SetTextColor(0.5, 1, 0.5) else shapeBtns[i]:SetBackdropColor(0.15, 0.15, 0.15, 0.7) shapeBtns[i]:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.6) shapeLbls[i]:SetTextColor(0.6, 0.6, 0.6) end end if current == "circle" then circleStyleFrame:Show() else circleStyleFrame:Hide() end end for i = 1, 3 do shapeBtns[i]:SetScript("OnClick", function() SFramesDB.Minimap.mapShape = this._sfShapeKey UpdateShapeUI() RefreshMinimap() end) end UpdateShapeUI() uiScroll:UpdateRange() self.minimapControls = controls self.minimapScroll = uiScroll end -------------------------------------------------------------------------------- -- Buff / Debuff Page (standalone tab) -------------------------------------------------------------------------------- function SFrames.ConfigUI:BuildBuffPage() local font = SFrames:GetFont() local page = self.buffPage local controls = {} local function RefreshBuffs() if SFrames.MinimapBuffs and SFrames.MinimapBuffs.Refresh then SFrames.MinimapBuffs:Refresh() end end local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 580) local root = uiScroll.child local buffSection = CreateSection(root, "Buff / Debuff 栏", 8, -8, 520, 480, font) -- Row 1: enable + show timer table.insert(controls, CreateCheckBox(buffSection, "启用 Buff / Debuff 栏", 14, -36, function() return SFramesDB.MinimapBuffs.enabled ~= false end, function(checked) SFramesDB.MinimapBuffs.enabled = checked end )) CreateDesc(buffSection, "替代暴雪默认 Buff 栏显示(需 /reload 生效)", 36, -52, font) table.insert(controls, CreateCheckBox(buffSection, "显示时间文本", 270, -36, function() return SFramesDB.MinimapBuffs.showTimer ~= false end, function(checked) SFramesDB.MinimapBuffs.showTimer = checked RefreshBuffs() end )) CreateDesc(buffSection, "图标下方显示剩余时间,永久 Buff 显示 N/A", 292, -52, font) -- Row 2: show debuffs table.insert(controls, CreateCheckBox(buffSection, "显示 Debuff", 14, -72, function() return SFramesDB.MinimapBuffs.showDebuffs ~= false end, function(checked) SFramesDB.MinimapBuffs.showDebuffs = checked RefreshBuffs() end )) CreateDesc(buffSection, "在 Buff 栏下方显示 Debuff(边框按类型着色)", 36, -88, font) -- Row 3: sliders - icon sizes table.insert(controls, CreateSlider(buffSection, "Buff 图标大小", 14, -114, 220, 20, 50, 1, function() return SFramesDB.MinimapBuffs.iconSize or 30 end, function(value) SFramesDB.MinimapBuffs.iconSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshBuffs() end )) table.insert(controls, CreateSlider(buffSection, "Debuff 图标大小", 270, -114, 220, 20, 50, 1, function() return SFramesDB.MinimapBuffs.debuffIconSize or 30 end, function(value) SFramesDB.MinimapBuffs.debuffIconSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshBuffs() end )) -- Row 4: per-row + spacing table.insert(controls, CreateSlider(buffSection, "每行图标数", 14, -176, 220, 4, 16, 1, function() return SFramesDB.MinimapBuffs.iconsPerRow or 8 end, function(value) SFramesDB.MinimapBuffs.iconsPerRow = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshBuffs() end )) table.insert(controls, CreateSlider(buffSection, "间距", 270, -176, 220, 0, 8, 1, function() return SFramesDB.MinimapBuffs.spacing or 2 end, function(value) SFramesDB.MinimapBuffs.spacing = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshBuffs() end )) table.insert(controls, CreateSlider(buffSection, "背景透明度", 14, -238, 220, 0, 1.0, 0.05, function() return SFramesDB.MinimapBuffs.bgAlpha or 0.92 end, function(value) SFramesDB.MinimapBuffs.bgAlpha = value RefreshBuffs() end, function(v) return string.format("%.0f%%", v * 100) end )) -- Row 5: grow direction + anchor CreateLabel(buffSection, "增长方向:", 14, -300, font, 11, 0.85, 0.75, 0.80) CreateButton(buffSection, "向左", 100, -300, 70, 22, function() SFramesDB.MinimapBuffs.growDirection = "LEFT" RefreshBuffs() end) CreateButton(buffSection, "向右", 176, -300, 70, 22, function() SFramesDB.MinimapBuffs.growDirection = "RIGHT" RefreshBuffs() end) CreateLabel(buffSection, "锚点位置:", 270, -300, font, 11, 0.85, 0.75, 0.80) local posLabels = { TOPRIGHT = "右上", TOPLEFT = "左上", BOTTOMRIGHT = "右下", BOTTOMLEFT = "左下" } local posKeys = { "TOPRIGHT", "TOPLEFT", "BOTTOMRIGHT", "BOTTOMLEFT" } local posStartX = 356 for _, key in ipairs(posKeys) do CreateButton(buffSection, posLabels[key], posStartX, -300, 40, 22, function() SFramesDB.MinimapBuffs.position = key RefreshBuffs() end) posStartX = posStartX + 42 end -- Row 6: position hint CreateDesc(buffSection, "使用 /nui layout 进入布局模式调整 Buff 栏位置", 14, -346, font, 480) -- Row 7: action buttons CreateButton(buffSection, "重置默认位置", 14, -370, 120, 24, function() SFramesDB.MinimapBuffs.offsetX = 0 SFramesDB.MinimapBuffs.offsetY = 0 SFramesDB.MinimapBuffs.position = "TOPRIGHT" SFramesDB.MinimapBuffs.growDirection = "LEFT" if SFramesDB.Positions then SFramesDB.Positions["MinimapBuffs"] = nil end if SFramesDB.Positions then SFramesDB.Positions["MinimapDebuffs"] = nil end RefreshBuffs() end) local simBtn simBtn = CreateButton(buffSection, "模拟预览", 142, -370, 100, 24, function() if not SFrames.MinimapBuffs then return end if SFrames.MinimapBuffs._simulating then SFrames.MinimapBuffs:StopSimulation() simBtn:SetText("模拟预览") else SFrames.MinimapBuffs:SimulateBuffs() simBtn:SetText("停止模拟") end end) -- Row 8: tips CreateLabel(buffSection, "模拟预览:显示假 Buff / Debuff 以预览布局效果,不影响实际状态。", 14, -404, font, 9, 0.65, 0.58, 0.62) CreateLabel(buffSection, "Debuff 边框颜色: |cff3399ff魔法|r |cff9900ff诅咒|r |cff996600疾病|r |cff009900毒药|r |cffcc0000物理|r", 14, -420, font, 9, 0.65, 0.58, 0.62) CreateLabel(buffSection, "提示:启用/禁用 Buff 栏需要 /reload 才能生效。其他调整实时生效。", 14, -444, font, 10, 0.6, 0.6, 0.65) uiScroll:UpdateRange() self.buffControls = controls self.buffScroll = uiScroll end function SFrames.ConfigUI:ShowFramesSubPage(mode) local subMode = mode if subMode ~= "player" and subMode ~= "target" and subMode ~= "focus" and subMode ~= "party" and subMode ~= "raid" then subMode = self.activeFrameSubPage or "player" end self.activeFrameSubPage = subMode self:EnsurePage(subMode) self.playerPage:Hide() self.targetPage:Hide() self.focusPage:Hide() self.partyPage:Hide() self.raidPage:Hide() local allSubTabs = { self.framesPlayerTab, self.framesTargetTab, self.framesFocusTab, self.framesPartyTab, self.framesRaidTab } for _, tab in ipairs(allSubTabs) do if tab then tab.sfSoftActive = false tab:Enable() tab:RefreshVisual() end end local activeTab, controls, scroll, titleSuffix if subMode == "target" then self.targetPage:Show() activeTab = self.framesTargetTab controls = self.targetControls scroll = self.targetScroll titleSuffix = "目标框体" elseif subMode == "focus" then self.focusPage:Show() activeTab = self.framesFocusTab controls = self.focusControls scroll = self.focusScroll titleSuffix = "焦点框体" elseif subMode == "party" then self.partyPage:Show() activeTab = self.framesPartyTab controls = self.partyControls scroll = self.partyScroll titleSuffix = "小队框体" elseif subMode == "raid" then self.raidPage:Show() activeTab = self.framesRaidTab controls = self.raidControls scroll = self.raidScroll titleSuffix = "团队框体" else self.playerPage:Show() activeTab = self.framesPlayerTab controls = self.playerControls scroll = self.playerScroll titleSuffix = "玩家框体" end if activeTab then activeTab.sfSoftActive = true activeTab:Disable() activeTab:RefreshVisual() end self.title:SetText("Nanami-UI 设置 - 框体设置 / " .. titleSuffix) self:RefreshControls(controls) if scroll and scroll.Reset then scroll:Reset() end end function SFrames.ConfigUI:ShowPage(mode) local requestedMode = mode or "ui" local pageMode = requestedMode if requestedMode == "player" or requestedMode == "target" or requestedMode == "focus" or requestedMode == "party" or requestedMode == "raid" then pageMode = "frames" end if requestedMode == "frames" then requestedMode = self.activeFrameSubPage or (SFramesDB and SFramesDB.configLastFrameSubPage) or "player" end if requestedMode == "player" or requestedMode == "target" or requestedMode == "focus" or requestedMode == "party" or requestedMode == "raid" then if SFramesDB then SFramesDB.configLastFrameSubPage = requestedMode SFramesDB.configLastPage = requestedMode end elseif SFramesDB then SFramesDB.configLastPage = pageMode end self.activePage = pageMode self.uiPage:Hide() self.framesPage:Hide() self.playerPage:Hide() self.targetPage:Hide() self.focusPage:Hide() self.bagPage:Hide() self.raidPage:Hide() self.partyPage:Hide() self.charPage:Hide() self.actionBarPage:Hide() self.keybindsPage:Hide() self.minimapPage:Hide() self.buffPage:Hide() self.personalizePage:Hide() self.themePage:Hide() self.profilePage:Hide() local allTabs = { self.uiTab, self.framesTab, self.bagTab, self.charTab, self.actionBarTab, self.keybindsTab, self.minimapTab, self.buffTab, self.personalizeTab, self.themeTab, self.profileTab } for _, tab in ipairs(allTabs) do tab.sfSoftActive = false tab:Enable() tab:RefreshVisual() end self:EnsurePage(pageMode) mode = pageMode if mode == "frames" then self.framesPage:Show() self.framesTab.sfSoftActive = true self.framesTab:Disable() self.framesTab:RefreshVisual() self:ShowFramesSubPage(requestedMode) elseif mode == "target" then self.targetPage:Show() self.targetTab.sfSoftActive = true self.targetTab:Disable() self.targetTab:RefreshVisual() self.title:SetText("Nanami-UI 设置 - 目标框体") self:RefreshControls(self.targetControls) elseif mode == "focus" then self.focusPage:Show() self.focusTab.sfSoftActive = true self.focusTab:Disable() self.focusTab:RefreshVisual() self.title:SetText("Nanami-UI 设置 - 焦点框体") self:RefreshControls(self.focusControls) elseif mode == "bags" then self.bagPage:Show() self.bagTab.sfSoftActive = true self.bagTab:Disable() self.bagTab:RefreshVisual() self.title:SetText("Nanami-UI 设置 - 背包") self:RefreshControls(self.bagControls) if self.bagScroll and self.bagScroll.Reset then self.bagScroll:Reset() end elseif mode == "raid" then self.raidPage:Show() self.raidTab.sfSoftActive = true self.raidTab:Disable() self.raidTab:RefreshVisual() self.title:SetText("Nanami-UI 设置 - 团队") self:RefreshControls(self.raidControls) if self.raidScroll and self.raidScroll.Reset then self.raidScroll:Reset() end elseif mode == "party" then self.partyPage:Show() self.partyTab.sfSoftActive = true self.partyTab:Disable() self.partyTab:RefreshVisual() self.title:SetText("Nanami-UI 设置 - 小队") self:RefreshControls(self.partyControls) if self.partyScroll and self.partyScroll.Reset then self.partyScroll:Reset() end elseif mode == "char" then self.charPage:Show() self.charTab.sfSoftActive = true self.charTab:Disable() self.charTab:RefreshVisual() self.title:SetText("Nanami-UI 设置 - 人物面板") self:RefreshControls(self.charControls) if self.charScroll and self.charScroll.Reset then self.charScroll:Reset() end elseif mode == "actionbar" then self.actionBarPage:Show() self.actionBarTab.sfSoftActive = true self.actionBarTab:Disable() self.actionBarTab:RefreshVisual() self.title:SetText("Nanami-UI 设置 - 动作条") self:RefreshControls(self.actionBarControls) if self.actionBarScroll and self.actionBarScroll.Reset then self.actionBarScroll:Reset() end elseif mode == "keybinds" then self.keybindsPage:Show() self.keybindsTab.sfSoftActive = true self.keybindsTab:Disable() self.keybindsTab:RefreshVisual() self.title:SetText("Nanami-UI 设置 - 按键绑定方案") self:RefreshControls(self.keybindsControls) if self.keybindsScroll and self.keybindsScroll.Reset then self.keybindsScroll:Reset() end if self.RefreshKBMList then self.RefreshKBMList() end elseif mode == "minimap" then self.minimapPage:Show() self.minimapTab.sfSoftActive = true self.minimapTab:Disable() self.minimapTab:RefreshVisual() self.title:SetText("Nanami-UI 设置 - 地图设置") self:RefreshControls(self.minimapControls) if self.minimapScroll and self.minimapScroll.Reset then self.minimapScroll:Reset() end elseif mode == "buff" then self.buffPage:Show() self.buffTab.sfSoftActive = true self.buffTab:Disable() self.buffTab:RefreshVisual() self.title:SetText("Nanami-UI 设置 - Buff / Debuff") self:RefreshControls(self.buffControls) if self.buffScroll and self.buffScroll.Reset then self.buffScroll:Reset() end elseif mode == "personalize" then self.personalizePage:Show() self.personalizeTab.sfSoftActive = true self.personalizeTab:Disable() self.personalizeTab:RefreshVisual() self.title:SetText("Nanami-UI 设置 - 个性化") self:RefreshControls(self.personalizeControls) if self.personalizeScroll and self.personalizeScroll.Reset then self.personalizeScroll:Reset() end elseif mode == "theme" then self.themePage:Show() self.themeTab.sfSoftActive = true self.themeTab:Disable() self.themeTab:RefreshVisual() self.title:SetText("Nanami-UI 设置 - 主题") self:RefreshControls(self.themeControls) if self.themeScroll and self.themeScroll.Reset then self.themeScroll:Reset() end elseif mode == "profile" then self.profilePage:Show() self.profileTab.sfSoftActive = true self.profileTab:Disable() self.profileTab:RefreshVisual() self.title:SetText("Nanami-UI 设置 - 配置管理") if self.profileScroll and self.profileScroll.Reset then self.profileScroll:Reset() end if self.RefreshProfileSummary then self:RefreshProfileSummary() end else self.uiPage:Show() self.uiTab.sfSoftActive = true self.uiTab:Disable() self.uiTab:RefreshVisual() self.title:SetText("Nanami-UI 设置 - 界面") self:RefreshControls(self.uiControls) if self.uiScroll and self.uiScroll.Reset then self.uiScroll:Reset() end end end function SFrames.ConfigUI:EnsureFrame() if self.frame then return end local font = SFrames:GetFont() local panel = CreateFrame("Frame", "SFramesConfigPanel", UIParent) panel:SetWidth(PANEL_WIDTH) panel:SetHeight(PANEL_HEIGHT) panel:SetPoint("CENTER", UIParent, "CENTER", 0, 0) panel:SetMovable(true) panel:EnableMouse(true) panel:SetClampedToScreen(true) panel:SetToplevel(true) panel:SetFrameStrata("TOOLTIP") panel:SetFrameLevel(100) panel:RegisterForDrag("LeftButton") panel:SetScript("OnDragStart", function() this:StartMoving() end) panel:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) tinsert(UISpecialFrames, "SFramesConfigPanel") -- Apply the rounded "Roll UI" textured backdrop to the main settings panel if SFrames and SFrames.CreateRoundBackdrop then SFrames:CreateRoundBackdrop(panel) panel.sfSoftBackdrop = true else EnsureSoftBackdrop(panel) end if panel.SetBackdropColor then panel:SetBackdropColor(SOFT_THEME.panelBg[1], SOFT_THEME.panelBg[2], SOFT_THEME.panelBg[3], SOFT_THEME.panelBg[4]) end if panel.SetBackdropBorderColor then panel:SetBackdropBorderColor(SOFT_THEME.panelBorder[1], SOFT_THEME.panelBorder[2], SOFT_THEME.panelBorder[3], SOFT_THEME.panelBorder[4]) end local title = panel:CreateFontString(nil, "OVERLAY") title:SetFont(font, 14, "OUTLINE") title:SetPoint("TOP", panel, "TOP", 0, -14) title:SetJustifyH("CENTER") title:SetTextColor(SOFT_THEME.title[1], SOFT_THEME.title[2], SOFT_THEME.title[3]) title:SetText("Nanami-UI 设置") local titleIco = SFrames:CreateIcon(panel, "logo", 18) titleIco:SetDrawLayer("OVERLAY") titleIco:SetPoint("RIGHT", title, "LEFT", -5, 0) titleIco:SetVertexColor(SOFT_THEME.title[1], SOFT_THEME.title[2], SOFT_THEME.title[3]) local divider = panel:CreateTexture(nil, "ARTWORK") divider:SetWidth(1) divider:SetPoint("TOPLEFT", panel, "TOPLEFT", 120, -40) divider:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 120, 10) divider:SetTexture(0.44, 0.42, 0.5, 0.9) local closeBtn = CreateFrame("Button", "SFramesConfigCloseBtn", panel) closeBtn:SetWidth(20) closeBtn:SetHeight(20) closeBtn:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -8, -8) closeBtn:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 14, insets = { left = 3, right = 3, top = 3, bottom = 3 }, }) local _cA = SOFT_THEME closeBtn:SetBackdropColor(_cA.buttonDownBg[1], _cA.buttonDownBg[2], _cA.buttonDownBg[3], _cA.buttonDownBg[4] or 0.85) closeBtn:SetBackdropBorderColor(_cA.btnBorder[1], _cA.btnBorder[2], _cA.btnBorder[3], _cA.btnBorder[4] or 0.7) local closeIco = SFrames:CreateIcon(closeBtn, "close", 14) closeIco:SetDrawLayer("OVERLAY") closeIco:SetPoint("CENTER", closeBtn, "CENTER", 0, 0) closeIco:SetVertexColor(1, 0.7, 0.7) closeBtn:SetScript("OnClick", function() SFrames.ConfigUI.frame:Hide() end) closeBtn:SetScript("OnEnter", function() this:SetBackdropColor(_cA.btnHoverBg[1], _cA.btnHoverBg[2], _cA.btnHoverBg[3], _cA.btnHoverBg[4] or 0.95) this:SetBackdropBorderColor(_cA.btnHoverBd[1], _cA.btnHoverBd[2], _cA.btnHoverBd[3], _cA.btnHoverBd[4] or 0.9) end) closeBtn:SetScript("OnLeave", function() this:SetBackdropColor(_cA.buttonDownBg[1], _cA.buttonDownBg[2], _cA.buttonDownBg[3], _cA.buttonDownBg[4] or 0.85) this:SetBackdropBorderColor(_cA.btnBorder[1], _cA.btnBorder[2], _cA.btnBorder[3], _cA.btnBorder[4] or 0.7) end) local tabUI = CreateFrame("Button", "SFramesConfigTabUI", panel, "UIPanelButtonTemplate") tabUI:SetWidth(100) tabUI:SetHeight(28) tabUI:SetPoint("TOPLEFT", panel, "TOPLEFT", 10, -50) tabUI:SetText("界面设置") tabUI:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("ui") end) StyleButton(tabUI) AddBtnIcon(tabUI, "settings", nil, "left") local tabFrames = CreateFrame("Button", "SFramesConfigTabFrames", panel, "UIPanelButtonTemplate") tabFrames:SetWidth(100) tabFrames:SetHeight(28) tabFrames:SetPoint("TOP", tabUI, "BOTTOM", 0, -4) tabFrames:SetText("框体设置") tabFrames:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("frames") end) StyleButton(tabFrames) AddBtnIcon(tabFrames, "character", nil, "left") local tabPlayer = CreateFrame("Button", "SFramesConfigTabPlayer", panel, "UIPanelButtonTemplate") tabPlayer:SetWidth(100) tabPlayer:SetHeight(28) tabPlayer:SetPoint("TOP", tabFrames, "BOTTOM", 0, -4) tabPlayer:SetText("玩家框体") tabPlayer:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("player") end) StyleButton(tabPlayer) AddBtnIcon(tabPlayer, "character", nil, "left") tabPlayer:Hide() local tabTarget = CreateFrame("Button", "SFramesConfigTabTarget", panel, "UIPanelButtonTemplate") tabTarget:SetWidth(100) tabTarget:SetHeight(28) tabTarget:SetPoint("TOP", tabPlayer, "BOTTOM", 0, -4) tabTarget:SetText("目标框体") tabTarget:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("target") end) StyleButton(tabTarget) AddBtnIcon(tabTarget, "search", nil, "left") tabTarget:Hide() local tabFocus = CreateFrame("Button", "SFramesConfigTabFocus", panel, "UIPanelButtonTemplate") tabFocus:SetWidth(100) tabFocus:SetHeight(28) tabFocus:SetPoint("TOP", tabFrames, "BOTTOM", 0, -4) tabFocus:SetText("焦点框体") tabFocus:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("focus") end) StyleButton(tabFocus) AddBtnIcon(tabFocus, "search", nil, "left") tabFocus:Hide() local tabParty = CreateFrame("Button", "SFramesConfigTabParty", panel, "UIPanelButtonTemplate") tabParty:SetWidth(100) tabParty:SetHeight(28) tabParty:SetPoint("TOP", tabFocus, "BOTTOM", 0, -4) tabParty:SetText("小队框架") tabParty:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("party") end) StyleButton(tabParty) AddBtnIcon(tabParty, "friends", nil, "left") tabParty:Hide() local tabRaid = CreateFrame("Button", "SFramesConfigTabRaid", panel, "UIPanelButtonTemplate") tabRaid:SetWidth(100) tabRaid:SetHeight(28) tabRaid:SetPoint("TOP", tabParty, "BOTTOM", 0, -4) tabRaid:SetText("团队框架") tabRaid:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("raid") end) StyleButton(tabRaid) AddBtnIcon(tabRaid, "party", nil, "left") tabRaid:Hide() local tabBags = CreateFrame("Button", "SFramesConfigTabBags", panel, "UIPanelButtonTemplate") tabBags:SetWidth(100) tabBags:SetHeight(28) tabBags:SetPoint("TOP", tabFrames, "BOTTOM", 0, -4) tabBags:SetText("背包设置") tabBags:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("bags") end) StyleButton(tabBags) AddBtnIcon(tabBags, "backpack", nil, "left") local tabChar = CreateFrame("Button", "SFramesConfigTabChar", panel, "UIPanelButtonTemplate") tabChar:SetWidth(100) tabChar:SetHeight(28) tabChar:SetPoint("TOP", tabBags, "BOTTOM", 0, -4) tabChar:SetText("人物面板") tabChar:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("char") end) StyleButton(tabChar) AddBtnIcon(tabChar, "charsheet", nil, "left") local tabActionBar = CreateFrame("Button", "SFramesConfigTabActionBar", panel, "UIPanelButtonTemplate") tabActionBar:SetWidth(100) tabActionBar:SetHeight(28) tabActionBar:SetPoint("TOP", tabChar, "BOTTOM", 0, -4) tabActionBar:SetText("动作条") tabActionBar:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("actionbar") end) StyleButton(tabActionBar) AddBtnIcon(tabActionBar, "attack", nil, "left") local tabKeybinds = CreateFrame("Button", "SFramesConfigTabKeybinds", panel, "UIPanelButtonTemplate") tabKeybinds:SetWidth(100) tabKeybinds:SetHeight(28) tabKeybinds:SetPoint("TOP", tabActionBar, "BOTTOM", 0, -4) tabKeybinds:SetText("按键方案") tabKeybinds:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("keybinds") end) StyleButton(tabKeybinds) AddBtnIcon(tabKeybinds, "key", nil, "left") local tabMinimap = CreateFrame("Button", "SFramesConfigTabMinimap", panel, "UIPanelButtonTemplate") tabMinimap:SetWidth(100) tabMinimap:SetHeight(28) tabMinimap:SetPoint("TOP", tabKeybinds, "BOTTOM", 0, -4) tabMinimap:SetText("地图设置") tabMinimap:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("minimap") end) StyleButton(tabMinimap) AddBtnIcon(tabMinimap, "worldmap", nil, "left") local tabBuff = CreateFrame("Button", "SFramesConfigTabBuff", panel, "UIPanelButtonTemplate") tabBuff:SetWidth(100) tabBuff:SetHeight(28) tabBuff:SetPoint("TOP", tabMinimap, "BOTTOM", 0, -4) tabBuff:SetText("Buff栏") tabBuff:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("buff") end) StyleButton(tabBuff) AddBtnIcon(tabBuff, "buff", nil, "left") local tabPersonalize = CreateFrame("Button", "SFramesConfigTabPersonalize", panel, "UIPanelButtonTemplate") tabPersonalize:SetWidth(100) tabPersonalize:SetHeight(28) tabPersonalize:SetPoint("TOP", tabBuff, "BOTTOM", 0, -4) tabPersonalize:SetText("个性化") tabPersonalize:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("personalize") end) StyleButton(tabPersonalize) AddBtnIcon(tabPersonalize, "potion", nil, "left") local tabTheme = CreateFrame("Button", "SFramesConfigTabTheme", panel, "UIPanelButtonTemplate") tabTheme:SetWidth(100) tabTheme:SetHeight(28) tabTheme:SetPoint("TOP", tabPersonalize, "BOTTOM", 0, -4) tabTheme:SetText("主题设置") tabTheme:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("theme") end) StyleButton(tabTheme) AddBtnIcon(tabTheme, "star", nil, "left") local tabProfile = CreateFrame("Button", "SFramesConfigTabProfile", panel, "UIPanelButtonTemplate") tabProfile:SetWidth(100) tabProfile:SetHeight(28) tabProfile:SetPoint("TOP", tabTheme, "BOTTOM", 0, -4) tabProfile:SetText("配置") tabProfile:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("profile") end) StyleButton(tabProfile) AddBtnIcon(tabProfile, "save", nil, "left") local content = CreateFrame("Frame", "SFramesConfigContentMain", panel) content:SetWidth(PANEL_WIDTH - 140) content:SetHeight(PANEL_HEIGHT - 60) content:SetPoint("TOPLEFT", panel, "TOPLEFT", 130, -50) local uiPage = CreateFrame("Frame", "SFramesConfigUIPage", content) uiPage:SetAllPoints(content) local framesPage = CreateFrame("Frame", "SFramesConfigFramesPage", content) framesPage:SetAllPoints(content) local framesTabs = CreateFrame("Frame", "SFramesConfigFramesTabs", framesPage) framesTabs:SetWidth(548) framesTabs:SetHeight(30) framesTabs:SetPoint("TOPLEFT", framesPage, "TOPLEFT", 4, -4) local framesPlayerTab = CreateFrame("Button", "SFramesConfigFramesPlayerTab", framesTabs, "UIPanelButtonTemplate") framesPlayerTab:SetWidth(96) framesPlayerTab:SetHeight(24) framesPlayerTab:SetPoint("TOPLEFT", framesTabs, "TOPLEFT", 0, 0) framesPlayerTab:SetText("玩家") framesPlayerTab:SetScript("OnClick", function() SFrames.ConfigUI:ShowFramesSubPage("player") end) StyleButton(framesPlayerTab) local framesTargetTab = CreateFrame("Button", "SFramesConfigFramesTargetTab", framesTabs, "UIPanelButtonTemplate") framesTargetTab:SetWidth(96) framesTargetTab:SetHeight(24) framesTargetTab:SetPoint("LEFT", framesPlayerTab, "RIGHT", 6, 0) framesTargetTab:SetText("目标") framesTargetTab:SetScript("OnClick", function() SFrames.ConfigUI:ShowFramesSubPage("target") end) StyleButton(framesTargetTab) local framesFocusTab = CreateFrame("Button", "SFramesConfigFramesFocusTab", framesTabs, "UIPanelButtonTemplate") framesFocusTab:SetWidth(96) framesFocusTab:SetHeight(24) framesFocusTab:SetPoint("LEFT", framesTargetTab, "RIGHT", 6, 0) framesFocusTab:SetText("焦点") framesFocusTab:SetScript("OnClick", function() SFrames.ConfigUI:ShowFramesSubPage("focus") end) StyleButton(framesFocusTab) local framesPartyTab = CreateFrame("Button", "SFramesConfigFramesPartyTab", framesTabs, "UIPanelButtonTemplate") framesPartyTab:SetWidth(96) framesPartyTab:SetHeight(24) framesPartyTab:SetPoint("LEFT", framesFocusTab, "RIGHT", 6, 0) framesPartyTab:SetText("小队") framesPartyTab:SetScript("OnClick", function() SFrames.ConfigUI:ShowFramesSubPage("party") end) StyleButton(framesPartyTab) local framesRaidTab = CreateFrame("Button", "SFramesConfigFramesRaidTab", framesTabs, "UIPanelButtonTemplate") framesRaidTab:SetWidth(96) framesRaidTab:SetHeight(24) framesRaidTab:SetPoint("LEFT", framesPartyTab, "RIGHT", 6, 0) framesRaidTab:SetText("团队") framesRaidTab:SetScript("OnClick", function() SFrames.ConfigUI:ShowFramesSubPage("raid") end) StyleButton(framesRaidTab) local framesContent = CreateFrame("Frame", "SFramesConfigFramesContent", framesPage) framesContent:SetPoint("TOPLEFT", framesTabs, "BOTTOMLEFT", 0, -6) framesContent:SetPoint("BOTTOMRIGHT", framesPage, "BOTTOMRIGHT", 0, 0) local playerPage = CreateFrame("Frame", "SFramesConfigPlayerPage", framesContent) playerPage:SetAllPoints(framesContent) local targetPage = CreateFrame("Frame", "SFramesConfigTargetPage", framesContent) targetPage:SetAllPoints(framesContent) local focusPage = CreateFrame("Frame", "SFramesConfigFocusPage", framesContent) focusPage:SetAllPoints(framesContent) local bagPage = CreateFrame("Frame", "SFramesConfigBagPage", content) bagPage:SetAllPoints(content) local raidPage = CreateFrame("Frame", "SFramesConfigRaidPage", framesContent) raidPage:SetAllPoints(framesContent) local partyPage = CreateFrame("Frame", "SFramesConfigPartyPage", framesContent) partyPage:SetAllPoints(framesContent) local charPage = CreateFrame("Frame", "SFramesConfigCharPage", content) charPage:SetAllPoints(content) local actionBarPage = CreateFrame("Frame", "SFramesConfigActionBarPage", content) actionBarPage:SetAllPoints(content) local keybindsPage = CreateFrame("Frame", "SFramesConfigKeybindsPage", content) keybindsPage:SetAllPoints(content) local minimapPage = CreateFrame("Frame", "SFramesConfigMinimapPage", content) minimapPage:SetAllPoints(content) local buffPage = CreateFrame("Frame", "SFramesConfigBuffPage", content) buffPage:SetAllPoints(content) local personalizePage = CreateFrame("Frame", "SFramesConfigPersonalizePage", content) personalizePage:SetAllPoints(content) local themePage = CreateFrame("Frame", "SFramesConfigThemePage", content) themePage:SetAllPoints(content) local profilePage = CreateFrame("Frame", "SFramesConfigProfilePage", content) profilePage:SetAllPoints(content) self.frame = panel self.title = title self.uiTab = tabUI self.framesTab = tabFrames self.playerTab = tabPlayer self.targetTab = tabTarget self.focusTab = tabFocus self.bagTab = tabBags self.raidTab = tabRaid self.partyTab = tabParty self.charTab = tabChar self.actionBarTab = tabActionBar self.keybindsTab = tabKeybinds self.minimapTab = tabMinimap self.buffTab = tabBuff self.personalizeTab = tabPersonalize self.themeTab = tabTheme self.profileTab = tabProfile self.content = content self.uiPage = uiPage self.framesPage = framesPage self.playerPage = playerPage self.targetPage = targetPage self.focusPage = focusPage self.bagPage = bagPage self.raidPage = raidPage self.partyPage = partyPage self.charPage = charPage self.actionBarPage = actionBarPage self.keybindsPage = keybindsPage self.minimapPage = minimapPage self.buffPage = buffPage self.personalizePage = personalizePage self.themePage = themePage self.profilePage = profilePage self.framesPlayerTab = framesPlayerTab self.framesTargetTab = framesTargetTab self.framesFocusTab = framesFocusTab self.framesPartyTab = framesPartyTab self.framesRaidTab = framesRaidTab self.activeFrameSubPage = "player" local btnSaveReload = CreateFrame("Button", "SFramesConfigSaveReloadBtn", panel, "UIPanelButtonTemplate") btnSaveReload:SetWidth(100) btnSaveReload:SetHeight(26) btnSaveReload:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -15, 12) btnSaveReload:SetText("保存并重载") btnSaveReload:SetScript("OnClick", function() ReloadUI() end) StyleButton(btnSaveReload) AddBtnIcon(btnSaveReload, "save") local btnConfirm = CreateFrame("Button", "SFramesConfigConfirmBtn", panel, "UIPanelButtonTemplate") btnConfirm:SetWidth(80) btnConfirm:SetHeight(26) btnConfirm:SetPoint("RIGHT", btnSaveReload, "LEFT", -6, 0) btnConfirm:SetText("确认") btnConfirm:SetScript("OnClick", function() SFrames.ConfigUI.frame:Hide() end) StyleButton(btnConfirm) AddBtnIcon(btnConfirm, "close") self._pageBuilt = {} end function SFrames.ConfigUI:BuildPersonalizePage() local font = SFrames:GetFont() local page = self.personalizePage local controls = {} local personalizeScroll = CreateScrollArea(page, 4, -4, 548, 458, 950) local root = personalizeScroll.child -- ── 拾取窗口 ──────────────────────────────────────────────── local lootOptSection = CreateSection(root, "拾取窗口", 8, -8, 520, 56, font) table.insert(controls, CreateCheckBox(lootOptSection, "拾取窗口跟随鼠标", 14, -30, function() if type(SFramesDB.lootDisplay) ~= "table" then return false end return SFramesDB.lootDisplay.followCursor == true end, function(checked) if type(SFramesDB.lootDisplay) ~= "table" then SFramesDB.lootDisplay = {} end SFramesDB.lootDisplay.followCursor = checked end )) CreateDesc(lootOptSection, "勾选后拾取窗口显示在鼠标附近,否则显示在固定位置", 36, -46, font) -- ── 鼠标提示框 ──────────────────────────────────────────────── local tooltipSection = CreateSection(root, "鼠标提示框", 8, -74, 520, 180, font) CreateDesc(tooltipSection, "选择游戏提示框的显示位置方式(三选一)", 14, -28, font) local function RefreshTooltipMode() if SFrames.FloatingTooltip and SFrames.FloatingTooltip.ToggleAnchor then SFrames.FloatingTooltip:ToggleAnchor(SFramesDB.tooltipMode == "CUSTOM") end if SFrames.ConfigUI.tooltipCBs then for _, cb in ipairs(SFrames.ConfigUI.tooltipCBs) do cb:Refresh() end end end SFrames.ConfigUI.tooltipCBs = {} local cbDefault = CreateCheckBox(tooltipSection, "系统默认位置", 14, -46, function() return (SFramesDB.tooltipMode == "DEFAULT" or SFramesDB.tooltipMode == nil) end, function(checked) if checked then SFramesDB.tooltipMode = "DEFAULT" RefreshTooltipMode() end end ) table.insert(SFrames.ConfigUI.tooltipCBs, cbDefault) table.insert(controls, cbDefault) local cbCursor = CreateCheckBox(tooltipSection, "跟随鼠标(鼠标右下方)", 14, -72, function() return SFramesDB.tooltipMode == "CURSOR" end, function(checked) if checked then SFramesDB.tooltipMode = "CURSOR" RefreshTooltipMode() end end ) table.insert(SFrames.ConfigUI.tooltipCBs, cbCursor) table.insert(controls, cbCursor) local cbCustom = CreateCheckBox(tooltipSection, "自定义固定位置", 270, -46, function() return SFramesDB.tooltipMode == "CUSTOM" end, function(checked) if checked then SFramesDB.tooltipMode = "CUSTOM" RefreshTooltipMode() end end ) table.insert(SFrames.ConfigUI.tooltipCBs, cbCustom) table.insert(controls, cbCustom) CreateDesc(tooltipSection, "启用后屏幕上会出现绿色锚点,拖动到目标位置后锁定", 292, -62, font) table.insert(controls, CreateSlider(tooltipSection, "提示框缩放", 14, -116, 200, 0.5, 2.0, 0.05, function() return SFramesDB.tooltipScale or 1.0 end, function(value) SFramesDB.tooltipScale = value if SFrames.FloatingTooltip and SFrames.FloatingTooltip.ApplyScale then SFrames.FloatingTooltip:ApplyScale() end end, function(v) return string.format("%.0f%%", v * 100) end )) -- ── AFK 待机动画 ────────────────────────────────────────────── -- ── 血条材质选择 ────────────────────────────────────────── local barTexSection = CreateSection(root, "血条材质", 8, -264, 520, 120, font) CreateDesc(barTexSection, "选择单位框架中血条和能量条使用的材质。修改后需要 /reload 生效。", 14, -30, font, 490) local texBtnStartX = 14 local texBtnStartY = -50 local texBtnW = 96 local texBtnH = 48 local texBtnGap = 6 local textures = SFrames.BarTextures or {} local texBtns = {} for idx, entry in ipairs(textures) do local eKey = entry.key local eLabel = entry.label local ePath = entry.path local x = texBtnStartX + (idx - 1) * (texBtnW + texBtnGap) local btn = CreateFrame("Button", NextWidgetName("BarTex"), barTexSection) btn:SetWidth(texBtnW) btn:SetHeight(texBtnH) btn:SetPoint("TOPLEFT", barTexSection, "TOPLEFT", x, texBtnStartY) btn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) btn:SetBackdropColor(0.08, 0.08, 0.10, 0.9) btn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) local preview = CreateFrame("StatusBar", NextWidgetName("BarTexPrev"), btn) preview:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2) preview:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 14) preview:SetStatusBarTexture(ePath) preview:SetStatusBarColor(0.2, 0.85, 0.3, 1) preview:SetMinMaxValues(0, 1) preview:SetValue(1) local label = btn:CreateFontString(nil, "OVERLAY") label:SetFont(font, 9, "OUTLINE") label:SetPoint("BOTTOM", btn, "BOTTOM", 0, 3) label:SetText(eLabel) label:SetTextColor(0.8, 0.8, 0.8) btn.texKey = eKey btn.texLabel = eLabel local selectBorder = btn:CreateTexture(nil, "OVERLAY") selectBorder:SetTexture("Interface\\Buttons\\WHITE8X8") selectBorder:SetPoint("TOPLEFT", btn, "TOPLEFT", -1, 1) selectBorder:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 1, -1) selectBorder:SetVertexColor(1, 0.85, 0.4, 0.8) selectBorder:Hide() btn.selectBorder = selectBorder btn:SetScript("OnClick", function() SFramesDB.barTexture = this.texKey for _, b in ipairs(texBtns) do if b.texKey == this.texKey then b:SetBackdropBorderColor(1, 0.85, 0.6, 1) if b.selectBorder then b.selectBorder:Show() end else b:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) if b.selectBorder then b.selectBorder:Hide() end end end SFrames:Print("血条材质已切换为: " .. this.texLabel .. " (/reload 生效)") end) btn:SetScript("OnEnter", function() this:SetBackdropBorderColor(0.7, 0.7, 0.8, 1) GameTooltip:SetOwner(this, "ANCHOR_TOP") GameTooltip:SetText(this.texLabel) GameTooltip:AddLine("点击选择此材质", 0.7, 0.7, 0.7) GameTooltip:Show() end) btn:SetScript("OnLeave", function() local cur = SFramesDB.barTexture or "default" if this.texKey == cur then this:SetBackdropBorderColor(1, 0.85, 0.6, 1) else this:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) end GameTooltip:Hide() end) table.insert(texBtns, btn) end local curTex = SFramesDB.barTexture or "default" for _, b in ipairs(texBtns) do if b.texKey == curTex then b:SetBackdropBorderColor(1, 0.85, 0.6, 1) if b.selectBorder then b.selectBorder:Show() end end end -- ── 框体样式预设 ────────────────────────────────────────── local styleSection = CreateSection(root, "框体样式", 8, -394, 520, 90, font) CreateDesc(styleSection, "切换单位框体的整体外观风格。即时生效,无需 /reload。", 14, -30, font, 490) local STYLE_PRESETS = { { key = "classic", label = "经典(默认)" }, { key = "gradient", label = "简约渐变" }, } local styleBtnW = 120 local styleBtnH = 28 local styleBtnGap = 8 local styleBtnStartX = 14 local styleBtnStartY = -52 local styleBtns = {} for idx, preset in ipairs(STYLE_PRESETS) do local x = styleBtnStartX + (idx - 1) * (styleBtnW + styleBtnGap) local btn = CreateFrame("Button", NextWidgetName("StylePreset"), styleSection) btn:SetWidth(styleBtnW) btn:SetHeight(styleBtnH) btn:SetPoint("TOPLEFT", styleSection, "TOPLEFT", x, styleBtnStartY) btn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) btn:SetBackdropColor(0.08, 0.08, 0.10, 0.9) btn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) local label = btn:CreateFontString(nil, "OVERLAY") label:SetFont(font, 10, "OUTLINE") label:SetPoint("CENTER", btn, "CENTER", 0, 0) label:SetText(preset.label) label:SetTextColor(0.85, 0.85, 0.85) btn.styleKey = preset.key btn.styleLabel = preset.label btn:SetScript("OnClick", function() if not SFramesDB then SFramesDB = {} end SFramesDB.frameStylePreset = this.styleKey -- Gradient mode: default enable rainbow bar if this.styleKey == "gradient" then SFramesDB.powerRainbow = true end -- Update button highlights for _, b in ipairs(styleBtns) do if b.styleKey == this.styleKey then b:SetBackdropBorderColor(1, 0.85, 0.6, 1) else b:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) end end -- Apply to all unit frames immediately if SFrames.Player and SFrames.Player.ApplyConfig then local ok, err = pcall(function() SFrames.Player:ApplyConfig() end) if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] Player style error: " .. tostring(err) .. "|r") end end if SFrames.Target and SFrames.Target.ApplyConfig then local ok, err = pcall(function() SFrames.Target:ApplyConfig() end) if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] Target style error: " .. tostring(err) .. "|r") end end if SFrames.Party and SFrames.Party.ApplyConfig then local ok, err = pcall(function() SFrames.Party:ApplyConfig() end) if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] Party style error: " .. tostring(err) .. "|r") end end if SFrames.Pet and SFrames.Pet.ApplyConfig then local ok, err = pcall(function() SFrames.Pet:ApplyConfig() end) if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] Pet style error: " .. tostring(err) .. "|r") end end if SFrames.Raid and SFrames.Raid.ApplyConfig then local ok, err = pcall(function() SFrames.Raid:ApplyConfig() end) if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] Raid style error: " .. tostring(err) .. "|r") end end if SFrames.ToT and SFrames.ToT.ApplyConfig then local ok, err = pcall(function() SFrames.ToT:ApplyConfig() end) if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] ToT style error: " .. tostring(err) .. "|r") end end if SFrames.Focus and SFrames.Focus.ApplySettings then local ok, err = pcall(function() SFrames.Focus:ApplySettings() end) if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] Focus style error: " .. tostring(err) .. "|r") end end SFrames:Print("框体样式已切换为: " .. this.styleLabel) end) btn:SetScript("OnEnter", function() this:SetBackdropColor(0.15, 0.12, 0.18, 0.95) end) btn:SetScript("OnLeave", function() this:SetBackdropColor(0.08, 0.08, 0.10, 0.9) local cur = (SFramesDB and SFramesDB.frameStylePreset) or "classic" if this.styleKey == cur then this:SetBackdropBorderColor(1, 0.85, 0.6, 1) else this:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) end end) table.insert(styleBtns, btn) end local curStyle = (SFramesDB and SFramesDB.frameStylePreset) or "classic" for _, b in ipairs(styleBtns) do if b.styleKey == curStyle then b:SetBackdropBorderColor(1, 0.85, 0.6, 1) end end local afkSection = CreateSection(root, "AFK 待机动画", 8, -494, 520, 146, font) table.insert(controls, CreateCheckBox(afkSection, "启用 AFK 待机画面", 14, -34, function() return SFramesDB.afkEnabled ~= false end, function(checked) SFramesDB.afkEnabled = checked end )) CreateDesc(afkSection, "进入暂离状态后显示全屏待机画面(角色跳舞 + 信息面板)", 36, -50, font) table.insert(controls, CreateCheckBox(afkSection, "在非休息区也启用", 14, -70, function() return SFramesDB.afkOutsideRest == true end, function(checked) SFramesDB.afkOutsideRest = checked end )) CreateDesc(afkSection, "默认仅在旅店/主城等休息区触发,勾选后在任何区域都会触发", 36, -86, font) table.insert(controls, CreateSlider(afkSection, "AFK 延迟(分钟)", 14, -116, 200, 0, 15, 1, function() return SFramesDB.afkDelay or 5 end, function(v) SFramesDB.afkDelay = v end, function(v) if v == 0 then return "立即" end return v .. " 分钟" end )) CreateDesc(afkSection, "进入暂离后等待多久才显示待机画面", 240, -116, font) CreateButton(afkSection, "预览待机画面", 370, -34, 120, 22, function() if SFrames.AFKScreen and SFrames.AFKScreen.Toggle then SFrames.AFKScreen._manualTrigger = true SFrames.AFKScreen:Show() end end) -- ── 字体选择 ────────────────────────────────────────────────── local fontSection = CreateSection(root, "全局字体", 8, -650, 520, 160, font) CreateDesc(fontSection, "选择 UI 全局使用的字体(聊天、框体、系统文字等),需 /reload 完全生效", 14, -28, font, 480) local FONT_BTN_W = 150 local FONT_BTN_H = 22 local FONT_COLS = 3 local FONT_X0 = 14 local FONT_Y0 = -48 local FONT_GAP_X = 8 local FONT_GAP_Y = 6 local fontBtns = {} for idx, entry in ipairs(SFrames.FontChoices) do local col = math.mod(idx - 1, FONT_COLS) local row = math.floor((idx - 1) / FONT_COLS) local bx = FONT_X0 + col * (FONT_BTN_W + FONT_GAP_X) local by = FONT_Y0 - row * (FONT_BTN_H + FONT_GAP_Y) local btn = CreateFrame("Button", "SFramesFontBtn" .. idx, fontSection) btn:SetWidth(FONT_BTN_W) btn:SetHeight(FONT_BTN_H) btn:SetPoint("TOPLEFT", fontSection, "TOPLEFT", bx, by) btn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1 }) btn:SetBackdropColor(0.12, 0.12, 0.15, 0.9) btn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) btn.fontKey = entry.key local label = btn:CreateFontString(nil, "OVERLAY") -- Try to use the actual font for preview; fallback to default if unavailable local previewOk = pcall(label.SetFont, label, entry.path, 11, "OUTLINE") if not previewOk then label:SetFont(font, 11, "OUTLINE") end label:SetPoint("CENTER", btn, "CENTER", 0, 0) label:SetText(entry.label) label:SetTextColor(0.9, 0.9, 0.9) btn:SetScript("OnEnter", function() if btn.fontKey ~= (SFramesDB.fontKey or "default") then this:SetBackdropBorderColor(0.8, 0.7, 0.5, 1) end end) btn:SetScript("OnLeave", function() if btn.fontKey ~= (SFramesDB.fontKey or "default") then this:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) end end) btn:SetScript("OnClick", function() SFramesDB.fontKey = btn.fontKey -- Update highlight for _, b in ipairs(fontBtns) do if b.fontKey == btn.fontKey then b:SetBackdropBorderColor(1, 0.85, 0.6, 1) else b:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) end end end) table.insert(fontBtns, btn) end -- Highlight current selection local curFont = SFramesDB.fontKey or "default" for _, b in ipairs(fontBtns) do if b.fontKey == curFont then b:SetBackdropBorderColor(1, 0.85, 0.6, 1) end end CreateLabel(fontSection, "提示:切换字体后需要 /reload 才能完全生效。", 14, -136, font, 10, 0.6, 0.6, 0.65) -- ── 彩虹能量条 ────────────────────────────────────────────────── local rainbowSection = CreateSection(root, "彩虹能量条", 8, -820, 520, 70, font) table.insert(controls, CreateCheckBox(rainbowSection, "启用彩虹色能量条", 14, -30, function() return SFramesDB.powerRainbow == true end, function(checked) SFramesDB.powerRainbow = checked if SFrames.Player then SFrames.Player:UpdatePower() end if SFrames.Target then SFrames.Target:UpdatePower() end if SFrames.Focus then SFrames.Focus:UpdatePower() end if SFrames.Party then SFrames.Party:UpdateAll() end if SFrames.Raid then SFrames.Raid:UpdateAll() end end )) CreateDesc(rainbowSection, "所有框体(玩家/目标/焦点/队伍/团队)的能量条均使用彩虹材质", 36, -46, font) personalizeScroll:UpdateRange() self.personalizeControls = controls self.personalizeScroll = personalizeScroll end function SFrames.ConfigUI:BuildThemePage() local font = SFrames:GetFont() local page = self.themePage local controls = {} local themeScroll = CreateScrollArea(page, 4, -4, 548, 458, 680) local root = themeScroll.child --------------------------------------------------------------------------- -- Section 1: Theme color selection (regular + class presets) --------------------------------------------------------------------------- local sec1 = CreateSection(root, "主题色", 8, -8, 520, 340, font) CreateDesc(sec1, "选择主题色后界面全局配色将跟随变化,重载 UI 后完全生效。", 14, -34, font, 480) local SWATCH_SIZE = 32 local SWATCH_GAP = 8 local SWATCH_ROW = SWATCH_SIZE + SWATCH_GAP + 14 local presets = SFrames.Theme.Presets local order = SFrames.Theme.PresetOrder local swatches = {} local function CreateSwatchRow(parent, presetList, startY) for idx = 1, table.getn(presetList) do local key = presetList[idx] local preset = presets[key] if preset then local col = (idx - 1) - math.floor((idx - 1) / 5) * 5 local row = math.floor((idx - 1) / 5) local sx = 14 + col * (SWATCH_SIZE + SWATCH_GAP) local sy = startY + row * -SWATCH_ROW local swatch = CreateFrame("Button", NextWidgetName("Swatch"), parent) swatch:SetWidth(SWATCH_SIZE) swatch:SetHeight(SWATCH_SIZE) swatch:SetPoint("TOPLEFT", parent, "TOPLEFT", sx, sy) swatch:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, edgeSize = 2, insets = { left = 2, right = 2, top = 2, bottom = 2 }, }) local r, g, b if preset.swatchRGB then r, g, b = preset.swatchRGB[1], preset.swatchRGB[2], preset.swatchRGB[3] else r, g, b = SFrames.Theme.HSVtoRGB(preset.hue, 0.50 * (preset.satMul or 1), 0.75) end swatch:SetBackdropColor(r, g, b, 1) swatch:SetBackdropBorderColor(0.25, 0.25, 0.25, 1) local nameFs = swatch:CreateFontString(nil, "OVERLAY") nameFs:SetFont(font, 8, "OUTLINE") nameFs:SetPoint("TOP", swatch, "BOTTOM", 0, -1) nameFs:SetText(preset.name) nameFs:SetTextColor(0.75, 0.75, 0.75) swatch.presetKey = key swatch.selectFrame = CreateFrame("Frame", nil, swatch) swatch.selectFrame:SetPoint("TOPLEFT", swatch, "TOPLEFT", -4, 4) swatch.selectFrame:SetPoint("BOTTOMRIGHT", swatch, "BOTTOMRIGHT", 4, -4) swatch.selectFrame:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 2, }) swatch.selectFrame:SetBackdropBorderColor(1, 0.85, 0, 1) swatch.selectFrame:Hide() swatch.selectGlow = swatch.selectFrame:CreateTexture(nil, "BACKGROUND") swatch.selectGlow:SetTexture("Interface\\Buttons\\WHITE8X8") swatch.selectGlow:SetAllPoints(swatch.selectFrame) swatch.selectGlow:SetVertexColor(1, 0.85, 0, 0.15) swatch:SetScript("OnClick", function() EnsureDB() SFramesDB.Theme.preset = this.presetKey SFramesDB.Theme.useClassTheme = false SFrames.Theme:Apply(this.presetKey) SFrames.ConfigUI:RefreshThemeSwatches() SFrames.ConfigUI:RefreshThemePreview() PlaySound("igMainMenuOptionCheckBoxOn") end) swatch:SetScript("OnEnter", function() this:SetBackdropBorderColor(0.8, 0.8, 0.8, 1) end) swatch:SetScript("OnLeave", function() local active = SFramesDB and SFramesDB.Theme and SFramesDB.Theme.preset local useClass = SFramesDB and SFramesDB.Theme and SFramesDB.Theme.useClassTheme if useClass then active = SFrames.Theme:GetCurrentPreset() end if this.presetKey == active then this:SetBackdropBorderColor(1, 0.85, 0, 1) else this:SetBackdropBorderColor(0.25, 0.25, 0.25, 1) end end) table.insert(swatches, swatch) end end end CreateSwatchRow(sec1, order, -52) local classLabel = sec1:CreateFontString(nil, "OVERLAY") classLabel:SetFont(font, 10, "OUTLINE") classLabel:SetPoint("TOPLEFT", sec1, "TOPLEFT", 14, -164) classLabel:SetText("职业色") classLabel:SetTextColor(0.85, 0.85, 0.85) local divLine = sec1:CreateTexture(nil, "ARTWORK") divLine:SetTexture("Interface\\Buttons\\WHITE8X8") divLine:SetHeight(1) divLine:SetPoint("TOPLEFT", classLabel, "BOTTOMLEFT", 0, -3) divLine:SetPoint("RIGHT", sec1, "RIGHT", -14, 0) divLine:SetVertexColor(0.35, 0.35, 0.40, 0.6) CreateSwatchRow(sec1, SFrames.Theme.ClassPresetOrder, -182) local classCheck = CreateCheckBox(sec1, "跟随当前职业自动切换", 14, -296, function() return SFramesDB and SFramesDB.Theme and SFramesDB.Theme.useClassTheme end, function(checked) EnsureDB() SFramesDB.Theme.useClassTheme = checked if checked then SFrames.Theme:Apply(SFrames.Theme:GetCurrentPreset()) else SFrames.Theme:Apply(SFramesDB.Theme.preset or "pink") end SFrames.ConfigUI:RefreshThemeSwatches() SFrames.ConfigUI:RefreshThemePreview() end ) table.insert(controls, classCheck) self._themeSwatches = swatches --------------------------------------------------------------------------- -- Section 3: Icon set picker --------------------------------------------------------------------------- local sec3 = CreateSection(root, "图标风格", 8, -356, 520, 110, font) CreateDesc(sec3, "选择图标风格后需重载 UI 以完全生效。", 14, -34, font, 480) local ICON_SETS = { "icon", "icon2", "icon3", "icon4", "icon5", "icon6", "icon7", "icon8" } local ICON_SET_SIZE = 48 local ICON_SET_GAP = 10 local faction = UnitFactionGroup and UnitFactionGroup("player") or "Alliance" local factionKey = (faction == "Horde") and "horde" or "alliance" local factionCoords = SFrames.ICON_TCOORDS and SFrames.ICON_TCOORDS[factionKey] if not factionCoords then factionCoords = (factionKey == "horde") and { 0.75, 0.875, 0, 0.125 } or { 0.625, 0.75, 0, 0.125 } end local iconSetBtns = {} for idx = 1, table.getn(ICON_SETS) do local setKey = ICON_SETS[idx] local texPath = "Interface\\AddOns\\Nanami-UI\\img\\" .. setKey local sx = 14 + (idx - 1) * (ICON_SET_SIZE + ICON_SET_GAP) local btn = CreateFrame("Button", NextWidgetName("IcoSet"), sec3) btn:SetWidth(ICON_SET_SIZE) btn:SetHeight(ICON_SET_SIZE) btn:SetPoint("TOPLEFT", sec3, "TOPLEFT", sx, -50) btn:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 10, insets = { left = 2, right = 2, top = 2, bottom = 2 }, }) btn:SetBackdropColor(0.08, 0.08, 0.1, 0.85) btn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) local preview = btn:CreateTexture(nil, "ARTWORK") preview:SetTexture(texPath) preview:SetTexCoord(factionCoords[1], factionCoords[2], factionCoords[3], factionCoords[4]) preview:SetPoint("CENTER", btn, "CENTER", 0, 0) preview:SetWidth(ICON_SET_SIZE - 8) preview:SetHeight(ICON_SET_SIZE - 8) btn.setKey = setKey btn.selectBorder = CreateFrame("Frame", nil, btn) btn.selectBorder:SetPoint("TOPLEFT", btn, "TOPLEFT", -2, 2) btn.selectBorder:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 2, -2) btn.selectBorder:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 2 }) btn.selectBorder:SetBackdropBorderColor(1, 1, 1, 1) btn.selectBorder:Hide() btn:SetScript("OnClick", function() EnsureDB() SFramesDB.Theme.iconSet = this.setKey SFrames.ConfigUI:RefreshIconSetButtons() if SFrames.MinimapButton and SFrames.MinimapButton.Refresh then SFrames.MinimapButton:Refresh() end PlaySound("igMainMenuOptionCheckBoxOn") end) btn:SetScript("OnEnter", function() this:SetBackdropBorderColor(0.7, 0.7, 0.7, 1) GameTooltip:SetOwner(this, "ANCHOR_NONE") GameTooltip:ClearAllPoints() GameTooltip:SetPoint("BOTTOM", this, "TOP", 0, 6) local num = this.setKey == "icon" and "1" or string.sub(this.setKey, 5) GameTooltip:SetText("图标风格 " .. num .. (this.setKey == "icon" and " (默认)" or "")) GameTooltip:AddLine("点击选择,重载 UI 后生效", 0.7, 0.7, 0.7) GameTooltip:Show() GameTooltip:SetFrameStrata("TOOLTIP") GameTooltip:Raise() end) btn:SetScript("OnLeave", function() local cur = SFramesDB and SFramesDB.Theme and SFramesDB.Theme.iconSet or "icon" if this.setKey == cur then this:SetBackdropBorderColor(1, 0.85, 0.6, 1) else this:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) end GameTooltip:Hide() end) table.insert(iconSetBtns, btn) end self._iconSetBtns = iconSetBtns self:RefreshIconSetButtons() --------------------------------------------------------------------------- -- Section 4: Preview --------------------------------------------------------------------------- local sec4 = CreateSection(root, "主题预览", 8, -478, 520, 180, font) local previewPanel = CreateFrame("Frame", NextWidgetName("Preview"), sec4) previewPanel:SetWidth(490) previewPanel:SetHeight(50) previewPanel:SetPoint("TOPLEFT", sec4, "TOPLEFT", 14, -34) EnsureSoftBackdrop(previewPanel) local previewTitle = previewPanel:CreateFontString(nil, "OVERLAY") previewTitle:SetFont(font, 12, "OUTLINE") previewTitle:SetPoint("TOPLEFT", previewPanel, "TOPLEFT", 10, -8) previewTitle:SetText("面板标题示例") local previewText = previewPanel:CreateFontString(nil, "OVERLAY") previewText:SetFont(font, 10, "OUTLINE") previewText:SetPoint("TOPLEFT", previewPanel, "TOPLEFT", 10, -26) previewText:SetText("正文文本 / Body text") local previewDim = previewPanel:CreateFontString(nil, "OVERLAY") previewDim:SetFont(font, 9, "OUTLINE") previewDim:SetPoint("TOPLEFT", previewPanel, "TOPLEFT", 10, -40) previewDim:SetText("辅助文字 / Dim text") local previewBtn = CreateFrame("Button", NextWidgetName("PreviewBtn"), sec4, "UIPanelButtonTemplate") previewBtn:SetWidth(90) previewBtn:SetHeight(24) previewBtn:SetPoint("TOPLEFT", sec4, "TOPLEFT", 14, -92) previewBtn:SetText("按钮示例") StyleButton(previewBtn) local previewAccent = sec4:CreateTexture(nil, "ARTWORK") previewAccent:SetTexture("Interface\\Buttons\\WHITE8X8") previewAccent:SetWidth(490) previewAccent:SetHeight(4) previewAccent:SetPoint("TOPLEFT", sec4, "TOPLEFT", 14, -122) local previewSlot = CreateFrame("Frame", NextWidgetName("PreviewSlot"), sec4) previewSlot:SetWidth(80) previewSlot:SetHeight(24) previewSlot:SetPoint("TOPLEFT", sec4, "TOPLEFT", 14, -134) previewSlot:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) local previewSlotLabel = previewSlot:CreateFontString(nil, "OVERLAY") previewSlotLabel:SetFont(font, 9, "OUTLINE") previewSlotLabel:SetPoint("CENTER", previewSlot, "CENTER", 0, 0) previewSlotLabel:SetText("色块") self._themePreview = { panel = previewPanel, title = previewTitle, text = previewText, dim = previewDim, btn = previewBtn, accent = previewAccent, slot = previewSlot, slotLabel = previewSlotLabel, } --------------------------------------------------------------------------- -- Hint --------------------------------------------------------------------------- CreateDesc(root, "提示: 更换主题后建议点击右下方 [保存并重载] 以确保所有界面生效。", 14, -650, font, 500) --------------------------------------------------------------------------- -- Finalize --------------------------------------------------------------------------- themeScroll:UpdateRange() self.themeControls = controls self.themeScroll = themeScroll self:RefreshThemeSwatches() self:RefreshThemePreview() end function SFrames.ConfigUI:RefreshThemeSwatches() if not self._themeSwatches then return end local active = SFramesDB and SFramesDB.Theme and SFramesDB.Theme.preset or "pink" local useClass = SFramesDB and SFramesDB.Theme and SFramesDB.Theme.useClassTheme if useClass then active = SFrames.Theme:GetCurrentPreset() end for _, sw in ipairs(self._themeSwatches) do if sw.presetKey == active then sw:SetBackdropBorderColor(1, 0.85, 0, 1) if sw.selectFrame then sw.selectFrame:Show() end else sw:SetBackdropBorderColor(0.25, 0.25, 0.25, 1) if sw.selectFrame then sw.selectFrame:Hide() end end end end function SFrames.ConfigUI:RefreshThemePreview() local p = self._themePreview if not p then return end local A = SFrames.ActiveTheme if p.panel and p.panel.SetBackdropColor then p.panel:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], A.panelBg[4] or 0.95) p.panel:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 0.9) end if p.title then p.title:SetTextColor(A.title[1], A.title[2], A.title[3]) end if p.text then p.text:SetTextColor(A.text[1], A.text[2], A.text[3]) end if p.dim then p.dim:SetTextColor(A.dimText[1], A.dimText[2], A.dimText[3]) end if p.accent then p.accent:SetVertexColor(A.accent[1], A.accent[2], A.accent[3], A.accent[4] or 1) end if p.slot and p.slot.SetBackdropColor then p.slot:SetBackdropColor(A.slotBg[1], A.slotBg[2], A.slotBg[3], A.slotBg[4] or 0.9) p.slot:SetBackdropBorderColor(A.slotSelected[1], A.slotSelected[2], A.slotSelected[3], A.slotSelected[4] or 1) end if p.slotLabel then p.slotLabel:SetTextColor(A.nameText[1], A.nameText[2], A.nameText[3]) end end function SFrames.ConfigUI:RefreshIconSetButtons() if not self._iconSetBtns then return end local cur = SFramesDB and SFramesDB.Theme and SFramesDB.Theme.iconSet or "icon" for _, btn in ipairs(self._iconSetBtns) do if btn.setKey == cur then btn:SetBackdropBorderColor(1, 0.85, 0.6, 1) if btn.selectBorder then btn.selectBorder:Show() end else btn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) if btn.selectBorder then btn.selectBorder:Hide() end end end end -------------------------------------------------------------------------------- -- Profile page: table serializer / deserializer (Lua 5.0 compatible) -------------------------------------------------------------------------------- local PROFILE_PREFIX = "!NUI1!" local PROFILE_SEP = "\30" local P64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" local function P64Encode(src) if not src or src == "" then return "" end local out = {} local len = string.len(src) local i = 1 while i <= len do local a = string.byte(src, i) local b = (i + 1 <= len) and string.byte(src, i + 1) or 0 local c = (i + 2 <= len) and string.byte(src, i + 2) or 0 local remaining = len - i + 1 local n = a * 65536 + b * 256 + c table.insert(out, string.sub(P64, math.floor(n / 262144) + 1, math.floor(n / 262144) + 1)) table.insert(out, string.sub(P64, math.floor(math.mod(n, 262144) / 4096) + 1, math.floor(math.mod(n, 262144) / 4096) + 1)) if remaining >= 2 then table.insert(out, string.sub(P64, math.floor(math.mod(n, 4096) / 64) + 1, math.floor(math.mod(n, 4096) / 64) + 1)) else table.insert(out, "=") end if remaining >= 3 then table.insert(out, string.sub(P64, math.mod(n, 64) + 1, math.mod(n, 64) + 1)) else table.insert(out, "=") end i = i + 3 end return table.concat(out) end local P64_INV = {} for i = 1, 64 do P64_INV[string.sub(P64, i, i)] = i - 1 end local function P64Decode(src) if not src or src == "" then return "" end src = string.gsub(src, "%s+", "") local out = {} local len = string.len(src) local i = 1 while i <= len do local v1 = P64_INV[string.sub(src, i, i)] or 0 local v2 = P64_INV[string.sub(src, i + 1, i + 1)] or 0 local v3 = P64_INV[string.sub(src, i + 2, i + 2)] local v4 = P64_INV[string.sub(src, i + 3, i + 3)] local n = v1 * 262144 + v2 * 4096 + (v3 or 0) * 64 + (v4 or 0) table.insert(out, string.char(math.floor(n / 65536))) if v3 then table.insert(out, string.char(math.floor(math.mod(n, 65536) / 256))) end if v4 then table.insert(out, string.char(math.mod(n, 256))) end i = i + 4 end return table.concat(out) end local function SerializeValue(val, depth) if depth > 20 then return "nil" end local t = type(val) if t == "string" then local escaped = string.gsub(val, "\\", "\\\\") escaped = string.gsub(escaped, "\"", "\\\"") escaped = string.gsub(escaped, "\n", "\\n") escaped = string.gsub(escaped, "\r", "\\r") return "\"" .. escaped .. "\"" elseif t == "number" then return tostring(val) elseif t == "boolean" then return val and "true" or "false" elseif t == "table" then local parts = {} local arrayLen = 0 for i = 1, 2048 do if val[i] ~= nil then arrayLen = i else break end end for i = 1, arrayLen do table.insert(parts, SerializeValue(val[i], depth + 1)) end local sorted = {} for k, v in pairs(val) do if type(k) == "number" and k >= 1 and k <= arrayLen and math.floor(k) == k then -- already handled else table.insert(sorted, k) end end table.sort(sorted, function(a, b) local ta, tb = type(a), type(b) if ta ~= tb then return ta < tb end return a < b end) for _, k in ipairs(sorted) do local ks if type(k) == "string" then ks = "[\"" .. k .. "\"]" else ks = "[" .. tostring(k) .. "]" end table.insert(parts, ks .. "=" .. SerializeValue(val[k], depth + 1)) end return "{" .. table.concat(parts, ",") .. "}" else return "nil" end end local function DeserializeTable(str) if not str or str == "" then return nil, "空数据" end local safeStr = string.gsub(str, "[^%w%s%p]", "") local dangerPatterns = { "function", "loadstring", "dofile", "pcall", "getfenv", "setfenv", "rawget", "rawset", "getmetatable", "setmetatable", "io%.", "os%.", "debug%." } for _, pat in ipairs(dangerPatterns) do if string.find(safeStr, pat) then return nil, "不安全的数据" end end local fn local loadFunc = loadstring or load if not loadFunc then return nil, "环境不支持" end fn = loadFunc("return " .. str) if not fn then return nil, "解析失败" end local ok, result = pcall(fn) if not ok then return nil, "执行失败" end if type(result) ~= "table" then return nil, "数据格式错误" end return result end local function DeepCopy(orig) if type(orig) ~= "table" then return orig end local copy = {} for k, v in pairs(orig) do copy[k] = DeepCopy(v) end return copy end local function CollectExportData() local data = {} if SFramesDB then data.db = DeepCopy(SFramesDB) end if SFramesGlobalDB then data.global = {} if SFramesGlobalDB.KeyBindProfiles then data.global.KeyBindProfiles = DeepCopy(SFramesGlobalDB.KeyBindProfiles) end end if SFrames.KeyBindManager and SFrames.KeyBindManager.CollectAllBindings then data.currentBindings = SFrames.KeyBindManager:CollectAllBindings() end -- Nanami-QT if QuickToolboxDB then data.qtDB = DeepCopy(QuickToolboxDB) end if QuickToolboxSharedDB then data.qtSharedDB = DeepCopy(QuickToolboxSharedDB) end -- Nanami-Plates if NanamiPlatesDB then data.platesDB = DeepCopy(NanamiPlatesDB) end -- Nanami-DPS if NanamiDPS_DB then data.dpsDB = DeepCopy(NanamiDPS_DB) end data.exportTime = time and time() or 0 data.charName = UnitName("player") or "Unknown" data.version = GetAddOnMetadata and GetAddOnMetadata("Nanami-UI", "Version") or "unknown" return data end local function ExportProfileString() local data = CollectExportData() local raw = SerializeValue(data, 0) return PROFILE_PREFIX .. P64Encode(raw) end local function ImportProfileString(text) if not text or text == "" then return nil, "文本为空" end text = string.gsub(text, "^%s+", "") text = string.gsub(text, "%s+$", "") local prefixLen = string.len(PROFILE_PREFIX) if string.sub(text, 1, prefixLen) ~= PROFILE_PREFIX then return nil, "无效的配置字符串(前缀不匹配)" end local encoded = string.sub(text, prefixLen + 1) local raw = P64Decode(encoded) if not raw or raw == "" then return nil, "Base64 解码失败" end local data, err = DeserializeTable(raw) if not data then return nil, err end return data end local function ApplyImportData(data) if not data then return false, "无数据" end local count = 0 if data.db and type(data.db) == "table" then for k, v in pairs(data.db) do SFramesDB[k] = DeepCopy(v) count = count + 1 end end if data.global and type(data.global) == "table" then if not SFramesGlobalDB then SFramesGlobalDB = {} end if data.global.KeyBindProfiles then SFramesGlobalDB.KeyBindProfiles = DeepCopy(data.global.KeyBindProfiles) end end if data.keyProfiles and type(data.keyProfiles) == "table" then if not SFramesGlobalDB then SFramesGlobalDB = {} end SFramesGlobalDB.KeyBindProfiles = DeepCopy(data.keyProfiles) end if data.currentBindings and type(data.currentBindings) == "table" then if SFrames.KeyBindManager and SFrames.KeyBindManager.ApplyBindings then SFrames.KeyBindManager:ApplyBindings(data.currentBindings) end end -- Nanami-QT if data.qtDB and type(data.qtDB) == "table" then if not QuickToolboxDB then QuickToolboxDB = {} end for k, v in pairs(data.qtDB) do QuickToolboxDB[k] = DeepCopy(v) end count = count + 1 end if data.qtSharedDB and type(data.qtSharedDB) == "table" then if not QuickToolboxSharedDB then QuickToolboxSharedDB = {} end for k, v in pairs(data.qtSharedDB) do QuickToolboxSharedDB[k] = DeepCopy(v) end count = count + 1 end -- Nanami-Plates if data.platesDB and type(data.platesDB) == "table" then if not NanamiPlatesDB then NanamiPlatesDB = {} end for k, v in pairs(data.platesDB) do NanamiPlatesDB[k] = DeepCopy(v) end count = count + 1 end -- Nanami-DPS if data.dpsDB and type(data.dpsDB) == "table" then if not NanamiDPS_DB then NanamiDPS_DB = {} end for k, v in pairs(data.dpsDB) do NanamiDPS_DB[k] = DeepCopy(v) end count = count + 1 end return true, count end local function BoolStr(v, trueStr, falseStr) if v then return "|cff44ff44" .. (trueStr or "开") .. "|r" end return "|cffff4444" .. (falseStr or "关") .. "|r" end local function CountTableKeys(t) if type(t) ~= "table" then return 0 end local n = 0 for _ in pairs(t) do n = n + 1 end return n end local function BuildSummaryText() local lines = {} local db = SFramesDB or {} table.insert(lines, "|cffffcc00角色:|r " .. (UnitName("player") or "Unknown")) local ver = GetAddOnMetadata and GetAddOnMetadata("Nanami-UI", "Version") or "N/A" table.insert(lines, "|cffffcc00版本:|r " .. ver) table.insert(lines, "|cffffcc00布局模式:|r " .. tostring(db.layoutMode or "default")) table.insert(lines, "") table.insert(lines, "|cffffcc00== 功能模块 ==|r") table.insert(lines, "单位框架: " .. BoolStr(db.enableUnitFrames ~= false)) table.insert(lines, "玩家框体: " .. BoolStr(db.enablePlayerFrame ~= false)) table.insert(lines, "目标框体: " .. BoolStr(db.enableTargetFrame ~= false)) table.insert(lines, "背包: " .. BoolStr(db.Bags and db.Bags.enable ~= false)) table.insert(lines, "动作条: " .. BoolStr(db.ActionBars and db.ActionBars.enable ~= false)) table.insert(lines, "商人: " .. BoolStr(db.enableMerchant ~= false)) table.insert(lines, "任务UI: " .. BoolStr(db.enableQuestUI ~= false)) table.insert(lines, "任务日志: " .. BoolStr(db.enableQuestLogSkin ~= false)) table.insert(lines, "训练师: " .. BoolStr(db.enableTrainer ~= false)) table.insert(lines, "法术书: " .. BoolStr(db.enableSpellBook ~= false)) table.insert(lines, "专业技能: " .. BoolStr(db.enableTradeSkill ~= false)) table.insert(lines, "社交: " .. BoolStr(db.enableSocial ~= false)) table.insert(lines, "观察: " .. BoolStr(db.enableInspect ~= false)) table.insert(lines, "飞行地图: " .. BoolStr(db.enableFlightMap ~= false)) table.insert(lines, "宠物栏: " .. BoolStr(db.enablePetStable ~= false)) table.insert(lines, "邮件: " .. BoolStr(db.enableMail ~= false)) table.insert(lines, "") table.insert(lines, "|cffffcc00== 玩家框体 ==|r") table.insert(lines, "缩放: " .. tostring(db.playerFrameScale or 1)) table.insert(lines, "宽度: " .. tostring(db.playerFrameWidth or 220)) table.insert(lines, "头像宽: " .. tostring(db.playerPortraitWidth or 50)) table.insert(lines, "血条高: " .. tostring(db.playerHealthHeight or 38)) table.insert(lines, "能量条高: " .. tostring(db.playerPowerHeight or 9)) table.insert(lines, "头像: " .. BoolStr(db.playerShowPortrait ~= false)) table.insert(lines, "职业图标: " .. BoolStr(db.playerShowClassIcon ~= false)) table.insert(lines, "透明度: " .. tostring(db.playerFrameAlpha or 1)) table.insert(lines, "") table.insert(lines, "|cffffcc00== 目标框体 ==|r") table.insert(lines, "缩放: " .. tostring(db.targetFrameScale or 1)) table.insert(lines, "宽度: " .. tostring(db.targetFrameWidth or 220)) table.insert(lines, "头像宽: " .. tostring(db.targetPortraitWidth or 50)) table.insert(lines, "血条高: " .. tostring(db.targetHealthHeight or 38)) table.insert(lines, "距离显示: " .. BoolStr(db.targetDistanceEnabled ~= false)) table.insert(lines, "距离文字大小: " .. tostring(db.targetDistanceFontSize or 14)) table.insert(lines, "透明度: " .. tostring(db.targetFrameAlpha or 1)) table.insert(lines, "全局字体: " .. tostring(db.fontKey or "default")) table.insert(lines, "") table.insert(lines, "|cffffcc00== 小队框架 ==|r") table.insert(lines, "布局: " .. tostring(db.partyLayout or "vertical")) table.insert(lines, "缩放: " .. tostring(db.partyFrameScale or 1)) table.insert(lines, "宽度: " .. tostring(db.partyFrameWidth or 150)) table.insert(lines, "高度: " .. tostring(db.partyFrameHeight or 35)) table.insert(lines, "") table.insert(lines, "|cffffcc00== 团队框架 ==|r") if db.Raid then table.insert(lines, "缩放: " .. tostring(db.Raid.scale or 1)) table.insert(lines, "列数: " .. tostring(db.Raid.columns or 5)) else table.insert(lines, "使用默认设置") end table.insert(lines, "") table.insert(lines, "|cffffcc00== 背包设置 ==|r") local bags = db.Bags or {} table.insert(lines, "列数: " .. tostring(bags.columns or 10)) table.insert(lines, "缩放: " .. tostring(bags.scale or 1)) table.insert(lines, "间距: " .. tostring(bags.bagSpacing or 0)) table.insert(lines, "自动卖灰: " .. BoolStr(bags.sellGrey ~= false)) table.insert(lines, "银行列数: " .. tostring(bags.bankColumns or 12)) table.insert(lines, "") table.insert(lines, "|cffffcc00== 动作条 ==|r") if db.ActionBars then table.insert(lines, "启用: " .. BoolStr(db.ActionBars.enable ~= false)) table.insert(lines, "缩放: " .. tostring(db.ActionBars.scale or 1)) else table.insert(lines, "使用默认设置") end table.insert(lines, "") table.insert(lines, "|cffffcc00== 聊天设置 ==|r") local chat = db.Chat or {} table.insert(lines, "启用: " .. BoolStr(chat.enable ~= false)) table.insert(lines, "配置项数: " .. CountTableKeys(chat)) if chat.fontSize then table.insert(lines, "字体大小: " .. tostring(chat.fontSize)) end table.insert(lines, "") table.insert(lines, "|cffffcc00 -- 窗口 --|r") table.insert(lines, "宽度: " .. tostring(chat.width or "N/A")) table.insert(lines, "高度: " .. tostring(chat.height or "N/A")) table.insert(lines, "缩放: " .. tostring(chat.scale or 1)) table.insert(lines, "背景透明度: " .. tostring(chat.bgAlpha or 0.45)) table.insert(lines, "悬停显示背景: " .. BoolStr(chat.hoverTransparent ~= false)) table.insert(lines, "边框: " .. BoolStr(chat.showBorder)) table.insert(lines, "职业色边框: " .. BoolStr(chat.borderClassColor)) table.insert(lines, "显示玩家等级: " .. BoolStr(chat.showPlayerLevel ~= false)) table.insert(lines, "输入框位置: " .. tostring(chat.editBoxPosition or "bottom")) table.insert(lines, "") table.insert(lines, "|cffffcc00 -- 标签/过滤 --|r") local tabCount = (chat.tabs and table.getn(chat.tabs)) or 0 table.insert(lines, "标签数: " .. tostring(tabCount)) if chat.tabs then for i = 1, table.getn(chat.tabs) do local t = chat.tabs[i] if t then local filterCount = t.filters and CountTableKeys(t.filters) or 0 local chanCount = t.channelFilters and CountTableKeys(t.channelFilters) or 0 table.insert(lines, " [" .. tostring(i) .. "] " .. tostring(t.name or "?") .. " (过滤:" .. filterCount .. " 频道:" .. chanCount .. ")") end end end table.insert(lines, "") table.insert(lines, "|cffffcc00 -- AI翻译 --|r") table.insert(lines, "翻译引擎: " .. BoolStr(chat.translateEnabled ~= false)) table.insert(lines, "消息监控: " .. BoolStr(chat.chatMonitorEnabled ~= false)) if chat.tabs then for i = 1, table.getn(chat.tabs) do local t = chat.tabs[i] if t and t.translateFilters then local enabledKeys = {} for k, v in pairs(t.translateFilters) do if v == true then table.insert(enabledKeys, k) end end if table.getn(enabledKeys) > 0 then table.insert(lines, " [" .. tostring(t.name or i) .. "] 翻译: " .. table.concat(enabledKeys, ",")) end end end end table.insert(lines, "") table.insert(lines, "|cffffcc00 -- 硬核设置 --|r") table.insert(lines, "全局关闭硬核频道: " .. BoolStr(chat.hcGlobalDisable)) table.insert(lines, "屏蔽死亡/满级信息: " .. BoolStr(chat.hcDeathDisable)) table.insert(lines, "最低死亡通报等级: " .. tostring(chat.hcDeathLevelMin or 10)) table.insert(lines, "") table.insert(lines, "|cffffcc00== 地图设置 ==|r") local mm = db.Minimap or {} table.insert(lines, "小地图配置项: " .. CountTableKeys(mm)) if db.WorldMap then table.insert(lines, "世界地图配置项: " .. CountTableKeys(db.WorldMap)) end if db.MapReveal ~= nil then table.insert(lines, "地图全亮: " .. BoolStr(db.MapReveal ~= false)) end table.insert(lines, "") table.insert(lines, "|cffffcc00== Buff 栏 ==|r") if db.MinimapBuffs then table.insert(lines, "配置项数: " .. CountTableKeys(db.MinimapBuffs)) end if db.hiddenBuffs then table.insert(lines, "隐藏的 Buff 数: " .. CountTableKeys(db.hiddenBuffs)) end table.insert(lines, "") table.insert(lines, "|cffffcc00== 主题 ==|r") local theme = db.Theme or {} table.insert(lines, "预设: " .. tostring(theme.preset or "default")) table.insert(lines, "职业主题: " .. BoolStr(theme.classTheme)) table.insert(lines, "图标集: " .. tostring(theme.iconSet or "icon")) table.insert(lines, "") table.insert(lines, "|cffffcc00== 框架定位 ==|r") if db.Positions then local posCount = CountTableKeys(db.Positions) table.insert(lines, "已保存的框架位置: " .. posCount .. " 个") for name, _ in pairs(db.Positions) do table.insert(lines, " - " .. tostring(name)) end else table.insert(lines, "无自定义位置") end table.insert(lines, "") table.insert(lines, "|cffffcc00== 按键绑定 ==|r") if SFramesGlobalDB and SFramesGlobalDB.KeyBindProfiles then local profCount = 0 for name, _ in pairs(SFramesGlobalDB.KeyBindProfiles) do profCount = profCount + 1 table.insert(lines, " 方案: " .. tostring(name)) end table.insert(lines, "已保存方案: " .. profCount .. " 个") else table.insert(lines, "已保存方案: 0 个") end if SFrames.KeyBindManager and SFrames.KeyBindManager.CollectAllBindings then local binds = SFrames.KeyBindManager:CollectAllBindings() table.insert(lines, "当前绑定: " .. (binds and table.getn(binds) or 0) .. " 个") end table.insert(lines, "") table.insert(lines, "|cffffcc00== 个性化 ==|r") if db.Tweaks then table.insert(lines, "微调项数: " .. CountTableKeys(db.Tweaks)) end table.insert(lines, "") table.insert(lines, "|cffffcc00== Nanami-QT (工具箱) ==|r") if QuickToolboxDB then table.insert(lines, "角色配置项: " .. CountTableKeys(QuickToolboxDB)) else table.insert(lines, "角色配置: |cff888888未加载|r") end if QuickToolboxSharedDB then table.insert(lines, "全局配置项: " .. CountTableKeys(QuickToolboxSharedDB)) else table.insert(lines, "全局配置: |cff888888未加载|r") end table.insert(lines, "") table.insert(lines, "|cffffcc00== Nanami-Plates (姓名板) ==|r") if NanamiPlatesDB then table.insert(lines, "配置项数: " .. CountTableKeys(NanamiPlatesDB)) else table.insert(lines, "配置: |cff888888未加载|r") end table.insert(lines, "") table.insert(lines, "|cffffcc00== Nanami-DPS (伤害统计) ==|r") if NanamiDPS_DB then table.insert(lines, "配置项数: " .. CountTableKeys(NanamiDPS_DB)) else table.insert(lines, "配置: |cff888888未加载|r") end return table.concat(lines, "\n") end -------------------------------------------------------------------------------- -- Profile page: export / import dialogs -------------------------------------------------------------------------------- local profileExportFrame, profileImportFrame local function ShowProfileExportDialog() local text = ExportProfileString() if not profileExportFrame then local font = SFrames:GetFont() local f = CreateFrame("Frame", "SFramesProfileExport", UIParent) f:SetWidth(520) f:SetHeight(380) f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) f:SetMovable(true) f:EnableMouse(true) f:SetClampedToScreen(true) f:SetToplevel(true) f:SetFrameStrata("TOOLTIP") f:SetFrameLevel(200) f:RegisterForDrag("LeftButton") f:SetScript("OnDragStart", function() this:StartMoving() end) f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) if SFrames.CreateRoundBackdrop then SFrames:CreateRoundBackdrop(f) else EnsureSoftBackdrop(f) end tinsert(UISpecialFrames, "SFramesProfileExport") local title = f:CreateFontString(nil, "OVERLAY") title:SetFont(font, 12, "OUTLINE") title:SetPoint("TOP", f, "TOP", 0, -12) title:SetText("|cffffcc00导出全部配置|r") f.title = title local desc = f:CreateFontString(nil, "OVERLAY") desc:SetFont(font, 10, "OUTLINE") desc:SetPoint("TOP", title, "BOTTOM", 0, -4) desc:SetText("Ctrl+A 全选,Ctrl+C 复制。将字符串分享给其他人即可导入") desc:SetTextColor(0.7, 0.7, 0.7) local sf = CreateFrame("ScrollFrame", "SFramesProfileExportScroll", f, "UIPanelScrollFrameTemplate") sf:SetPoint("TOPLEFT", f, "TOPLEFT", 14, -52) sf:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -34, 44) local edit = CreateFrame("EditBox", "SFramesProfileExportEdit", sf) edit:SetWidth(450) edit:SetHeight(240) edit:SetMultiLine(true) edit:SetFont(font, 9, "OUTLINE") edit:SetAutoFocus(false) edit:SetScript("OnEscapePressed", function() f:Hide() end) sf:SetScrollChild(edit) f.edit = edit local closeBtn = CreateFrame("Button", "SFramesProfileExportClose", f, "UIPanelButtonTemplate") closeBtn:SetWidth(100) closeBtn:SetHeight(26) closeBtn:SetPoint("BOTTOM", f, "BOTTOM", 0, 12) closeBtn:SetText("关闭") StyleButton(closeBtn) closeBtn:SetScript("OnClick", function() f:Hide() end) profileExportFrame = f end profileExportFrame.edit:SetText(text) profileExportFrame:Show() profileExportFrame:Raise() profileExportFrame.edit:HighlightText() profileExportFrame.edit:SetFocus() end local function ShowProfileImportDialog() if not profileImportFrame then local font = SFrames:GetFont() local f = CreateFrame("Frame", "SFramesProfileImport", UIParent) f:SetWidth(520) f:SetHeight(420) f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) f:SetMovable(true) f:EnableMouse(true) f:SetClampedToScreen(true) f:SetToplevel(true) f:SetFrameStrata("TOOLTIP") f:SetFrameLevel(200) f:RegisterForDrag("LeftButton") f:SetScript("OnDragStart", function() this:StartMoving() end) f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) if SFrames.CreateRoundBackdrop then SFrames:CreateRoundBackdrop(f) else EnsureSoftBackdrop(f) end tinsert(UISpecialFrames, "SFramesProfileImport") local title = f:CreateFontString(nil, "OVERLAY") title:SetFont(font, 12, "OUTLINE") title:SetPoint("TOP", f, "TOP", 0, -12) title:SetText("|cffffcc00导入配置|r") f.title = title local desc = f:CreateFontString(nil, "OVERLAY") desc:SetFont(font, 10, "OUTLINE") desc:SetPoint("TOP", title, "BOTTOM", 0, -4) desc:SetText("将配置字符串粘贴到下方 (Ctrl+V),然后点击导入") desc:SetTextColor(0.7, 0.7, 0.7) local sf = CreateFrame("ScrollFrame", "SFramesProfileImportScroll", f, "UIPanelScrollFrameTemplate") sf:SetPoint("TOPLEFT", f, "TOPLEFT", 14, -52) sf:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -34, 78) local edit = CreateFrame("EditBox", "SFramesProfileImportEdit", sf) edit:SetWidth(450) edit:SetHeight(240) edit:SetMultiLine(true) edit:SetFont(font, 9, "OUTLINE") edit:SetAutoFocus(false) edit:SetScript("OnEscapePressed", function() f:Hide() end) sf:SetScrollChild(edit) f.edit = edit local statusLabel = f:CreateFontString(nil, "OVERLAY") statusLabel:SetFont(font, 10, "OUTLINE") statusLabel:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 16, 50) statusLabel:SetWidth(340) statusLabel:SetJustifyH("LEFT") statusLabel:SetText("") f.statusLabel = statusLabel local importBtn = CreateFrame("Button", "SFramesProfileImportBtn", f, "UIPanelButtonTemplate") importBtn:SetWidth(120) importBtn:SetHeight(26) importBtn:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 130, 14) importBtn:SetText("导入并应用") StyleButton(importBtn) importBtn:SetScript("OnClick", function() local inputText = f.edit:GetText() if not inputText or inputText == "" then f.statusLabel:SetText("|cffff4444请先粘贴配置数据|r") return end local data, err = ImportProfileString(inputText) if not data then f.statusLabel:SetText("|cffff4444错误: " .. (err or "未知") .. "|r") return end local ok, count = ApplyImportData(data) if ok then local src = data.charName or "未知" local ver = data.version or "未知" f.statusLabel:SetText("|cff44ff44导入成功! 来源: " .. src .. " (v" .. ver .. ")|r") SFrames:Print("已导入配置 (来源: " .. src .. "),需要 /reload 生效") else f.statusLabel:SetText("|cffff4444导入失败|r") end end) local cancelBtn = CreateFrame("Button", "SFramesProfileImportCancel", f, "UIPanelButtonTemplate") cancelBtn:SetWidth(100) cancelBtn:SetHeight(26) cancelBtn:SetPoint("LEFT", importBtn, "RIGHT", 8, 0) cancelBtn:SetText("取消") StyleButton(cancelBtn) cancelBtn:SetScript("OnClick", function() f:Hide() end) profileImportFrame = f end profileImportFrame.edit:SetText("") profileImportFrame.statusLabel:SetText("") profileImportFrame:Show() profileImportFrame:Raise() profileImportFrame.edit:SetFocus() end -------------------------------------------------------------------------------- -- Profile page: partial apply dialog -------------------------------------------------------------------------------- local APPLY_CATEGORIES = { { key = "ui", label = "界面功能开关", keys = {"enableMerchant","enableQuestUI","enableQuestLogSkin","enableTrainer","enableSpellBook","enableTradeSkill","enableSocial","enableInspect","enableFlightMap","enablePetStable","enableMail","enableUnitFrames","enablePlayerFrame","enableTargetFrame","enablePartyFrame","enableChat","showLevel","classColorHealth","smoothBars","fontKey","frameStylePreset"} }, { key = "player", label = "玩家框体", keys = {"playerFrameScale","playerFrameWidth","playerPortraitWidth","playerHealthHeight","playerPowerHeight","playerPowerWidth","playerPowerOffsetX","playerPowerOffsetY","playerPowerOnTop","playerPortraitBgAlpha","playerShowClass","playerShowClassIcon","playerShowPortrait","playerFrameAlpha","playerBgAlpha","playerNameFontSize","playerValueFontSize","playerHealthFontSize","playerPowerFontSize","playerHealthTexture","playerPowerTexture","playerNameFontKey","playerHealthFontKey","playerPowerFontKey","castbarStandalone","castbarRainbow","powerRainbow","petHealthTexture","petPowerTexture"} }, { key = "target", label = "目标框体", keys = {"targetFrameScale","targetFrameWidth","targetPortraitWidth","targetHealthHeight","targetPowerHeight","targetPowerWidth","targetPowerOffsetX","targetPowerOffsetY","targetPowerOnTop","targetPortraitBgAlpha","targetShowClass","targetShowClassIcon","targetShowPortrait","targetFrameAlpha","targetBgAlpha","targetNameFontSize","targetValueFontSize","targetHealthFontSize","targetPowerFontSize","targetHealthTexture","targetPowerTexture","targetNameFontKey","targetHealthFontKey","targetPowerFontKey","targetDistanceEnabled","targetDistanceOnFrame","targetDistanceScale","targetDistanceFontSize","targetDistanceFontKey","totHealthTexture"} }, { key = "focus", label = "焦点框体", keys = {"focusEnabled","focusFrameScale","focusFrameWidth","focusPortraitWidth","focusHealthHeight","focusPowerHeight","focusPowerWidth","focusPowerOffsetX","focusPowerOffsetY","focusPowerOnTop","focusPortraitBgAlpha","focusShowPortrait","focusShowCastBar","focusShowAuras","focusNameFontSize","focusValueFontSize","focusHealthFontSize","focusPowerFontSize","focusCastFontSize","focusDistanceFontSize","focusHealthTexture","focusPowerTexture","focusNameFontKey","focusHealthFontKey","focusPowerFontKey","focusCastFontKey","focusDistanceFontKey","focusBgAlpha"} }, { key = "party", label = "小队框架", keys = {"partyLayout","partyFrameScale","partyFrameWidth","partyFrameHeight","partyPortraitWidth","partyHealthHeight","partyPowerHeight","partyPowerWidth","partyPowerOffsetX","partyPowerOffsetY","partyPowerOnTop","partyPortraitBgAlpha","partyHorizontalGap","partyVerticalGap","partyNameFontSize","partyValueFontSize","partyHealthFontSize","partyPowerFontSize","partyHealthTexture","partyPowerTexture","partyNameFontKey","partyHealthFontKey","partyPowerFontKey","partyShowBuffs","partyShowDebuffs","partyBgAlpha","partyPortrait3D"} }, { key = "raid", label = "团队框架", keys = {"raidLayout","raidFrameScale","raidFrameWidth","raidFrameHeight","raidHealthHeight","raidPowerWidth","raidPowerHeight","raidPowerOffsetX","raidPowerOffsetY","raidPowerOnTop","raidHorizontalGap","raidVerticalGap","raidGroupGap","raidNameFontSize","raidValueFontSize","raidHealthFontSize","raidPowerFontSize","raidHealthTexture","raidPowerTexture","raidNameFontKey","raidHealthFontKey","raidPowerFontKey","raidShowPower","enableRaidFrames","raidHealthFormat","raidShowGroupLabel","raidBgAlpha"} }, { key = "bags", label = "背包设置", table_key = "Bags" }, { key = "actionbar",label = "动作条设置", table_key = "ActionBars" }, { key = "chat_general", label = "聊天-通用", chat_keys = {"enable","showBorder","borderClassColor","showPlayerLevel","chatMonitorEnabled","layoutVersion"} }, { key = "chat_window", label = "聊天-窗口", chat_keys = {"width","height","scale","fontSize","bgAlpha","hoverTransparent","sidePadding","topPadding","bottomPadding","editBoxPosition","editBoxX","editBoxY"} }, { key = "chat_tabs", label = "聊天-标签/过滤", chat_keys = {"tabs","activeTab","nextTabId"} }, { key = "chat_translate",label = "聊天-AI翻译", chat_keys = {"translateEnabled"}, chat_tab_keys = {"translateFilters","channelTranslateFilters"} }, { key = "chat_hardcore", label = "聊天-硬核设置", chat_keys = {"hcGlobalDisable","hcDeathDisable","hcDeathLevelMin"} }, { key = "theme", label = "主题设置", table_key = "Theme" }, { key = "minimap", label = "地图设置", table_key = "Minimap" }, { key = "buff", label = "Buff 栏设置", table_key = "MinimapBuffs" }, { key = "tweaks", label = "微调 / 个性化", table_key = "Tweaks" }, { key = "positions",label = "界面位置", table_key = "Positions" }, { key = "qt", label = "Nanami-QT", ext_db = "_qtDB", ext_global = "QuickToolboxDB" }, { key = "qtshared", label = "QT 全局配置", ext_db = "_qtSharedDB", ext_global = "QuickToolboxSharedDB" }, { key = "plates", label = "Nanami-Plates", ext_db = "_platesDB", ext_global = "NanamiPlatesDB" }, { key = "dps", label = "Nanami-DPS", ext_db = "_dpsDB", ext_global = "NanamiDPS_DB" }, } local profileApplyFrame local function ShowPartialApplyDialog(profileData, profileName) if not profileApplyFrame then local font = SFrames:GetFont() local f = CreateFrame("Frame", "SFramesProfileApply", UIParent) f:SetWidth(400) f:SetHeight(540) f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) f:SetMovable(true) f:EnableMouse(true) f:SetClampedToScreen(true) f:SetToplevel(true) f:SetFrameStrata("TOOLTIP") f:SetFrameLevel(200) f:RegisterForDrag("LeftButton") f:SetScript("OnDragStart", function() this:StartMoving() end) f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) f: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 }, }) f:SetBackdropColor(0.05, 0.05, 0.08, 0.97) f:SetBackdropBorderColor(0.4, 0.3, 0.5, 0.95) tinsert(UISpecialFrames, "SFramesProfileApply") local title = f:CreateFontString(nil, "OVERLAY") title:SetFont(font, 12, "OUTLINE") title:SetPoint("TOP", f, "TOP", 0, -12) title:SetText("|cffffcc00选择要应用的配置项|r") f.title = title local checks = {} local startY = -36 for idx, cat in ipairs(APPLY_CATEGORIES) do local catKey = cat.key local catLabel = cat.label local cb = CreateFrame("CheckButton", NextWidgetName("ApplyCat"), f, "UICheckButtonTemplate") cb:SetWidth(18) cb:SetHeight(18) local col = (idx <= 10) and 0 or 1 local row = (idx <= 10) and (idx - 1) or (idx - 11) cb:SetPoint("TOPLEFT", f, "TOPLEFT", 16 + col * 180, startY - row * 24) cb:SetChecked(1) cb.sfChecked = true cb.catKey = catKey local text = _G[cb:GetName() .. "Text"] if text then text:SetText(catLabel) text:SetWidth(150) end StyleCheckBox(cb) if cb.sfBox then cb.sfBox:EnableMouse(false) end if cb.sfApplyState then cb.sfApplyState(true) end cb:SetScript("OnClick", function() local v = this:GetChecked() local isChecked = (v == 1 or v == true) this.sfChecked = isChecked if this.sfApplyState then this.sfApplyState(isChecked) end end) table.insert(checks, cb) end f.checks = checks local selectAll = CreateFrame("Button", NextWidgetName("Btn"), f, "UIPanelButtonTemplate") selectAll:SetWidth(80) selectAll:SetHeight(22) selectAll:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -440) selectAll:SetText("全选") StyleButton(selectAll) selectAll:SetScript("OnClick", function() local cks = profileApplyFrame and profileApplyFrame.checks if cks then for i = 1, table.getn(cks) do cks[i]:SetChecked(1) cks[i].sfChecked = true if cks[i].sfApplyState then cks[i].sfApplyState(true) end end end end) local deselectAll = CreateFrame("Button", NextWidgetName("Btn"), f, "UIPanelButtonTemplate") deselectAll:SetWidth(80) deselectAll:SetHeight(22) deselectAll:SetPoint("LEFT", selectAll, "RIGHT", 4, 0) deselectAll:SetText("取消全选") StyleButton(deselectAll) deselectAll:SetScript("OnClick", function() local cks = profileApplyFrame and profileApplyFrame.checks if cks then for i = 1, table.getn(cks) do cks[i]:SetChecked(0) cks[i].sfChecked = false if cks[i].sfApplyState then cks[i].sfApplyState(false) end end end end) local statusLabel = f:CreateFontString(nil, "OVERLAY") statusLabel:SetFont(font, 10, "OUTLINE") statusLabel:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 16, 42) statusLabel:SetWidth(340) statusLabel:SetJustifyH("LEFT") statusLabel:SetText("") f.statusLabel = statusLabel local applyBtn = CreateFrame("Button", NextWidgetName("Btn"), f, "UIPanelButtonTemplate") applyBtn:SetWidth(120) applyBtn:SetHeight(26) applyBtn:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 80, 12) applyBtn:SetText("应用选中项") StyleButton(applyBtn) applyBtn:SetScript("OnClick", function() if not f.profileData then return end local applied = 0 for _, cb in ipairs(f.checks) do if cb:GetChecked() == 1 or cb:GetChecked() == true then local catKey = cb.catKey for _, cat in ipairs(APPLY_CATEGORIES) do if cat.key == catKey then if cat.ext_db then -- External addon DB (QT/Plates/DPS) local srcData = f.profileData[cat.ext_db] if srcData and type(srcData) == "table" then local globalName = cat.ext_global local target = getglobal(globalName) if not target then setglobal(globalName, {}) target = getglobal(globalName) end for k, v in pairs(srcData) do target[k] = v end applied = applied + 1 end elseif cat.chat_keys then -- Chat sub-category: read from profileData.Chat local srcChat = f.profileData.Chat or (f.profileData.db and f.profileData.db.Chat) if srcChat and type(srcChat) == "table" then if not SFramesDB.Chat then SFramesDB.Chat = {} end for _, k in ipairs(cat.chat_keys) do if srcChat[k] ~= nil then SFramesDB.Chat[k] = DeepCopy(srcChat[k]) applied = applied + 1 end end -- chat_tab_keys: copy per-tab sub-fields (e.g. translateFilters) if cat.chat_tab_keys and srcChat.tabs and SFramesDB.Chat.tabs then for ti = 1, table.getn(srcChat.tabs) do local srcTab = srcChat.tabs[ti] local dstTab = SFramesDB.Chat.tabs[ti] if srcTab and dstTab then for _, tk in ipairs(cat.chat_tab_keys) do if srcTab[tk] ~= nil then dstTab[tk] = DeepCopy(srcTab[tk]) end end end end applied = applied + 1 end end elseif cat.table_key then if f.profileData[cat.table_key] then SFramesDB[cat.table_key] = f.profileData[cat.table_key] applied = applied + 1 end elseif cat.keys then for _, k in ipairs(cat.keys) do if f.profileData[k] ~= nil then SFramesDB[k] = f.profileData[k] applied = applied + 1 end end end break end end end end f.statusLabel:SetText("|cff44ff44已应用 " .. applied .. " 项设置,/reload 生效|r") SFrames:Print("已部分应用配置(" .. applied .. " 项),需要 /reload 生效") end) local cancelBtn = CreateFrame("Button", NextWidgetName("Btn"), f, "UIPanelButtonTemplate") cancelBtn:SetWidth(100) cancelBtn:SetHeight(26) cancelBtn:SetPoint("LEFT", applyBtn, "RIGHT", 8, 0) cancelBtn:SetText("取消") StyleButton(cancelBtn) cancelBtn:SetScript("OnClick", function() f:Hide() end) profileApplyFrame = f end profileApplyFrame.profileData = profileData profileApplyFrame.title:SetText("|cffffcc00应用配置: " .. (profileName or "未知") .. "|r") profileApplyFrame.statusLabel:SetText("") for _, cb in ipairs(profileApplyFrame.checks) do cb:SetChecked(1) end profileApplyFrame:Show() profileApplyFrame:Raise() end -------------------------------------------------------------------------------- -- Profile page: Build -------------------------------------------------------------------------------- function SFrames.ConfigUI:BuildProfilePage() local font = SFrames:GetFont() local page = self.profilePage local profileScroll = CreateScrollArea(page, 4, -4, 548, 458, 1800) local root = profileScroll.child self.profileScroll = profileScroll -- ── 角色配置管理 ────────────────────────────────────── local charSection = CreateSection(root, "角色配置管理", 8, -8, 520, 280, font) CreateDesc(charSection, "每个角色的配置会自动保存。你可以查看同账号下其他角色的配置,并选择应用到当前角色。", 14, -30, font, 490) local charListHolder = CreateFrame("Frame", NextWidgetName("CharList"), charSection) charListHolder:SetWidth(492) charListHolder:SetHeight(190) charListHolder:SetPoint("TOPLEFT", charSection, "TOPLEFT", 14, -50) self._charListHolder = charListHolder self._charListButtons = {} local applyBtn = CreateFrame("Button", NextWidgetName("Btn"), charSection, "UIPanelButtonTemplate") applyBtn:SetWidth(120) applyBtn:SetHeight(24) applyBtn:SetPoint("TOPLEFT", charSection, "TOPLEFT", 14, -250) applyBtn:SetText("应用此配置") StyleButton(applyBtn) AddBtnIcon(applyBtn, "save") applyBtn:SetScript("OnClick", function() local sel = SFrames.ConfigUI._selectedCharProfile if not sel then SFrames:Print("请先选择一个角色配置") return end ShowPartialApplyDialog(sel.data, sel.name .. " (" .. sel.realm .. ")") end) local deleteBtn = CreateFrame("Button", NextWidgetName("Btn"), charSection, "UIPanelButtonTemplate") deleteBtn:SetWidth(120) deleteBtn:SetHeight(24) deleteBtn:SetPoint("LEFT", applyBtn, "RIGHT", 8, 0) deleteBtn:SetText("删除此配置") StyleButton(deleteBtn) AddBtnIcon(deleteBtn, "close") deleteBtn:SetScript("OnClick", function() local sel = SFrames.ConfigUI._selectedCharProfile if not sel then SFrames:Print("请先选择一个角色配置") return end SFrames:DeleteCharProfile(sel.realm, sel.name) SFrames:Print("已删除 " .. sel.name .. " (" .. sel.realm .. ") 的配置记录") SFrames.ConfigUI._selectedCharProfile = nil SFrames.ConfigUI:RefreshCharList() end) local exportSelBtn = CreateFrame("Button", NextWidgetName("Btn"), charSection, "UIPanelButtonTemplate") exportSelBtn:SetWidth(120) exportSelBtn:SetHeight(24) exportSelBtn:SetPoint("LEFT", deleteBtn, "RIGHT", 8, 0) exportSelBtn:SetText("导出此配置") StyleButton(exportSelBtn) AddBtnIcon(exportSelBtn, "scroll") exportSelBtn:SetScript("OnClick", function() local sel = SFrames.ConfigUI._selectedCharProfile if sel and sel.data then local cleanDB = {} local data = { charName = sel.name, version = "profile" } for k, v in pairs(sel.data) do if k == "_qtDB" then data.qtDB = v elseif k == "_qtSharedDB" then data.qtSharedDB = v elseif k == "_platesDB" then data.platesDB = v elseif k == "_dpsDB" then data.dpsDB = v else cleanDB[k] = v end end data.db = cleanDB local raw = SerializeValue(data, 0) local text = PROFILE_PREFIX .. P64Encode(raw) if profileExportFrame and profileExportFrame.edit then profileExportFrame.edit:SetText(text) profileExportFrame:Show() profileExportFrame:Raise() profileExportFrame.edit:HighlightText() profileExportFrame.edit:SetFocus() else ShowProfileExportDialog() end else ShowProfileExportDialog() end end) -- ── 导入配置 ────────────────────────────────────────── local importSection = CreateSection(root, "导入配置字符串", 8, -298, 520, 80, font) CreateDesc(importSection, "从其他玩家获取配置字符串后粘贴导入。导入后需要 /reload 生效。", 14, -30, font, 350) local importBtn = CreateFrame("Button", NextWidgetName("Btn"), importSection, "UIPanelButtonTemplate") importBtn:SetWidth(120) importBtn:SetHeight(24) importBtn:SetPoint("TOPLEFT", importSection, "TOPLEFT", 14, -54) importBtn:SetText("粘贴导入") StyleButton(importBtn) AddBtnIcon(importBtn, "scroll") importBtn:SetScript("OnClick", function() ShowProfileImportDialog() end) local exportCurBtn = CreateFrame("Button", NextWidgetName("Btn"), importSection, "UIPanelButtonTemplate") exportCurBtn:SetWidth(140) exportCurBtn:SetHeight(24) exportCurBtn:SetPoint("LEFT", importBtn, "RIGHT", 8, 0) exportCurBtn:SetText("导出当前配置") StyleButton(exportCurBtn) AddBtnIcon(exportCurBtn, "save") exportCurBtn:SetScript("OnClick", function() ShowProfileExportDialog() end) -- ── 配置概览 ────────────────────────────────────────── local summarySection = CreateSection(root, "当前配置概览", 8, -388, 520, 1140, font) local summaryFS = summarySection:CreateFontString(nil, "OVERLAY") summaryFS:SetFont(font, 10, "OUTLINE") summaryFS:SetPoint("TOPLEFT", summarySection, "TOPLEFT", 14, -34) summaryFS:SetWidth(490) summaryFS:SetJustifyH("LEFT") summaryFS:SetSpacing(3) summaryFS:SetText(BuildSummaryText()) self._profileSummaryFS = summaryFS self:RefreshCharList() end function SFrames.ConfigUI:RefreshCharList() if not self._charListHolder then return end local holder = self._charListHolder if self._charListButtons then for _, btn in ipairs(self._charListButtons) do btn:Hide() end end self._charListButtons = {} self._selectedCharProfile = nil local profiles = SFrames:GetAllCharProfiles() local font = SFrames:GetFont() local curRealm, curName = SFrames:GetCharKey() local rowH = 22 local y = 0 table.sort(profiles, function(a, b) if a.realm ~= b.realm then return a.realm < b.realm end return a.name < b.name end) for idx, prof in ipairs(profiles) do local pRealm = prof.realm local pName = prof.name local pClass = prof.class local pLevel = prof.level local pData = prof.data local isCurrent = (pRealm == curRealm and pName == curName) local btn = CreateFrame("Button", NextWidgetName("CharProf"), holder) btn:SetWidth(490) btn:SetHeight(rowH) btn:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, -y) btn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 }, }) if isCurrent then btn:SetBackdropColor(0.15, 0.25, 0.15, 0.7) btn:SetBackdropBorderColor(0.3, 0.5, 0.3, 0.8) else btn:SetBackdropColor(0.08, 0.08, 0.10, 0.5) btn:SetBackdropBorderColor(0.2, 0.2, 0.25, 0.6) end local classColors = { WARRIOR = {0.78, 0.61, 0.43}, PALADIN = {0.96, 0.55, 0.73}, HUNTER = {0.67, 0.83, 0.45}, ROGUE = {1, 0.96, 0.41}, PRIEST = {1, 1, 1}, SHAMAN = {0, 0.44, 0.87}, MAGE = {0.41, 0.80, 0.94}, WARLOCK = {0.58, 0.51, 0.79}, DRUID = {1, 0.49, 0.04}, } local cc = classColors[pClass] or {0.8, 0.8, 0.8} local nameFS = btn:CreateFontString(nil, "OVERLAY") nameFS:SetFont(font, 10, "OUTLINE") nameFS:SetPoint("LEFT", btn, "LEFT", 6, 0) nameFS:SetText(pName) nameFS:SetTextColor(cc[1], cc[2], cc[3]) local infoFS = btn:CreateFontString(nil, "OVERLAY") infoFS:SetFont(font, 9, "OUTLINE") infoFS:SetPoint("LEFT", nameFS, "RIGHT", 8, 0) local infoText = "Lv" .. pLevel .. " " .. pRealm if isCurrent then infoText = infoText .. " |cff44ff44(当前角色)|r" end infoFS:SetText(infoText) infoFS:SetTextColor(0.6, 0.6, 0.6) btn.profileData = { realm = pRealm, name = pName, class = pClass, level = pLevel, data = pData } btn:SetScript("OnClick", function() SFrames.ConfigUI._selectedCharProfile = this.profileData for _, b in ipairs(SFrames.ConfigUI._charListButtons) do b:SetBackdropBorderColor(0.2, 0.2, 0.25, 0.6) end this:SetBackdropBorderColor(1, 0.85, 0.5, 1) end) btn:SetScript("OnEnter", function() this:SetBackdropColor(0.15, 0.15, 0.18, 0.8) end) btn:SetScript("OnLeave", function() local sel = SFrames.ConfigUI._selectedCharProfile if sel and sel.realm == this.profileData.realm and sel.name == this.profileData.name then this:SetBackdropColor(0.18, 0.18, 0.22, 0.8) elseif this.profileData.realm == curRealm and this.profileData.name == curName then this:SetBackdropColor(0.15, 0.25, 0.15, 0.7) else this:SetBackdropColor(0.08, 0.08, 0.10, 0.5) end end) table.insert(self._charListButtons, btn) y = y + rowH + 2 end if table.getn(profiles) == 0 then local emptyFS = holder:CreateFontString(nil, "OVERLAY") emptyFS:SetFont(font, 10, "OUTLINE") emptyFS:SetPoint("TOPLEFT", holder, "TOPLEFT", 4, -4) emptyFS:SetText("暂无其他角色配置。登录其他角色后自动保存。") emptyFS:SetTextColor(0.5, 0.5, 0.5) end end function SFrames.ConfigUI:RefreshProfileSummary() if self._profileSummaryFS then self._profileSummaryFS:SetText(BuildSummaryText()) end if self._charListHolder then self:RefreshCharList() end end function SFrames.ConfigUI:EnsurePage(mode) if self._pageBuilt[mode] then return end self._pageBuilt[mode] = true if mode == "ui" then self:BuildUIPage() elseif mode == "frames" then return elseif mode == "player" then self:BuildPlayerPage() elseif mode == "target" then self:BuildTargetPage() elseif mode == "focus" then self:BuildFocusPage() elseif mode == "party" then self:BuildPartyPage() elseif mode == "bags" then self:BuildBagPage() elseif mode == "raid" then self:BuildRaidPage() elseif mode == "char" then self:BuildCharPage() elseif mode == "actionbar" then self:BuildActionBarPage() elseif mode == "keybinds" then self:BuildKeybindsPage() elseif mode == "minimap" then self:BuildMinimapPage() elseif mode == "buff" then self:BuildBuffPage() elseif mode == "personalize" then self:BuildPersonalizePage() elseif mode == "theme" then self:BuildThemePage() elseif mode == "profile" then self:BuildProfilePage() end end function SFrames.ConfigUI:Build(mode) EnsureDB() self:EnsureFrame() local requested = string.lower(mode or "") if requested == "" or requested == "ui" then requested = ((SFramesDB and SFramesDB.configLastPage) or "ui") end local page = string.lower(requested) local validPages = { ui = true, frames = true, player = true, target = true, focus = true, bags = true, char = true, party = true, raid = true, actionbar = true, keybinds = true, minimap = true, buff = true, personalize = true, theme = true, profile = true } if not validPages[page] then page = "ui" end local activeBucket = page if page == "player" or page == "target" or page == "focus" or page == "party" or page == "raid" then activeBucket = "frames" end if self.frame:IsShown() and self.activePage == activeBucket and (activeBucket ~= "frames" or self.activeFrameSubPage == page or page == "frames") then self.frame:Hide() return end self:ShowPage(page) self.frame:Show() self.frame:Raise() end function SFrames.ConfigUI:OpenUI() self:Build("ui") end function SFrames.ConfigUI:OpenBags() self:Build("bags") end