完成多出修改

修复拾取界面点击无效问题
修复宠物训练界面不显示训练点问题
新增天赋分享到聊天界面
修复飞行界面无法关闭问题
修复术士宠物的显示问题
为天赋界面添加默认数据库支持
框架现在也可自主选择是否启用
玩家框架和目标框架可取消显示3D头像以及透明度修改
背包和银行也添加透明度自定义支持
优化tooltip性能和背包物品显示方式
修复布局模式在ui缩放后不能正常定位的问题
添加硬核模式危险和死亡的工会通报
添加拾取和已拾取的框体
等等
This commit is contained in:
rucky
2026-03-23 10:25:25 +08:00
parent 63337b14d2
commit ec9e3c29d6
34 changed files with 13897 additions and 578 deletions

633
KeyBindManager.lua Normal file
View File

@@ -0,0 +1,633 @@
--------------------------------------------------------------------------------
-- Nanami-UI: KeyBind Manager
-- Profile-based keybinding management (save/load/delete/rename/export/import)
-- Covers ALL keybindings, not just action bars.
--------------------------------------------------------------------------------
SFrames.KeyBindManager = {}
local KBM = SFrames.KeyBindManager
--------------------------------------------------------------------------------
-- Data helpers
--------------------------------------------------------------------------------
local function EnsureDB()
if not SFramesGlobalDB then SFramesGlobalDB = {} end
if not SFramesGlobalDB.KeyBindProfiles then SFramesGlobalDB.KeyBindProfiles = {} end
end
local function GetCharName()
return UnitName("player") or "Unknown"
end
local function GetTimestamp()
return time and time() or 0
end
--------------------------------------------------------------------------------
-- Collect / Apply bindings
--------------------------------------------------------------------------------
function KBM:CollectAllBindings()
local result = {}
local n = GetNumBindings()
for i = 1, n do
local command, key1, key2 = GetBinding(i)
if command and (key1 or key2) then
table.insert(result, {
command = command,
key1 = key1,
key2 = key2,
})
end
end
return result
end
function KBM:ClearAllBindings()
local n = GetNumBindings()
for i = 1, n do
local command, key1, key2 = GetBinding(i)
if key2 then SetBinding(key2, nil) end
if key1 then SetBinding(key1, nil) end
end
end
function KBM:ApplyBindings(data)
if not data or type(data) ~= "table" then return false end
self:ClearAllBindings()
local applied = 0
for _, entry in ipairs(data) do
if entry.command then
if entry.key1 then
SetBinding(entry.key1, entry.command)
applied = applied + 1
end
if entry.key2 then
SetBinding(entry.key2, entry.command)
applied = applied + 1
end
end
end
SaveBindings(2)
if SFrames.ActionBars and SFrames.ActionBars.RefreshAllHotkeys then
SFrames.ActionBars:RefreshAllHotkeys()
end
return true, applied
end
--------------------------------------------------------------------------------
-- Base64 encode / decode (pure Lua 5.0 compatible)
--------------------------------------------------------------------------------
local B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local function Base64Encode(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
local c1 = math.floor(n / 262144)
local c2 = math.floor(math.mod(n, 262144) / 4096)
local c3 = math.floor(math.mod(n, 4096) / 64)
local c4 = math.mod(n, 64)
table.insert(out, string.sub(B64, c1 + 1, c1 + 1))
table.insert(out, string.sub(B64, c2 + 1, c2 + 1))
if remaining >= 2 then
table.insert(out, string.sub(B64, c3 + 1, c3 + 1))
else
table.insert(out, "=")
end
if remaining >= 3 then
table.insert(out, string.sub(B64, c4 + 1, c4 + 1))
else
table.insert(out, "=")
end
i = i + 3
end
return table.concat(out)
end
local B64_INV = {}
for i = 1, 64 do
B64_INV[string.sub(B64, i, i)] = i - 1
end
local function Base64Decode(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 = B64_INV[string.sub(src, i, i)] or 0
local v2 = B64_INV[string.sub(src, i + 1, i + 1)] or 0
local v3 = B64_INV[string.sub(src, i + 2, i + 2)]
local v4 = B64_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
--------------------------------------------------------------------------------
-- Serialization (export/import text format, Base64 encoded)
--------------------------------------------------------------------------------
local EXPORT_PREFIX = "!NKB1!"
local SEP = "\t"
local function EncodeRaw(data)
local lines = {}
for _, entry in ipairs(data) do
local line = entry.command
if entry.key1 then
line = line .. SEP .. entry.key1
else
line = line .. SEP
end
if entry.key2 then
line = line .. SEP .. entry.key2
end
table.insert(lines, line)
end
return table.concat(lines, "\n")
end
local function DecodeRaw(raw)
local data = {}
for line in string.gfind(raw .. "\n", "(.-)\n") do
line = string.gsub(line, "\r", "")
if line ~= "" and string.sub(line, 1, 1) ~= "#" then
local parts = {}
for part in string.gfind(line .. SEP, "(.-)" .. SEP) do
table.insert(parts, part)
end
local command = parts[1]
if command and command ~= "" then
local key1 = parts[2]
local key2 = parts[3]
if key1 == "" then key1 = nil end
if key2 == "" then key2 = nil end
if key1 or key2 then
table.insert(data, {
command = command,
key1 = key1,
key2 = key2,
})
end
end
end
end
return data
end
function KBM:SerializeBindings(data)
if not data then data = self:CollectAllBindings() end
local raw = EncodeRaw(data)
return EXPORT_PREFIX .. Base64Encode(raw)
end
function KBM:DeserializeBindings(text)
if not text or text == "" then return nil, "文本为空" end
text = string.gsub(text, "^%s+", "")
text = string.gsub(text, "%s+$", "")
if string.len(text) == 0 then return nil, "文本为空" end
local raw
if string.sub(text, 1, string.len(EXPORT_PREFIX)) == EXPORT_PREFIX then
local encoded = string.sub(text, string.len(EXPORT_PREFIX) + 1)
raw = Base64Decode(encoded)
if not raw or raw == "" then return nil, "解码失败" end
else
raw = text
end
local data = DecodeRaw(raw)
if table.getn(data) == 0 then return nil, "未找到有效的绑定数据" end
return data
end
--------------------------------------------------------------------------------
-- Profile CRUD
--------------------------------------------------------------------------------
function KBM:SaveProfile(name)
if not name or name == "" then return false, "方案名不能为空" end
EnsureDB()
local data = self:CollectAllBindings()
SFramesGlobalDB.KeyBindProfiles[name] = {
timestamp = GetTimestamp(),
charName = GetCharName(),
bindings = data,
}
SFrames:Print("按键绑定方案已保存: |cffffd100" .. name .. "|r (" .. table.getn(data) .. " 条)")
return true
end
function KBM:LoadProfile(name)
if not name or name == "" then return false, "方案名不能为空" end
EnsureDB()
local profile = SFramesGlobalDB.KeyBindProfiles[name]
if not profile then return false, "方案不存在: " .. name end
local ok, count = self:ApplyBindings(profile.bindings)
if ok then
SFrames:Print("按键绑定方案已加载: |cffffd100" .. name .. "|r (" .. count .. " 个按键)")
end
return ok
end
function KBM:DeleteProfile(name)
if not name or name == "" then return false end
EnsureDB()
if not SFramesGlobalDB.KeyBindProfiles[name] then return false end
SFramesGlobalDB.KeyBindProfiles[name] = nil
SFrames:Print("按键绑定方案已删除: |cffffd100" .. name .. "|r")
return true
end
function KBM:RenameProfile(oldName, newName)
if not oldName or oldName == "" or not newName or newName == "" then
return false, "名称不能为空"
end
EnsureDB()
local profiles = SFramesGlobalDB.KeyBindProfiles
if not profiles[oldName] then return false, "方案不存在" end
if profiles[newName] then return false, "目标名称已存在" end
profiles[newName] = profiles[oldName]
profiles[oldName] = nil
SFrames:Print("按键绑定方案已重命名: |cffffd100" .. oldName .. "|r -> |cffffd100" .. newName .. "|r")
return true
end
function KBM:GetProfileList()
EnsureDB()
local list = {}
for name, _ in pairs(SFramesGlobalDB.KeyBindProfiles) do
table.insert(list, name)
end
table.sort(list)
return list
end
function KBM:GetProfileInfo(name)
EnsureDB()
local p = SFramesGlobalDB.KeyBindProfiles[name]
if not p then return nil end
return {
name = name,
charName = p.charName or "?",
timestamp = p.timestamp or 0,
count = p.bindings and table.getn(p.bindings) or 0,
}
end
--------------------------------------------------------------------------------
-- Export / Import Dialog UI
--------------------------------------------------------------------------------
local exportFrame, importFrame
local function CreateDialogFrame(name, titleText, width, height)
local f = CreateFrame("Frame", name, UIParent)
f:SetWidth(width)
f:SetHeight(height)
f:SetPoint("CENTER", UIParent, "CENTER", 0, 80)
f:SetFrameStrata("TOOLTIP")
f:SetFrameLevel(200)
f:SetToplevel(true)
f:EnableMouse(true)
f:SetMovable(true)
f:SetClampedToScreen(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function() this:StartMoving() end)
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
if SFrames and SFrames.CreateBackdrop then
SFrames:CreateBackdrop(f)
else
f:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
f:SetBackdropColor(0.08, 0.07, 0.1, 0.96)
f:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.9)
end
local font = (SFrames and SFrames.GetFont) and SFrames:GetFont() or "Fonts\\ARIALN.TTF"
local title = f:CreateFontString(nil, "OVERLAY")
title:SetFont(font, 13, "OUTLINE")
title:SetPoint("TOP", f, "TOP", 0, -10)
title:SetText(titleText)
local _T = SFrames.ActiveTheme
if _T and _T.title then
title:SetTextColor(_T.title[1], _T.title[2], _T.title[3])
else
title:SetTextColor(1, 0.82, 0)
end
f.title = title
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -2, -2)
close:SetWidth(20)
close:SetHeight(20)
return f, font
end
local function CreateThemedButton(parent, text, x, y, width, height, onClick)
local name = "SFramesKBM_Btn_" .. string.gsub(text, "%s", "") .. "_" .. tostring(math.random(10000, 99999))
local btn = CreateFrame("Button", name, parent, "UIPanelButtonTemplate")
btn:SetWidth(width)
btn:SetHeight(height)
btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
btn:SetText(text)
btn:SetScript("OnClick", onClick)
local font = (SFrames and SFrames.GetFont) and SFrames:GetFont() or "Fonts\\ARIALN.TTF"
local _T = SFrames.ActiveTheme
if not _T then return btn end
local function HideBtnTex(tex)
if not tex then return end
if tex.SetTexture then tex:SetTexture(nil) end
if tex.SetAlpha then tex:SetAlpha(0) end
if tex.Hide then tex:Hide() end
end
HideBtnTex(btn:GetNormalTexture())
HideBtnTex(btn:GetPushedTexture())
HideBtnTex(btn:GetHighlightTexture())
HideBtnTex(btn:GetDisabledTexture())
for _, sfx in ipairs({"Left","Right","Middle"}) do
local t = _G[name .. sfx]
if t then t:SetAlpha(0); t:Hide() end
end
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 },
})
btn:SetBackdropColor(_T.btnBg[1], _T.btnBg[2], _T.btnBg[3], _T.btnBg[4])
btn:SetBackdropBorderColor(_T.btnBorder[1], _T.btnBorder[2], _T.btnBorder[3], _T.btnBorder[4])
local fs = btn:GetFontString()
if fs then
fs:SetFont(font, 11, "OUTLINE")
fs:SetTextColor(_T.btnText[1], _T.btnText[2], _T.btnText[3])
end
btn:SetScript("OnEnter", function()
this:SetBackdropColor(_T.btnHoverBg[1], _T.btnHoverBg[2], _T.btnHoverBg[3], _T.btnHoverBg[4])
this:SetBackdropBorderColor(_T.btnHoverBd[1], _T.btnHoverBd[2], _T.btnHoverBd[3], _T.btnHoverBd[4])
local t = this:GetFontString()
if t then t:SetTextColor(_T.btnActiveText[1], _T.btnActiveText[2], _T.btnActiveText[3]) end
end)
btn:SetScript("OnLeave", function()
this:SetBackdropColor(_T.btnBg[1], _T.btnBg[2], _T.btnBg[3], _T.btnBg[4])
this:SetBackdropBorderColor(_T.btnBorder[1], _T.btnBorder[2], _T.btnBorder[3], _T.btnBorder[4])
local t = this:GetFontString()
if t then t:SetTextColor(_T.btnText[1], _T.btnText[2], _T.btnText[3]) end
end)
btn:SetScript("OnMouseDown", function()
this:SetBackdropColor(_T.btnDownBg[1], _T.btnDownBg[2], _T.btnDownBg[3], _T.btnDownBg[4])
end)
btn:SetScript("OnMouseUp", function()
this:SetBackdropColor(_T.btnHoverBg[1], _T.btnHoverBg[2], _T.btnHoverBg[3], _T.btnHoverBg[4])
end)
return btn
end
function KBM:ShowExportDialog()
local text = self:SerializeBindings()
if not exportFrame then
local f, font = CreateDialogFrame("SFramesKBMExport", "|cffffcc00导出按键绑定|r", 480, 340)
local desc = f:CreateFontString(nil, "OVERLAY")
desc:SetFont(font, 10, "OUTLINE")
desc:SetPoint("TOP", f.title, "BOTTOM", 0, -4)
desc:SetText("已编码为字符串Ctrl+A 全选Ctrl+C 复制")
desc:SetTextColor(0.7, 0.7, 0.7)
local sf = CreateFrame("ScrollFrame", "SFramesKBMExportScroll", f, "UIPanelScrollFrameTemplate")
sf:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -48)
sf:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -32, 42)
local edit = CreateFrame("EditBox", "SFramesKBMExportEdit", sf)
edit:SetWidth(420)
edit:SetHeight(200)
edit:SetMultiLine(true)
edit:SetFont(font, 10, "OUTLINE")
edit:SetAutoFocus(false)
edit:SetScript("OnEscapePressed", function() f:Hide() end)
sf:SetScrollChild(edit)
f.edit = edit
CreateThemedButton(f, "关闭", 190, -306, 100, 26, function()
f:Hide()
end)
exportFrame = f
end
exportFrame.edit:SetText(text)
exportFrame:Show()
exportFrame.edit:HighlightText()
exportFrame.edit:SetFocus()
end
function KBM:ShowImportDialog()
if not importFrame then
local f, font = CreateDialogFrame("SFramesKBMImport", "|cffffcc00导入按键绑定|r", 480, 370)
local desc = f:CreateFontString(nil, "OVERLAY")
desc:SetFont(font, 10, "OUTLINE")
desc:SetPoint("TOP", f.title, "BOTTOM", 0, -4)
desc:SetText("将编码后的字符串粘贴到下方 (Ctrl+V),然后点击导入")
desc:SetTextColor(0.7, 0.7, 0.7)
local sf = CreateFrame("ScrollFrame", "SFramesKBMImportScroll", f, "UIPanelScrollFrameTemplate")
sf:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -48)
sf:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -32, 72)
local edit = CreateFrame("EditBox", "SFramesKBMImportEdit", sf)
edit:SetWidth(420)
edit:SetHeight(200)
edit:SetMultiLine(true)
edit:SetFont(font, 10, "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", 14, 46)
statusLabel:SetWidth(300)
statusLabel:SetJustifyH("LEFT")
statusLabel:SetText("")
f.statusLabel = statusLabel
CreateThemedButton(f, "导入并应用", 120, -336, 120, 26, function()
local inputText = f.edit:GetText()
if not inputText or inputText == "" then
f.statusLabel:SetText("|cffff4444请先粘贴绑定数据|r")
return
end
local data, err = KBM:DeserializeBindings(inputText)
if not data then
f.statusLabel:SetText("|cffff4444错误: " .. (err or "未知") .. "|r")
return
end
local ok, count = KBM:ApplyBindings(data)
if ok then
f.statusLabel:SetText("|cff44ff44成功导入 " .. count .. " 个按键绑定|r")
SFrames:Print("已从文本导入 " .. count .. " 个按键绑定")
else
f.statusLabel:SetText("|cffff4444导入失败|r")
end
end)
CreateThemedButton(f, "取消", 250, -336, 100, 26, function()
f:Hide()
end)
importFrame = f
end
importFrame.edit:SetText("")
importFrame.statusLabel:SetText("")
importFrame:Show()
importFrame.edit:SetFocus()
end
--------------------------------------------------------------------------------
-- Input name dialog (for save / rename)
--------------------------------------------------------------------------------
local inputDialog
local function ShowInputDialog(titleText, defaultText, callback)
if not inputDialog then
local f, font = CreateDialogFrame("SFramesKBMInput", "", 360, 120)
local edit = CreateFrame("EditBox", "SFramesKBMInputEdit", f)
edit:SetWidth(320)
edit:SetHeight(24)
edit:SetPoint("TOP", f, "TOP", 0, -40)
edit:SetFont(font, 12, "OUTLINE")
edit:SetAutoFocus(true)
edit:SetMaxLetters(50)
edit: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 },
})
edit:SetBackdropColor(0.1, 0.1, 0.1, 0.9)
edit:SetBackdropBorderColor(0.5, 0.5, 0.5, 0.8)
edit:SetTextInsets(6, 6, 0, 0)
f.edit = edit
CreateThemedButton(f, "确定", 90, -78, 80, 24, function()
local val = f.edit:GetText()
if val and val ~= "" and f.callback then
f.callback(val)
end
f:Hide()
end)
CreateThemedButton(f, "取消", 185, -78, 80, 24, function()
f:Hide()
end)
edit:SetScript("OnEscapePressed", function() f:Hide() end)
edit:SetScript("OnEnterPressed", function()
local val = f.edit:GetText()
if val and val ~= "" and f.callback then
f.callback(val)
end
f:Hide()
end)
inputDialog = f
end
inputDialog.title:SetText(titleText)
inputDialog.edit:SetText(defaultText or "")
inputDialog.callback = callback
inputDialog:Show()
inputDialog.edit:SetFocus()
inputDialog.edit:HighlightText()
end
--------------------------------------------------------------------------------
-- Confirm dialog
--------------------------------------------------------------------------------
local confirmDialog
local function ShowConfirmDialog(message, callback)
if not confirmDialog then
local f, font = CreateDialogFrame("SFramesKBMConfirm", "", 360, 110)
local msg = f:CreateFontString(nil, "OVERLAY")
msg:SetFont(font, 11, "OUTLINE")
msg:SetPoint("TOP", f, "TOP", 0, -34)
msg:SetWidth(320)
msg:SetJustifyH("CENTER")
msg:SetTextColor(0.9, 0.9, 0.9)
f.msg = msg
CreateThemedButton(f, "确定", 90, -70, 80, 24, function()
if f.callback then f.callback() end
f:Hide()
end)
CreateThemedButton(f, "取消", 185, -70, 80, 24, function()
f:Hide()
end)
confirmDialog = f
end
confirmDialog.title:SetText("|cffffcc00确认|r")
confirmDialog.msg:SetText(message)
confirmDialog.callback = callback
confirmDialog:Show()
end
KBM.ShowInputDialog = ShowInputDialog
KBM.ShowConfirmDialog = ShowConfirmDialog