Files
Nanami-UI/ConfigUI.lua
2026-04-09 09:46:47 +08:00

7008 lines
306 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--------------------------------------------------------------------------------
-- 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