From fecf4da2b9e6337b26ec0a684f494ffc4f03a2e8 Mon Sep 17 00:00:00 2001 From: Ade <923+ade@noreply.octowow.st> Date: Thu, 8 Oct 2026 06:07:07 +0000 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?/=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sync.lua | 355 ++++++++++++++++++++++++++++++++++++++++++++++++++ ui_base.lua | 311 +++++++++++++++++++++++++++++++++++++++++++ ui_config.lua | 163 +++++++++++++++++++++++ ui_equip.lua | 82 ++++++++++++ ui_layout.lua | 97 ++++++++++++++ 5 files changed, 1008 insertions(+) create mode 100644 sync.lua create mode 100644 ui_base.lua create mode 100644 ui_config.lua create mode 100644 ui_equip.lua create mode 100644 ui_layout.lua diff --git a/sync.lua b/sync.lua new file mode 100644 index 0000000..f8ff9cc --- /dev/null +++ b/sync.lua @@ -0,0 +1,355 @@ +-- Ade Alts: sync +local AA = AdeAlts + +---------------------------------------------------------------------- +-- sync between accounts (guild / party / raid addon messages) +---------------------------------------------------------------------- +local function Encode(c) + local head = "C^" .. c.name .. "^" .. c.realm .. "^" .. (c.class or "") .. "^" .. (c.updated or 0) .. "^" + local msgs, cur = {}, "" + for key, exp in pairs(c.cds) do + local pair = key .. "=" .. exp + if string.len(head) + string.len(cur) + string.len(pair) + 1 > 240 then + table.insert(msgs, head .. cur) + cur = "" + end + if cur ~= "" then cur = cur .. "~" end + cur = cur .. pair + end + table.insert(msgs, head .. cur) + return msgs +end + +function AA.Send(msg) + if IsInGuild and IsInGuild() then SendAddonMessage(AA.PREFIX, msg, "GUILD") end + if GetNumRaidMembers() > 0 then + SendAddonMessage(AA.PREFIX, msg, "RAID") + elseif GetNumPartyMembers() > 0 then + SendAddonMessage(AA.PREFIX, msg, "PARTY") + end +end + +function AA.Broadcast() + if not AA.me then return end + AA.lastBroadcast = GetTime() + for _, m in ipairs(Encode(AA.me)) do AA.Send(m) end +end + +function AA.ReceiveRecord(text) + local _, _, name, realm, class, upd, body = string.find(text, "^C%^([^%^]*)%^([^%^]*)%^([^%^]*)%^(%d*)%^(.*)$") + if not name or name == "" then return false end + upd = tonumber(upd) or 0 + local k = name .. "-" .. realm + local c = AA.db.chars[k] + if c and (c.updated or 0) > upd then return false end -- we already have newer data + if not c then + c = { name = name, realm = realm, class = class, cds = {}, remote = true } + AA.db.chars[k] = c + end + if class ~= "" then c.class = class end + c.cds = c.cds or {} + for key, exp in string.gfind(body, "([^~=]+)=(%d+)") do + c.cds[key] = tonumber(exp) + end + c.updated = upd + AA.ClampRecord(c) + return true +end + +-- shared file between accounts on the same game folder (needs SuperWoW) +local function InvStr(list) + if not list then return "" end + local parts = {} + for _, e in ipairs(list) do table.insert(parts, e[1] .. ":" .. e[2]) end + return table.concat(parts, "~") +end + +local function ParseInv(str) + local list = {} + for id, n in string.gfind(str or "", "(%d+):(%d+)") do + table.insert(list, { tonumber(id), tonumber(n) }) + end + return list +end + +local function EncodeAlt(c) + return "A^" .. c.name .. "^" .. c.realm .. "^" .. (c.class or "") .. "^" .. (c.level or 0) .. "^" .. (c.gold or 0) + .. "^" .. (c.dataTs or 0) .. "^" .. (c.invTs or 0) .. "^" .. (c.bankTs or 0) + .. "^" .. InvStr(c.inv) .. "^" .. InvStr(c.bank) +end + +local function ReceiveAlt(text) + local _, _, name, realm, class, lvl, gold, dts, its, bts, inv, bank = + string.find(text, "^A%^([^%^]*)%^([^%^]*)%^([^%^]*)%^(%d*)%^(%d*)%^(%d*)%^(%d*)%^(%d*)%^([^%^]*)%^(.*)$") + if not name or name == "" then return false end + local k = name .. "-" .. realm + local c = AA.db.chars[k] + if not c then + c = { name = name, realm = realm, class = class, cds = {}, remote = true } + AA.db.chars[k] = c + end + dts, its, bts = tonumber(dts) or 0, tonumber(its) or 0, tonumber(bts) or 0 + local ch = false + if dts > (c.dataTs or 0) then + c.dataTs = dts + c.level = tonumber(lvl) + c.gold = tonumber(gold) + if class ~= "" then c.class = class end + ch = true + end + if its > (c.invTs or 0) then + c.invTs = its + c.inv = ParseInv(inv) + ch = true + end + if bts > (c.bankTs or 0) then + c.bankTs = bts + c.bank = ParseInv(bank) + ch = true + end + return ch +end + +local function ReceiveItem(text) + local _, _, id, name, tex, q = string.find(text, "^I%^(%d+)%^([^%^]*)%^([^%^]*)%^(%d*)$") + if not id then return end + id = tonumber(id) + AA.db.items = AA.db.items or {} + local it = AA.db.items[id] + if not it or not it.n or not it.t or it.t == "" then + AA.db.items[id] = { n = name, t = tex, q = tonumber(q) or 1 } + end +end + +local function EncodeProf(c, pname, p) + return "P^" .. c.name .. "^" .. c.realm .. "^" .. pname .. "^" .. (p.r or 0) .. "^" .. (p.m or 0) + .. "^" .. (p.ts or 0) .. "^" .. (p.rts or 0) .. "^" .. table.concat(p.rec or {}, "~") +end + +local function ReceiveProf(text) + local _, _, name, realm, pname, r, m, ts, rts, rec = + string.find(text, "^P%^([^%^]*)%^([^%^]*)%^([^%^]*)%^(%d*)%^(%d*)%^(%d*)%^(%d*)%^(.*)$") + if not name or name == "" or pname == "" then return false end + if AA.BadProf(pname, tonumber(m)) then return false end + local k = name .. "-" .. realm + local c = AA.db.chars[k] + if not c then + c = { name = name, realm = realm, cds = {}, remote = true } + AA.db.chars[k] = c + end + c.prof = c.prof or {} + local p = c.prof[pname] + if not p then + p = {} + c.prof[pname] = p + end + ts, rts = tonumber(ts) or 0, tonumber(rts) or 0 + local ch = false + if ts > (p.ts or 0) then + p.ts = ts + p.r = tonumber(r) + p.m = tonumber(m) + ch = true + end + if rts > (p.rts or 0) then + p.rts = rts + local list = {} + for rn in string.gfind(rec, "[^~]+") do table.insert(list, rn) end + p.rec = list + p.sig = nil + ch = true + end + return ch +end + +local function EncodeEquip(c) + local parts = {} + for slot = 0, 19 do + if c.equip and c.equip[slot] then table.insert(parts, slot .. "=" .. c.equip[slot]) end + end + return "E^" .. c.name .. "^" .. c.realm .. "^" .. (c.eqTs or 0) .. "^" .. table.concat(parts, "~") +end + +local function ReceiveEquip(text) + local _, _, name, realm, ts, body = string.find(text, "^E%^([^%^]*)%^([^%^]*)%^(%d*)%^(.*)$") + if not name or name == "" then return false end + local k = name .. "-" .. realm + local c = AA.db.chars[k] + if not c then + c = { name = name, realm = realm, cds = {}, remote = true } + AA.db.chars[k] = c + end + ts = tonumber(ts) or 0 + if ts <= (c.eqTs or 0) then return false end + local eq = {} + for slot, ref in string.gfind(body, "(%d+)=(%-?%d+:%-?%d+:%-?%d+:%-?%d+)") do + eq[tonumber(slot)] = ref + end + c.equip = eq + c.eqTs = ts + return true +end + +local function FileRead(name) + if not (ImportFile and ExportFile) then return nil end + local ok, txt = pcall(ImportFile, name) + if ok and txt and txt ~= "" then return txt end + ok, txt = pcall(ImportFile, name .. ".txt") + if ok and txt and txt ~= "" then return txt end + return nil +end + +local function MergeText(txt) + local n = 0 + for line in string.gfind(txt, "[^\r\n]+") do + local t = string.sub(line, 1, 2) + if t == "C^" then + if AA.ReceiveRecord(line) then n = n + 1 end + elseif t == "A^" then + if ReceiveAlt(line) then n = n + 1 end + elseif t == "P^" then + if ReceiveProf(line) then n = n + 1 end + elseif t == "E^" then + if ReceiveEquip(line) then n = n + 1 end + elseif t == "I^" then + ReceiveItem(line) + end + end + return n +end + +local legacyDone = false +AA.FileMerge = function() + if not AA.db then return end + local n = 0 + local txt = FileRead(AA.FILE) + if txt then n = n + MergeText(txt) end + if not legacyDone then + legacyDone = true + local old = FileRead(AA.LEGACY) + if old then n = n + MergeText(old) end + end + if n > 0 then AA.Refresh() end +end + +local function FileWrite() + if not (ImportFile and ExportFile) or not AA.db then return end + local parts, seen = {}, {} + for k, c in pairs(AA.db.chars) do + for _, m in ipairs(Encode(c)) do table.insert(parts, m) end + if c.dataTs or c.invTs or c.bankTs then table.insert(parts, EncodeAlt(c)) end + local lists = { c.inv or {}, c.bank or {} } + for _, list in ipairs(lists) do + for _, e in ipairs(list) do seen[e[1]] = true end + end + if c.prof then + for pname, p in pairs(c.prof) do table.insert(parts, EncodeProf(c, pname, p)) end + end + if c.equip then + table.insert(parts, EncodeEquip(c)) + for slot, ref in pairs(c.equip) do + local _, _, sid = string.find(ref, "^(%d+)") + if sid then seen[tonumber(sid)] = true end + end + end + end + if AA.db.items then + for id in pairs(seen) do + local it = AA.db.items[id] + if it and it.n then + table.insert(parts, "I^" .. id .. "^" .. it.n .. "^" .. (it.t or "") .. "^" .. (it.q or 1)) + end + end + end + pcall(ExportFile, AA.FILE, table.concat(parts, "\n")) + AA.fileDirty = false + AA.lastFileWrite = GetTime() +end + +AA.FileSync = function() + AA.FileMerge() + FileWrite() +end + +local function ExportString() + local parts = {} + for k, c in pairs(AA.db.chars) do + for _, m in ipairs(Encode(c)) do table.insert(parts, m) end + end + return table.concat(parts, "#") +end + +local function ImportString(text) + local n = 0 + for rec in string.gfind(text, "[^#]+") do + if AA.ReceiveRecord(rec) then n = n + 1 end + end + return n +end + +-- export / import box +local io = CreateFrame("Frame", "AdeAltsIO", UIParent) +io:SetWidth(440) +io:SetHeight(90) +io:SetPoint("CENTER", UIParent, "CENTER", 0, 150) +io:SetFrameStrata("DIALOG") +io:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, +}) +io:SetBackdropColor(0, 0, 0, 0.95) +io:EnableMouse(true) +io:Hide() +table.insert(UISpecialFrames, "AdeAltsIO") + +local iotitle = io:CreateFontString(nil, "OVERLAY", "GameFontNormal") +iotitle:SetPoint("TOPLEFT", io, "TOPLEFT", 12, -10) +iotitle:SetText("Copy (Ctrl+C) to export, or paste (Ctrl+V) and press Import") + +local eb = CreateFrame("EditBox", "AdeAltsIOEdit", io) +eb:SetWidth(410) +eb:SetHeight(22) +eb:SetPoint("TOPLEFT", io, "TOPLEFT", 15, -30) +eb:SetFontObject(ChatFontNormal) +eb:SetAutoFocus(false) +eb:SetMaxLetters(0) +eb:SetTextInsets(4, 4, 2, 2) +eb:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 8, edgeSize = 8, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, +}) +eb:SetBackdropColor(0.1, 0.1, 0.1, 1) +eb:SetScript("OnEscapePressed", function() this:ClearFocus() end) + +local bimp = CreateFrame("Button", nil, io, "UIPanelButtonTemplate") +bimp:SetWidth(90); bimp:SetHeight(22) +bimp:SetPoint("BOTTOMLEFT", io, "BOTTOMLEFT", 15, 10) +bimp:SetText("Import") +bimp:SetScript("OnClick", function() + local n = ImportString(eb:GetText() or "") + AA.Print("imported " .. n .. " record(s).") + AA.Refresh() +end) + +local bclose = CreateFrame("Button", nil, io, "UIPanelButtonTemplate") +bclose:SetWidth(90); bclose:SetHeight(22) +bclose:SetPoint("BOTTOMRIGHT", io, "BOTTOMRIGHT", -15, 10) +bclose:SetText("Close") +bclose:SetScript("OnClick", function() io:Hide() end) + +function AA.ShowIO(exportMode) + io:Show() + if exportMode then + eb:SetText(ExportString()) + eb:SetFocus() + eb:HighlightText() + else + eb:SetText("") + eb:SetFocus() + end +end + diff --git a/ui_base.lua b/ui_base.lua new file mode 100644 index 0000000..224c2d6 --- /dev/null +++ b/ui_base.lua @@ -0,0 +1,311 @@ +-- Ade Alts: ui_base +local AA = AdeAlts + +---------------------------------------------------------------------- +-- alts manager window (opened by the minimap button or /ade) +---------------------------------------------------------------------- +local CLASSICON = { + WARRIOR = { 0, .25, 0, .25 }, MAGE = { .25, .5, 0, .25 }, ROGUE = { .5, .75, 0, .25 }, + DRUID = { .75, 1, 0, .25 }, HUNTER = { 0, .25, .25, .5 }, SHAMAN = { .25, .5, .25, .5 }, + PRIEST = { .5, .75, .25, .5 }, WARLOCK = { .75, 1, .25, .5 }, PALADIN = { 0, .25, .5, .75 }, +} +local QCOL = { + [0] = { .62, .62, .62 }, [1] = { 1, 1, 1 }, [2] = { .12, 1, 0 }, + [3] = { 0, .44, .87 }, [4] = { .64, .21, .93 }, [5] = { 1, .5, 0 }, +} +AA.CELL = 33 +AA.COLS, AA.GROWS = 7, 8 -- recalculated by Layout() +AA.ROWH, AA.LROWS = 42, 7 +AA.SROWH, AA.SROWS = 22, 13 +AA.DEF_W, AA.DEF_H = 780, 490 +AA.RROWH, AA.RROWS = 18, 12 +AA.view, AA.profSel = "bags", nil + +local function Commas(n) + local str = tostring(n) + while true do + local k + str, k = string.gsub(str, "^(-?%d+)(%d%d%d)", "%1,%2") + if k == 0 then break end + end + return str +end + +function AA.Money(c) + c = c or 0 + local g = math.floor(c / 10000) + local s = math.floor(math.mod(c, 10000) / 100) + local cc = math.mod(c, 100) + return "|cffffd700" .. Commas(g) .. "g|r |cffc7c7cf" .. s .. "s|r |cffeda55f" .. cc .. "c|r" +end + +function AA.Ago(ts) + if not ts or ts == 0 then return "never" end + local d = time() - ts + if d < 90 then return "just now" end + return AA.FormatTime(d) .. " ago" +end + +function AA.ClassName(token) + if not token or token == "" then return "" end + return string.upper(string.sub(token, 1, 1)) .. string.lower(string.sub(token, 2)) +end + +function AA.ItemName(id) + local it = AA.db and AA.db.items and AA.db.items[id] + return (it and it.n) or ("item:" .. id) +end + +function AA.ItemTex(id) + local it = AA.db and AA.db.items and AA.db.items[id] + if it and it.t and it.t ~= "" then return "Interface\\Icons\\" .. it.t end + return "Interface\\Icons\\INV_Misc_QuestionMark" +end + +function AA.ItemColor(id) + local it = AA.db and AA.db.items and AA.db.items[id] + local c = QCOL[(it and it.q) or 1] or QCOL[1] + return c[1], c[2], c[3] +end + +function AA.SortedAlts() + local list = {} + for k in pairs(AA.db.chars) do table.insert(list, k) end + local mode = AA.db.sort or "level" + table.sort(list, function(a, b) + local ca, cb = AA.db.chars[a], AA.db.chars[b] + if mode == "level" then + local la, lb = ca.level or 0, cb.level or 0 + if la ~= lb then return la > lb end + end + if ca.name ~= cb.name then return ca.name < cb.name end + return a < b + end) + return list +end + +function AA.SetClassTex(tex, class) + local co = class and CLASSICON[class] + if co then + tex:SetTexture("Interface\\Glues\\CharacterCreate\\UI-CharacterCreate-Classes") + tex:SetTexCoord(co[1], co[2], co[3], co[4]) + else + tex:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark") + tex:SetTexCoord(0, 1, 0, 1) + end +end + +-- backgrounds: only these colours change with the opacity setting (never icons / text) ----- +local skinned = {} +function AA.SkinFrame(fr, a, fixed) + local rec = { f = fr, base = a or 0.9 } + if pfUI and pfUI.api and pfUI.api.CreateBackdrop then + local ok = pcall(pfUI.api.CreateBackdrop, fr) + if ok and fr.backdrop and fr.backdrop.GetBackdropColor then + local r, g, b, a0 = fr.backdrop:GetBackdropColor() + rec.pf = true + rec.r, rec.g, rec.b = r or 0, g or 0, b or 0 + rec.base = a0 or 0.85 -- pfUI's own alpha counts as 100 % + if not fixed then table.insert(skinned, rec) end + return + end + end + fr:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + fr:SetBackdropColor(0, 0, 0, rec.base) + if not fixed then table.insert(skinned, rec) end +end + +-- chosen background colour (settings), or nil for the default +function AA.BgColor() + local c = AA.db and AA.db.bg + if c then return c[1], c[2], c[3] end +end + +function AA.ApplyOpacity(o) + local cr, cg, cb = AA.BgColor() + for _, rec in ipairs(skinned) do + if rec.pf then + rec.f.backdrop:SetBackdropColor(cr or rec.r, cg or rec.g, cb or rec.b, rec.base * o) + else + rec.f:SetBackdropColor(cr or 0, cg or 0, cb or 0, rec.base * o) + end + end +end + +function AA.ItemTip(b) + local ref = b.itemStr + if not ref and b.itemId then ref = b.itemId .. ":0:0:0" end + if not ref then return end + GameTooltip:SetOwner(b, "ANCHOR_RIGHT") + local ok = pcall(GameTooltip.SetHyperlink, GameTooltip, "item:" .. ref) + if not ok or GameTooltip:NumLines() == 0 then + GameTooltip:ClearLines() + GameTooltip:AddLine(b.itemId and AA.ItemName(b.itemId) or ref) + end + GameTooltip:Show() +end + +function AA.ScrollWheel(sf, step) + local sb = getglobal(sf:GetName() .. "ScrollBar") + if sb then sb:SetValue(sb:GetValue() - arg1 * step * 2) end +end + +function AA.ResetScroll(sf) + local sb = getglobal(sf:GetName() .. "ScrollBar") + if sb then sb:SetValue(0) end +end + +-- main frame ---------------------------------------------------------- +AA.M = CreateFrame("Frame", "AdeAltsManager", UIParent) +AA.M:SetWidth(AA.DEF_W) +AA.M:SetHeight(AA.DEF_H) +AA.M:SetPoint("CENTER", UIParent, "CENTER", 0, 0) +AA.M:SetFrameStrata("HIGH") +AA.M:SetMovable(true) +AA.M:EnableMouse(true) +AA.M:RegisterForDrag("LeftButton") +AA.M:SetScript("OnDragStart", function() this:StartMoving() end) +AA.M:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) +AA.M:Hide() +AA.SkinFrame(AA.M, 0.95) +table.insert(UISpecialFrames, "AdeAltsManager") + +local mtitle = AA.M:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") +mtitle:SetPoint("TOPLEFT", AA.M, "TOPLEFT", 16, -12) +mtitle:SetText("Ade Alts |cff808080v" .. AA.VERSION .. "|r") + +local mclose = CreateFrame("Button", nil, AA.M, "UIPanelCloseButton") +mclose:SetPoint("TOPRIGHT", AA.M, "TOPRIGHT", -4, -4) + +AA.cdbtn = CreateFrame("Button", nil, AA.M, "UIPanelButtonTemplate") +AA.cdbtn:SetWidth(90) +AA.cdbtn:SetHeight(20) +AA.cdbtn:SetPoint("TOPRIGHT", AA.M, "TOPRIGHT", -40, -10) +AA.cdbtn:SetText("Cooldowns") +AA.cdbtn:SetScript("OnClick", function() AA.ToggleWindow() end) + +-- search -------------------------------------------------------------- +local slabel = AA.M:CreateFontString(nil, "OVERLAY", "GameFontNormal") +slabel:SetPoint("TOPLEFT", AA.M, "TOPLEFT", 16, -40) +slabel:SetText("Search items:") + +AA.sbox = CreateFrame("EditBox", "AdeAltsSearch", AA.M) +AA.sbox:SetWidth(280) +AA.sbox:SetHeight(22) +AA.sbox:SetPoint("LEFT", slabel, "RIGHT", 10, 0) +AA.sbox:SetFontObject(ChatFontNormal) +AA.sbox:SetAutoFocus(false) +AA.sbox:SetMaxLetters(40) +AA.sbox:SetTextInsets(6, 6, 2, 2) +AA.sbox:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 8, edgeSize = 8, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, +}) +AA.sbox:SetBackdropColor(0.1, 0.1, 0.1, 1) + +local shint = AA.M:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall") +shint:SetPoint("LEFT", AA.sbox, "RIGHT", 10, 0) +shint:SetText("name of an item in bags or bank of any alt") + +-- alt list ------------------------------------------------------------ +AA.L = CreateFrame("Frame", nil, AA.M) +AA.L:SetWidth(190) +AA.L:SetHeight(350) +AA.L:SetPoint("TOPLEFT", AA.M, "TOPLEFT", 12, -66) +AA.SkinFrame(AA.L, 0.6) + +AA.altRows = {} +local lsf = CreateFrame("ScrollFrame", "AdeAltsListScroll", AA.L, "FauxScrollFrameTemplate") +lsf:SetPoint("TOPLEFT", AA.L, "TOPLEFT", 0, -32) +lsf:SetPoint("BOTTOMRIGHT", AA.L, "BOTTOMRIGHT", -26, 4) +lsf:SetScript("OnVerticalScroll", function() FauxScrollFrame_OnVerticalScroll(AA.ROWH, AA.UpdateAltList) end) +AA.L:EnableMouseWheel(true) +AA.L:SetScript("OnMouseWheel", function() AA.ScrollWheel(lsf, AA.ROWH) end) + +local sortBtns = {} +local sortLabel = AA.L:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") +sortLabel:SetPoint("TOPLEFT", AA.L, "TOPLEFT", 10, -12) +sortLabel:SetText("Sort by:") +local function MakeSort(mode, label, x) + local b = CreateFrame("Button", "AdeAltsSort" .. mode, AA.L, "UIPanelButtonTemplate") + b:SetWidth(52) + b:SetHeight(18) + b:SetPoint("TOPLEFT", AA.L, "TOPLEFT", x, -8) + b:SetText(label) + b.mode = mode + b:SetScript("OnClick", function() + AA.db.sort = this.mode + AA.ResetScroll(lsf) + AA.RefreshManager() + end) + sortBtns[mode] = b +end +MakeSort("level", "Level", 64) +MakeSort("name", "Name", 120) + +function AA.MakeAltRow() + local r = CreateFrame("Button", nil, AA.L) + r:SetWidth(158) + r:SetHeight(AA.ROWH - 2) + r.sel = r:CreateTexture(nil, "BACKGROUND") + r.sel:SetAllPoints(r) + r.sel:SetTexture(0.2, 0.5, 0.9, 0.35) + r.sel:Hide() + r:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") + r.icon = r:CreateTexture(nil, "ARTWORK") + r.icon:SetWidth(32) + r.icon:SetHeight(32) + r.icon:SetPoint("LEFT", r, "LEFT", 4, 0) + r.name = r:CreateFontString(nil, "OVERLAY", "GameFontNormal") + r.name:SetPoint("TOPLEFT", r.icon, "TOPRIGHT", 6, -2) + r.name:SetWidth(112) + r.name:SetJustifyH("LEFT") + r.info = r:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + r.info:SetPoint("TOPLEFT", r.name, "BOTTOMLEFT", 0, -3) + r.info:SetWidth(112) + r.info:SetJustifyH("LEFT") + r:EnableMouseWheel(true) + r:SetScript("OnMouseWheel", function() AA.ScrollWheel(lsf, AA.ROWH) end) + r:SetScript("OnClick", function() + AA.selKey = this.key + if AA.sbox:GetText() ~= "" then AA.sbox:SetText("") end + AA.RefreshManager() + end) + r:Hide() + table.insert(AA.altRows, r) +end + +AA.UpdateAltList = function() + if not AA.db then return end + local keys = AA.SortedAlts() + for mode, b in pairs(sortBtns) do + if (AA.db.sort or "level") == mode then b:Disable() else b:Enable() end + end + FauxScrollFrame_Update(lsf, table.getn(keys), AA.LROWS, AA.ROWH) + local off = FauxScrollFrame_GetOffset(lsf) + for i = 1, AA.LROWS do + local r = AA.altRows[i] + local k = keys[off + i] + if k then + local c = AA.db.chars[k] + r.key = k + AA.SetClassTex(r.icon, c.class) + r.name:SetText("|cff" .. AA.ClassHex(c.class) .. c.name .. "|r") + local lv = "?" + if c.level and c.level > 0 then lv = c.level end + r.info:SetText("Lv " .. lv .. " " .. AA.ClassName(c.class)) + if k == AA.selKey then r.sel:Show() else r.sel:Hide() end + r:Show() + else + r:Hide() + end + end +end + diff --git a/ui_config.lua b/ui_config.lua new file mode 100644 index 0000000..cf246d1 --- /dev/null +++ b/ui_config.lua @@ -0,0 +1,163 @@ +-- Ade Alts: ui_config +local AA = AdeAlts + + +-- settings window ------------------------------------------------------ +AA.ApplyConfig = function() + if not AA.db then return end + AA.M:SetScale(AA.db.scale or 1) + AA.M:SetAlpha(1) + AA.ApplyOpacity(AA.db.alpha or 1) + local cr, cg, cb = AA.BgColor() + AA.f:SetBackdropColor(cr or 0, cg or 0, cb or 0, AA.db.cdalpha or 1) + if AA.UpdateSwatch then AA.UpdateSwatch() end + AA.Layout() +end + +local cfg = CreateFrame("Frame", "AdeAltsConfig", UIParent) +cfg:SetWidth(280) +cfg:SetHeight(404) +cfg:SetPoint("CENTER", UIParent, "CENTER", 0, 0) +cfg:SetFrameStrata("DIALOG") +cfg:SetMovable(true) +cfg:EnableMouse(true) +cfg:RegisterForDrag("LeftButton") +cfg:SetScript("OnDragStart", function() this:StartMoving() end) +cfg:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) +cfg:Hide() +AA.SkinFrame(cfg, 0.97, true) +table.insert(UISpecialFrames, "AdeAltsConfig") + +local ctitle = cfg:CreateFontString(nil, "OVERLAY", "GameFontNormal") +ctitle:SetPoint("TOPLEFT", cfg, "TOPLEFT", 16, -14) +ctitle:SetText("Ade Alts settings") +local cclose = CreateFrame("Button", nil, cfg, "UIPanelCloseButton") +cclose:SetPoint("TOPRIGHT", cfg, "TOPRIGHT", -4, -4) + +local sliders = {} +local function SliderLabel(sl, v) + if sl.kind == "pct" then + return sl.label .. ": " .. math.floor(v * 100 + 0.5) .. "%" + end + return sl.label .. ": " .. math.floor(v + 0.5) .. " px" +end + +local function MakeSlider(name, y, lo, hi, step, field, label, kind, default) + local sl = CreateFrame("Slider", name, cfg, "OptionsSliderTemplate") + sl:SetWidth(240) + sl:SetHeight(16) + sl:SetPoint("TOP", cfg, "TOP", 0, y) + sl:SetMinMaxValues(lo, hi) + sl:SetValueStep(step) + if kind == "pct" then + getglobal(name .. "Low"):SetText(math.floor(lo * 100 + 0.5) .. "%") + getglobal(name .. "High"):SetText(math.floor(hi * 100 + 0.5) .. "%") + else + getglobal(name .. "Low"):SetText(lo) + getglobal(name .. "High"):SetText(hi) + end + sl.field, sl.label, sl.kind, sl.step, sl.default = field, label, kind, step, default + sl:SetScript("OnValueChanged", function() + local v = this:GetValue() + if this.kind == "pct" then + v = math.floor(v * 100 + 0.5) / 100 + else + v = math.floor(v / this.step + 0.5) * this.step + end + getglobal(this:GetName() .. "Text"):SetText(SliderLabel(this, v)) + if cfg.loading or not AA.db then return end + AA.db[this.field] = v + AA.ApplyConfig() + end) + table.insert(sliders, sl) + return sl +end + +MakeSlider("AdeAltsScaleSlider", -56, 0.7, 1.5, 0.05, "scale", "Window scale", "pct", 1) +MakeSlider("AdeAltsAlphaSlider", -106, 0.1, 1.0, 0.05, "alpha", "Background opacity", "pct", 1) +MakeSlider("AdeAltsWidthSlider", -156, 640, 1500, 10, "width", "Window width", "px", AA.DEF_W) +MakeSlider("AdeAltsHeightSlider", -206, 420, 1000, 10, "height", "Window height", "px", AA.DEF_H) +MakeSlider("AdeAltsCdAlphaSlider", -256, 0.1, 1.0, 0.05, "cdalpha", "Cooldowns window opacity", "pct", 1) + +-- background colour (standard colour picker) ------------------------------- +local swlabel = cfg:CreateFontString(nil, "OVERLAY", "GameFontNormal") +swlabel:SetPoint("TOPLEFT", cfg, "TOPLEFT", 20, -304) +swlabel:SetText("Background color") +local swatch = CreateFrame("Button", nil, cfg) +swatch:SetWidth(36) +swatch:SetHeight(18) +swatch:SetPoint("TOPRIGHT", cfg, "TOPRIGHT", -20, -302) +swatch:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 8, edgeSize = 10, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, +}) +swatch:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square") + +AA.UpdateSwatch = function() + local r, g, b = AA.BgColor() + swatch:SetBackdropColor(r or 0, g or 0, b or 0, 1) +end + +swatch:SetScript("OnClick", function() + if not AA.db then return end + local pr, pg, pb = AA.BgColor() + pr, pg, pb = pr or 0, pg or 0, pb or 0 + local had = AA.db.bg + ColorPickerFrame:Hide() + ColorPickerFrame.hasOpacity = nil + ColorPickerFrame.opacityFunc = nil + ColorPickerFrame.func = function() + local r, g, b = ColorPickerFrame:GetColorRGB() + AA.db.bg = { r, g, b } + AA.ApplyConfig() + end + ColorPickerFrame.cancelFunc = function() + AA.db.bg = had + AA.ApplyConfig() + end + ColorPickerFrame.previousValues = { r = pr, g = pg, b = pb } + ColorPickerFrame:SetColorRGB(pr, pg, pb) + ColorPickerFrame:SetFrameStrata("FULLSCREEN_DIALOG") + ColorPickerFrame:Show() +end) + +local function LoadConfigUI() + AA.UpdateSwatch() + cfg.loading = true + for _, sl in ipairs(sliders) do + local v = (AA.db and AA.db[sl.field]) or sl.default + sl:SetValue(v) + getglobal(sl:GetName() .. "Text"):SetText(SliderLabel(sl, v)) + end + cfg.loading = false +end +cfg:SetScript("OnShow", LoadConfigUI) + +local creset = CreateFrame("Button", nil, cfg, "UIPanelButtonTemplate") +creset:SetWidth(90) +creset:SetHeight(22) +creset:SetPoint("BOTTOM", cfg, "BOTTOM", 0, 16) +creset:SetText("Reset") +creset:SetScript("OnClick", function() + for _, sl in ipairs(sliders) do AA.db[sl.field] = sl.default end + AA.db.bg = nil + AA.ApplyConfig() + LoadConfigUI() +end) + +local setbtn = CreateFrame("Button", nil, AA.M, "UIPanelButtonTemplate") +setbtn:SetWidth(80) +setbtn:SetHeight(20) +setbtn:SetPoint("RIGHT", AA.cdbtn, "LEFT", -6, 0) +setbtn:SetText("Settings") +setbtn:SetScript("OnClick", function() + if cfg:IsVisible() then cfg:Hide() else cfg:Show() end +end) + +AA.ToggleManager = function() + if AA.M:IsVisible() then AA.M:Hide() else AA.M:Show() end +end + + diff --git a/ui_equip.lua b/ui_equip.lua new file mode 100644 index 0000000..63f643e --- /dev/null +++ b/ui_equip.lua @@ -0,0 +1,82 @@ +-- Ade Alts: ui_equip (equipped gear: 3 columns of 6 / 6 / 5 slots) +local AA = AdeAlts + +-- inventory slot id, label, column, row +local EQSLOTS = { + { 1, "Head", 1, 0 }, { 2, "Neck", 1, 1 }, { 3, "Shoulders", 1, 2 }, + { 15, "Back", 1, 3 }, { 5, "Chest", 1, 4 }, { 9, "Wrists", 1, 5 }, + { 10, "Hands", 2, 0 }, { 6, "Waist", 2, 1 }, { 7, "Legs", 2, 2 }, + { 8, "Feet", 2, 3 }, { 11, "Ring 1", 2, 4 }, { 12, "Ring 2", 2, 5 }, + { 13, "Trinket 1", 3, 0 }, { 14, "Trinket 2", 3, 1 }, { 16, "Main hand", 3, 2 }, + { 17, "Off hand", 3, 3 }, { 18, "Ranged", 3, 4 }, +} + +AA.EQ = CreateFrame("Frame", nil, AA.P) +AA.EQ:SetPoint("TOPLEFT", AA.P, "TOPLEFT", 6, -84) +AA.EQ:SetWidth(546) +AA.EQ:SetHeight(300) +AA.SkinFrame(AA.EQ, 0.5) +AA.EQ:Hide() +AA.EQ.title = AA.EQ:CreateFontString(nil, "OVERLAY", "GameFontNormal") +AA.EQ.title:SetPoint("TOPLEFT", AA.EQ, "TOPLEFT", 10, -7) +AA.EQ.title:SetText("Equipped gear") +AA.EQ.note = AA.EQ:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall") +AA.EQ.note:SetPoint("TOPRIGHT", AA.EQ, "TOPRIGHT", -10, -9) +AA.EQ.empty = AA.EQ:CreateFontString(nil, "OVERLAY", "GameFontDisable") +AA.EQ.empty:SetPoint("CENTER", AA.EQ, "CENTER", 0, 0) +AA.EQ.empty:Hide() + +AA.EQ.cells = {} +for i, def in ipairs(EQSLOTS) do + local b = CreateFrame("Button", nil, AA.EQ) + b.slotId = def[1] + b.slotName = def[2] + b.col = def[3] + b.row = def[4] + -- icon with a quality coloured border + b.frame = CreateFrame("Frame", nil, b) + b.frame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 8, edgeSize = 10, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + b.frame:SetBackdropColor(0.04, 0.04, 0.04, 0.85) + b.frame:SetPoint("LEFT", b, "LEFT", 0, 0) + b.icon = b.frame:CreateTexture(nil, "ARTWORK") + b.icon:SetPoint("TOPLEFT", b.frame, "TOPLEFT", 3, -3) + b.icon:SetPoint("BOTTOMRIGHT", b.frame, "BOTTOMRIGHT", -3, 3) + b.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + b.name = b:CreateFontString(nil, "OVERLAY", "GameFontNormal") + b.name:SetPoint("TOPLEFT", b.frame, "TOPRIGHT", 8, -3) + b.name:SetJustifyH("LEFT") + b.slot = b:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall") + b.slot:SetPoint("TOPLEFT", b.name, "BOTTOMLEFT", 0, -2) + b.slot:SetJustifyH("LEFT") + b:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square") + b:SetScript("OnEnter", function() AA.ItemTip(this) end) + b:SetScript("OnLeave", function() GameTooltip:Hide() end) + AA.EQ.cells[i] = b +end + +-- positions the slots for a box of the given size +function AA.LayoutEquip(W, H) + local EQ = AA.EQ + EQ:SetWidth(W) + EQ:SetHeight(H) + local colW = math.floor((W - 20 - 2 * 8) / 3) + local rh = math.floor((H - 40) / 6) + if rh > 52 then rh = 52 end + if rh < 30 then rh = 30 end + local isz = rh - 8 + for _, b in ipairs(EQ.cells) do + b:ClearAllPoints() + b:SetPoint("TOPLEFT", EQ, "TOPLEFT", 10 + (b.col - 1) * (colW + 8), -34 - b.row * rh) + b:SetWidth(colW) + b:SetHeight(rh - 4) + b.frame:SetWidth(isz) + b.frame:SetHeight(isz) + b.name:SetWidth(colW - isz - 12) + b.slot:SetWidth(colW - isz - 12) + end +end diff --git a/ui_layout.lua b/ui_layout.lua new file mode 100644 index 0000000..5a93ca4 --- /dev/null +++ b/ui_layout.lua @@ -0,0 +1,97 @@ +-- Ade Alts: ui_layout +local AA = AdeAlts + + +-- layout: everything follows the configured window width / height -------------------------- +function AA.Layout() + local W = (AA.db and AA.db.width) or AA.DEF_W + local H = (AA.db and AA.db.height) or AA.DEF_H + if W < 640 then W = 640 end + if H < 420 then H = 420 end + AA.M:SetWidth(W) + AA.M:SetHeight(H) + local ph = H - 100 + local pw = W - 222 + AA.L:SetHeight(ph) + AA.P:SetWidth(pw) + AA.P:SetHeight(ph) + + -- alt list + AA.LROWS = math.max(1, math.floor((ph - 40) / AA.ROWH)) + while table.getn(AA.altRows) < AA.LROWS do AA.MakeAltRow() end + for i, r in ipairs(AA.altRows) do + r:Hide() + r:ClearAllPoints() + r:SetPoint("TOPLEFT", AA.L, "TOPLEFT", 8, -36 - (i - 1) * AA.ROWH) + end + + -- inventory / bank boxes and their item grids + local bw = math.floor((pw - 18) / 2) + local bh = ph - 90 + AA.COLS = math.max(1, math.floor((bw - 37) / AA.CELL)) + AA.GROWS = math.max(1, math.floor((bh - 30) / AA.CELL)) + AA.BI:SetWidth(bw) + AA.BI:SetHeight(bh) + AA.BB:SetWidth(bw) + AA.BB:SetHeight(bh) + AA.BB:ClearAllPoints() + AA.BB:SetPoint("TOPLEFT", AA.P, "TOPLEFT", 12 + bw, -84) + local boxes = { AA.BI, AA.BB } + for _, B in ipairs(boxes) do + while table.getn(B.cells) < AA.COLS * AA.GROWS do AA.AddCell(B) end + for i, b in ipairs(B.cells) do + b:Hide() + b:ClearAllPoints() + local r = math.floor((i - 1) / AA.COLS) + local c = math.mod(i - 1, AA.COLS) + b:SetPoint("TOPLEFT", B, "TOPLEFT", 9 + c * AA.CELL, -28 - r * AA.CELL) + end + end + + -- equipment view + AA.LayoutEquip(pw - 12, bh) + + -- professions view + local psw = 230 + local prw = pw - 12 - psw - 6 + AA.PS:SetWidth(psw) + AA.PS:SetHeight(bh) + AA.PR:SetWidth(prw) + AA.PR:SetHeight(bh) + AA.PR:ClearAllPoints() + AA.PR:SetPoint("TOPLEFT", AA.P, "TOPLEFT", 12 + psw, -84) + AA.RROWS = math.max(1, math.floor((bh - 34) / AA.RROWH)) + while table.getn(AA.recRows) < AA.RROWS do AA.MakeRecRow() end + for i, r in ipairs(AA.recRows) do + r:Hide() + r:SetWidth(prw - 36) + r.text:SetWidth(prw - 40) + r:ClearAllPoints() + r:SetPoint("TOPLEFT", AA.PR, "TOPLEFT", 10, -28 - (i - 1) * AA.RROWH) + end + + -- search results + local sw, sh = pw - 12, ph - 12 + AA.S:SetWidth(sw) + AA.S:SetHeight(sh) + AA.SROWS = math.max(1, math.floor((sh - 36) / AA.SROWH)) + while table.getn(AA.resRows) < AA.SROWS do AA.MakeResRow() end + local rowW = sw - 40 + local nameW = rowW - 322 + if nameW < 120 then nameW = 120 end + for i, r in ipairs(AA.resRows) do + r:Hide() + r:SetWidth(rowW) + r:ClearAllPoints() + r:SetPoint("TOPLEFT", AA.S, "TOPLEFT", 8, -30 - (i - 1) * AA.SROWH) + r.name:SetWidth(nameW) + r.cnt:ClearAllPoints() + r.cnt:SetPoint("LEFT", r, "LEFT", 32 + nameW, 0) + r.who:ClearAllPoints() + r.who:SetPoint("LEFT", r, "LEFT", 96 + nameW, 0) + r.where:ClearAllPoints() + r.where:SetPoint("LEFT", r, "LEFT", 230 + nameW, 0) + end + + AA.RefreshManager() +end