mirror of
https://github.com/brues-code/pfUI.git
synced 2026-09-22 07:36:56 +00:00
73b409fb88
The RegisterSlashCommand helper in api/api.lua was effectively unused (only macrotweak called it); every other command hand-rolled the SLASH_*/SlashCmdList pair. Convert the existing manual registrations to the helper with force=true, preserving the current always-bind behavior while centralizing the pattern behind one code path (and its _G. and conflict-check handling). Left as-is: pfUI.lua's /rl, /pfui, /gm (registered before api.lua defines the helper) and the vendored libs' debug commands.
397 lines
14 KiB
Lua
397 lines
14 KiB
Lua
pfUI:RegisterModule("share", function ()
|
|
-- Profile sharing rides ClassicAPI's C_EncodingUtil:
|
|
-- export: diff vs defaults -> SerializeCBOR -> CompressString (zlib)
|
|
-- -> EncodeBase64, prefixed with the format magic
|
|
-- import: the reverse, into a plain table — no loadstring, so a
|
|
-- pasted profile can't execute code
|
|
-- The Decode/Encode button converts between the transport blob and an
|
|
-- editable JSON view of the same table.
|
|
local MAGIC = "!pf1!"
|
|
|
|
-- Deep-diff `tbl` against `default`, keeping only changed or added
|
|
-- values. `ignored` keys are skipped at the top level only (matches the
|
|
-- old serializer: "position"/"disabled" when Ignore Layout is checked).
|
|
-- Returns nil when nothing differs.
|
|
local function DiffConfig(tbl, default, ignored)
|
|
local diff = nil
|
|
for k, v in pairs(tbl) do
|
|
if not ( ignored and ignored[k] ) then
|
|
local dv = default and default[k]
|
|
if type(v) == "table" then
|
|
local sub = DiffConfig(v, type(dv) == "table" and dv or nil)
|
|
if sub then
|
|
diff = diff or {}
|
|
diff[k] = sub
|
|
end
|
|
elseif v ~= dv and ( type(v) == "string" or type(v) == "number" or type(v) == "boolean" ) then
|
|
diff = diff or {}
|
|
diff[k] = v
|
|
end
|
|
end
|
|
end
|
|
return diff
|
|
end
|
|
|
|
-- EditBoxes don't soft-wrap one giant unbroken line; chunk the blob.
|
|
local function wrap(str, width)
|
|
local out = {}
|
|
for i = 1, strlen(str), width do
|
|
table.insert(out, strsub(str, i, i + width - 1))
|
|
end
|
|
return table.concat(out, "\n")
|
|
end
|
|
|
|
local function Encode(tbl)
|
|
return MAGIC .. wrap(C_EncodingUtil.EncodeBase64(
|
|
C_EncodingUtil.CompressString(
|
|
C_EncodingUtil.SerializeCBOR(tbl))), 92)
|
|
end
|
|
|
|
local function Decode(text)
|
|
if not text then return nil end
|
|
text = gsub(text, "%s", "")
|
|
if strsub(text, 1, strlen(MAGIC)) ~= MAGIC then return nil end
|
|
local ok, result = pcall(function()
|
|
return C_EncodingUtil.DeserializeCBOR(
|
|
C_EncodingUtil.DecompressString(
|
|
C_EncodingUtil.DecodeBase64(strsub(text, strlen(MAGIC) + 1))))
|
|
end)
|
|
if ok and type(result) == "table" then return result end
|
|
return nil
|
|
end
|
|
|
|
local function DecodeJSON(text)
|
|
if not text or gsub(text, "%s", "") == "" then return nil end
|
|
local ok, result = pcall(function()
|
|
return C_EncodingUtil.DeserializeJSON(text)
|
|
end)
|
|
if ok and type(result) == "table" then return result end
|
|
return nil
|
|
end
|
|
|
|
-- Legacy import (pre-!pf1! exports): base64 -> LZW -> Lua source
|
|
-- "pfUI_config = {...}". The base64 layer is standard and handled by
|
|
-- C_EncodingUtil.DecodeBase64; only the custom LZW format needs the old
|
|
-- Lua decoder. Import-only — new exports always use the CBOR pipeline.
|
|
local function decompress(input)
|
|
-- based on Rochet2's lzw compression
|
|
if type(input) ~= "string" or strlen(input) < 1 then
|
|
return nil
|
|
end
|
|
|
|
local control = strsub(input, 1, 1)
|
|
if control == "u" then
|
|
return strsub(input, 2)
|
|
elseif control ~= "c" then
|
|
return nil
|
|
end
|
|
input = strsub(input, 2)
|
|
local len = strlen(input)
|
|
|
|
if len < 2 then
|
|
return nil
|
|
end
|
|
|
|
local dict = {}
|
|
for i = 0, 255 do
|
|
local ic, iic = strchar(i), strchar(i, 0)
|
|
dict[iic] = ic
|
|
end
|
|
|
|
local a, b = 0, 1
|
|
|
|
local result = {}
|
|
local n = 1
|
|
local last = strsub(input, 1, 2)
|
|
result[n] = dict[last]
|
|
n = n+1
|
|
for i = 3, len, 2 do
|
|
local code = strsub(input, i, i+1)
|
|
local lastStr = dict[last]
|
|
if not lastStr then
|
|
return nil
|
|
end
|
|
local toAdd = dict[code]
|
|
if toAdd then
|
|
result[n] = toAdd
|
|
n = n+1
|
|
local str = lastStr..strsub(toAdd, 1, 1)
|
|
if a >= 256 then
|
|
a, b = 0, b+1
|
|
if b >= 256 then
|
|
dict = {}
|
|
b = 1
|
|
end
|
|
end
|
|
dict[strchar(a,b)] = str
|
|
a = a+1
|
|
else
|
|
local str = lastStr..strsub(lastStr, 1, 1)
|
|
result[n] = str
|
|
n = n+1
|
|
if a >= 256 then
|
|
a, b = 0, b+1
|
|
if b >= 256 then
|
|
dict = {}
|
|
b = 1
|
|
end
|
|
end
|
|
dict[strchar(a,b)] = str
|
|
a = a+1
|
|
end
|
|
last = code
|
|
end
|
|
return table.concat(result)
|
|
end
|
|
|
|
local function DecodeLegacy(text)
|
|
if not text or gsub(text, "%s", "") == "" then return nil end
|
|
|
|
-- encoded blob? peel base64 + LZW down to Lua source. Raw (already
|
|
-- decoded) source pastes are accepted as-is.
|
|
local source = text
|
|
local stripped = gsub(text, "%s", "")
|
|
local ok, decoded = pcall(function()
|
|
return C_EncodingUtil.DecodeBase64(stripped)
|
|
end)
|
|
if ok and decoded then
|
|
local decompressed = decompress(decoded)
|
|
if decompressed then source = decompressed end
|
|
end
|
|
|
|
local chunk = loadstring(source)
|
|
if not chunk then return nil end
|
|
|
|
-- Sandbox the chunk: an empty environment means it can assign its
|
|
-- config table but can't reach any global or API function — a legacy
|
|
-- profile string is data, never code.
|
|
local sandbox = {}
|
|
setfenv(chunk, sandbox)
|
|
if not pcall(chunk) then return nil end
|
|
if type(sandbox.pfUI_config) == "table" then return sandbox.pfUI_config end
|
|
return nil
|
|
end
|
|
|
|
do -- Window
|
|
local f = CreateFrame("Frame", "pfShare", UIParent)
|
|
f:Hide()
|
|
f:SetPoint("CENTER", 0, 0)
|
|
f:SetWidth(580)
|
|
f:SetHeight(420)
|
|
f:SetMovable(true)
|
|
f:EnableMouse(true)
|
|
f:RegisterForDrag("LeftButton")
|
|
f:SetScript("OnDragStart", function() f:StartMoving() end)
|
|
f:SetScript("OnDragStop", function() f:StopMovingOrSizing() end)
|
|
f:SetFrameStrata("DIALOG")
|
|
|
|
f:SetScript("OnShow", function()
|
|
if pfUI.gui and pfUI.gui:IsShown() then
|
|
f.hadGUI = true
|
|
pfUI.gui:Hide()
|
|
else
|
|
f.hadGUI = nil
|
|
end
|
|
end)
|
|
|
|
f:SetScript("OnHide", function()
|
|
if f.hadGUI then
|
|
pfUI.gui:Show()
|
|
end
|
|
end)
|
|
|
|
CreateBackdrop(f, nil, true, 0.8)
|
|
CreateBackdropShadow(f)
|
|
table.insert(UISpecialFrames, "pfShare")
|
|
|
|
do -- Edit Box
|
|
f.scroll = pfUI.api.CreateScrollFrame("pfShareScroll", f)
|
|
f.scroll:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -30)
|
|
f.scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -10, 50)
|
|
f.scroll:SetWidth(560)
|
|
f.scroll:SetHeight(400)
|
|
|
|
f.scroll.backdrop = CreateFrame("Frame", "pfShareScrollBackdrop", f.scroll)
|
|
f.scroll.backdrop:SetFrameLevel(1)
|
|
f.scroll.backdrop:SetPoint("TOPLEFT", f.scroll, "TOPLEFT", -5, 5)
|
|
f.scroll.backdrop:SetPoint("BOTTOMRIGHT", f.scroll, "BOTTOMRIGHT", 5, -5)
|
|
pfUI.api.CreateBackdrop(f.scroll.backdrop, nil, true)
|
|
|
|
f.scroll.text = CreateFrame("EditBox", "pfShareEditBox", f.scroll)
|
|
f.scroll.text.bg = f.scroll.text:CreateTexture(nil, "OVERLAY")
|
|
f.scroll.text.bg:SetAllPoints(f.scroll.text)
|
|
f.scroll.text.bg:SetTexture(1,1,1,.05)
|
|
f.scroll.text:SetMultiLine(true)
|
|
f.scroll.text:SetWidth(560)
|
|
f.scroll.text:SetHeight(400)
|
|
f.scroll.text:SetAllPoints(f.scroll)
|
|
f.scroll.text:SetTextInsets(15,15,15,15)
|
|
f.scroll.text:SetFont(pfUI.media["font:RobotoMono.ttf"], 9)
|
|
f.scroll.text:SetAutoFocus(false)
|
|
f.scroll.text:SetJustifyH("LEFT")
|
|
f.scroll.text:SetScript("OnEscapePressed", function() this:ClearFocus() end)
|
|
f.scroll.text:SetScript("OnTextChanged", function()
|
|
this:GetParent():UpdateScrollChildRect()
|
|
this:GetParent():UpdateScrollState()
|
|
|
|
local text = this:GetText()
|
|
local blob = Decode(text)
|
|
local json = not blob and DecodeJSON(text) or nil
|
|
local legacy = not blob and not json and DecodeLegacy(text) or nil
|
|
|
|
if blob or json or legacy then
|
|
f.loadButton:Enable()
|
|
f.loadButton.text:SetTextColor(.5,1,.5,1)
|
|
else
|
|
f.loadButton:Disable()
|
|
f.loadButton.text:SetTextColor(1,.5,.5,1)
|
|
end
|
|
|
|
if blob or legacy then
|
|
-- decoding a legacy string yields the JSON view; re-encoding it
|
|
-- from there produces a new-format blob (migration path)
|
|
f.readButton:Enable()
|
|
f.readButton.text:SetText(T["Decode"])
|
|
f.readButton.func = function()
|
|
local current = f.scroll.text:GetText()
|
|
local config = Decode(current) or DecodeLegacy(current)
|
|
if config then
|
|
f.scroll.text:SetText(C_EncodingUtil.SerializeJSON(config))
|
|
end
|
|
end
|
|
elseif json then
|
|
f.readButton:Enable()
|
|
f.readButton.text:SetText(T["Encode"])
|
|
f.readButton.func = function()
|
|
local config = DecodeJSON(f.scroll.text:GetText())
|
|
if config then
|
|
f.scroll.text:SetText(Encode(config))
|
|
end
|
|
end
|
|
else
|
|
f.readButton:Disable()
|
|
f.readButton.text:SetText(T["N/A"])
|
|
end
|
|
end)
|
|
f.scroll:SetScrollChild(f.scroll.text)
|
|
end
|
|
|
|
do -- button: close
|
|
f.closeButton = CreateFrame("Button", "pfShareClose", f)
|
|
f.closeButton:SetPoint("TOPRIGHT", -5, -5)
|
|
f.closeButton:SetHeight(12)
|
|
f.closeButton:SetWidth(12)
|
|
f.closeButton.texture = f.closeButton:CreateTexture("pfQuestionDialogCloseTex")
|
|
f.closeButton.texture:SetTexture(pfUI.media["img:close"])
|
|
f.closeButton.texture:ClearAllPoints()
|
|
f.closeButton.texture:SetAllPoints(f.closeButton)
|
|
f.closeButton.texture:SetVertexColor(1,.25,.25,1)
|
|
pfUI.api.SkinButton(f.closeButton, 1, .5, .5)
|
|
f.closeButton:SetScript("OnClick", function()
|
|
this:GetParent():Hide()
|
|
end)
|
|
end
|
|
|
|
do -- checkbox: ignore positions
|
|
f.ignorePosition = CreateFrame("CheckButton", "pfShareIgnorePosition", f, "UICheckButtonTemplate")
|
|
f.ignorePosition:SetNormalTexture("")
|
|
f.ignorePosition:SetPushedTexture("")
|
|
f.ignorePosition:SetHighlightTexture("")
|
|
CreateBackdrop(f.ignorePosition, nil, true)
|
|
f.ignorePosition:SetWidth(14)
|
|
f.ignorePosition:SetHeight(14)
|
|
f.ignorePosition:SetPoint("BOTTOMLEFT", 10, 10)
|
|
|
|
f.ignorePositionCaption = f.ignorePosition:CreateFontString("Status", "LOW", "GameFontNormal")
|
|
f.ignorePositionCaption:SetFont(pfUI.font_default, C.global.font_size + 2, "OUTLINE")
|
|
f.ignorePositionCaption:SetPoint("LEFT", f.ignorePosition, "RIGHT", 5, 0)
|
|
f.ignorePositionCaption:SetFontObject(GameFontWhite)
|
|
f.ignorePositionCaption:SetJustifyH("LEFT")
|
|
f.ignorePositionCaption:SetText(T["Ignore Layout"])
|
|
end
|
|
|
|
|
|
do -- button: load
|
|
f.loadButton = CreateFrame("Button", "pfShareLoad", f)
|
|
pfUI.api.SkinButton(f.loadButton)
|
|
f.loadButton:SetPoint("BOTTOMRIGHT", -5, 5)
|
|
f.loadButton:SetWidth(75)
|
|
f.loadButton:SetHeight(25)
|
|
f.loadButton.text = f.loadButton:CreateFontString("Caption", "LOW", "GameFontWhite")
|
|
f.loadButton.text:SetAllPoints(f.loadButton)
|
|
f.loadButton.text:SetFont(pfUI.font_default, pfUI_config.global.font_size, "OUTLINE")
|
|
f.loadButton.text:SetText(T["Import"])
|
|
f.loadButton:SetScript("OnClick", function()
|
|
local text = f.scroll.text:GetText()
|
|
local config = Decode(text) or DecodeJSON(text) or DecodeLegacy(text)
|
|
if not config then return end
|
|
|
|
_G.pfUI_config = config
|
|
C = _G.pfUI_config
|
|
pfUI:LoadConfig()
|
|
|
|
-- Skip firstrun wizard when importing a shared profile
|
|
-- The imported config is a complete setup, no wizard needed
|
|
if pfUI.firstrun and pfUI.firstrun.steps then
|
|
for _, step in pairs(pfUI.firstrun.steps) do
|
|
pfUI_init[step.name] = true
|
|
end
|
|
end
|
|
|
|
CreateQuestionDialog(T["Some settings need to reload the UI to take effect.\nDo you want to reloadUI now?"], ReloadUI)
|
|
end)
|
|
end
|
|
|
|
do -- button: read
|
|
f.readButton = CreateFrame("Button", "pfShareDecode", f)
|
|
pfUI.api.SkinButton(f.readButton)
|
|
f.readButton:SetPoint("RIGHT", f.loadButton, "LEFT", -10, 0)
|
|
f.readButton:SetWidth(75)
|
|
f.readButton:SetHeight(25)
|
|
f.readButton.text = f.readButton:CreateFontString("Caption", "LOW", "GameFontWhite")
|
|
f.readButton.text:SetAllPoints(f.readButton)
|
|
f.readButton.text:SetFont(pfUI.font_default, pfUI_config.global.font_size, "OUTLINE")
|
|
f.readButton.text:SetText(T["N/A"])
|
|
f.readButton:SetScript("OnClick", function()
|
|
this.func()
|
|
end)
|
|
end
|
|
|
|
do -- button: export
|
|
f.exportButton = CreateFrame("Button", "pfShareExport", f)
|
|
pfUI.api.SkinButton(f.exportButton)
|
|
f.exportButton:SetPoint("RIGHT", f.readButton, "LEFT", -10, 0)
|
|
f.exportButton:SetWidth(75)
|
|
f.exportButton:SetHeight(25)
|
|
f.exportButton.text = f.exportButton:CreateFontString("Caption", "LOW", "GameFontWhite")
|
|
f.exportButton.text:SetAllPoints(f.exportButton)
|
|
f.exportButton.text:SetFont(pfUI.font_default, pfUI_config.global.font_size, "OUTLINE")
|
|
f.exportButton.text:SetText(T["Export"])
|
|
f.exportButton:SetScript("OnClick", function()
|
|
-- generate a default config
|
|
local myconfig = CopyTable(pfUI_config)
|
|
_G.pfUI_config = {}
|
|
pfUI:LoadConfig()
|
|
local defconfig = CopyTable(pfUI_config)
|
|
|
|
-- restore config and references
|
|
_G.pfUI_config = CopyTable(myconfig)
|
|
C = _G.pfUI_config
|
|
|
|
local ignored = {}
|
|
ignored["position"] = f.ignorePosition:GetChecked()
|
|
ignored["disabled"] = f.ignorePosition:GetChecked()
|
|
|
|
local encoded = Encode(DiffConfig(myconfig, defconfig, ignored) or {})
|
|
f.scroll.text:SetText(encoded)
|
|
f.scroll.text.value = encoded
|
|
f.scroll:SetVerticalScroll(0)
|
|
end)
|
|
end
|
|
|
|
pfUI.api.RegisterSlashCommand("PFEXPORT", { "/export", "/import", "/share" }, function(msg, editbox)
|
|
f:Show()
|
|
end, true)
|
|
end
|
|
end)
|