-------------------------------------------------------------------------------
-- Sey_Weekly.lua  |  Author: Sey (Ultimate Flawless & Ultra-Light Version)## Author: |cffF58CBASey|r  ## Version: 1.0.1 ## SavedVariables: SeyWeeklyDB
-------------------------------------------------------------------------------

local ADDON_NAME = "Sey_Weekly"
local ICON_PATH  = "Interface\\AddOns\\OrzUI\\Media\\2UI.blp"
local ICON_CHECK = "|TInterface\\Buttons\\UI-CheckBox-Check:16:16:0:-2|t"
local HEADER_GOLD_TEX = "|TInterface\\MoneyFrame\\UI-GoldIcon:12:12:2:0|t"
local CHAR_GOLD_TEX   = "|TInterface\\MoneyFrame\\UI-GoldIcon:11:11:2:-1|t"
local VOIDCORE_ID = 3418 

local COPPER_PER_SILVER = 100; local SILVER_PER_GOLD = 100; local COPPER_PER_GOLD = COPPER_PER_SILVER * SILVER_PER_GOLD

local C = {
    bg = {0.04, 0.04, 0.05, 1.00}, header = {0.07, 0.07, 0.09, 1.00}, sepBg = {0, 0, 0, 0}, sepBtn = {0.10, 0.10, 0.12, 1.00},
    sepHighlight = {1.00, 1.00, 1.00, 0.06}, sepShadow = {0.00, 0.00, 0.00, 0.50}, rowOdd = {0.00, 0.00, 0.00, 0.00},
    rowEven = {0.06, 0.06, 0.07, 1.00}, line = {0.22, 0.20, 0.16, 0.60}, border = {0.20, 0.20, 0.22, 1.00},
    titleBg = {0.06, 0.06, 0.08, 1.00}, txtNormal = {0.82, 0.82, 0.82}, txtHeader = {0.45, 0.65, 0.85}, muteBlue = {0.35, 0.50, 0.65, 0.80}
}
local COLOR_KILL = {0.15, 1.00, 0.30}; local COLOR_PART = {1.00, 1.00, 1.00}; local COLOR_ZERO = {0.42, 0.42, 0.42}; local COLOR_WHITE = {1.00, 1.00, 1.00}; local COLOR_CAP_GRAY = {0.533, 0.533, 0.533}

local function GetLastWeeklyReset()
    local now = time()
    if C_DateAndTime and C_DateAndTime.GetSecondsUntilWeeklyReset then
        local sec = C_DateAndTime.GetSecondsUntilWeeklyReset()
        if sec and sec > 0 then return now + sec - 604800 end
    end
    local t = date("*t", now)
    local daysSinceReset = (t.wday - 5 + 7) % 7
    if daysSinceReset == 0 and t.hour < 8 then daysSinceReset = 7 end
    local todayMidnight = now - (t.hour * 3600 + t.min * 60 + t.sec)
    return todayMidnight - daysSinceReset * 86400 + 8 * 3600
end

local ROW_H = 24; local DUNG_ROW_H = 32; local VOID_H = 30; local CURR_ROW_H = 26; local NAME_W = 160; local TOON_W = 110; local PAD_L = 10; local PAD_T = 30
local RAID_DIFF_MAP = { [17]="lfr", [14]="normal", [15]="heroic", [16]="mythic", [233]="mythic" }
local RAID_LIST = { { key="voidspire", label="虚空尖塔", total=6, bosses={"Imperator Averzian","Vorasius","Fallen-King Salhadaar","Vaelgor","War-Chaplain Senn","Alleria Windrunner"} }, { key="dreamrift", label="梦境裂隙", total=1, bosses={"Chimaerus"} }, { key="quelndanas", label="进军路", total=2, bosses={"Belo'ren","Midnight"} }, { key="fungal", label="真菌深渊", total=1 } }
local INSTANCE_TO_RAID_NORM = { ["虚空尖塔"]="voidspire", ["THE VOIDSPIRE"]="voidspire", ["梦境裂隙"]="dreamrift", ["THE DREAMRIFT"]="dreamrift", ["奎尔达纳斯进军"]="quelndanas", ["奎尔达纳斯进军路"]="quelndanas", ["MARCH ON QUEL'DANAS"]="quelndanas", ["真菌深渊"]="fungal", ["真菌 深渊"]="fungal", ["SPOREFALL"]="fungal" }
local WB_LIST = { { key="luashal", label="卢阿莎尔", questID=92560 }, { key="cragpine", label="岩松", questID=92123 }, { key="thormbelan", label="索姆贝兰", questID=92034 }, { key="predaxas", label="普雷达萨斯", questID=92636 } }
local CURRENCY_LIST = { { key="cofferkey", label="修复的宝箱钥匙", type="currency", id=3028 }, { key="keyshard", label="宝箱钥匙碎片", type="currency", id=3310 }, { key="trovebounty", label="寻宝者的恩惠", type="item", id=252415 } }
local CREST_LIST = { 
    { key="sparkdust", label="闪耀的火花粉尘", id=3212, color="d1d1d1", rgb={1.00, 1.00, 1.00} }, 
    { key="manaflux", label="套装转换", id=3378, color="d1d1d1", rgb={1.00, 1.00, 1.00} }, 
    { key="adventurer", label="冒险者", id=3383, color="ffffff", rgb={1.00, 1.00, 1.00} }, 
    { key="veteran", label="老兵", id=3341, color="4dff4d", rgb={0.30, 1.00, 0.30} }, 
    { key="champion", label="勇士", id=3343, color="66b3ff", rgb={0.40, 0.70, 1.00} }, 
    { key="hero", label="英雄", id=3345, color="c759ff", rgb={0.78, 0.35, 1.00} }, 
    { key="myth", label="传说", id=3347, color="ff8000", rgb={1.00, 0.50, 0.00} },
    { key="bloody", label="血腥纹章", id=2123, color="ff4444", rgb={1.00, 0.27, 0.27} },
    { key="honor", label="荣誉点数", id=1792, color="ffd100", rgb={1.00, 0.82, 0.00} },
    { key="conquest", label="征服点数", id=1602, color="c759ff", rgb={0.78, 0.35, 1.00} },
    { key="dundun", label="钝顿碎片", id=3376, color="00ccff", rgb={0.00, 0.80, 1.00} }
}
local DUNGEON_SHORT_NAMES = { ["虚裂塔泽纳斯"] = "泽纳斯", ["魔法学者的花园"] = "花园", ["玛伊萨拉洞穴"] = "玛伊萨拉", ["风行者尖塔"] = "尖塔", ["萨隆的深坑"] = "萨隆", ["三头政权王座"] = "三头政权", ["阿尔加太学院"] = "学院", ["天空之塔"] = "天空之塔" }
local function normName(s) return (s or ""):gsub("%p",""):gsub("%s+"," "):upper():match("^%s*(.-)%s*$") end

local DB, mainFrame, optFrame, dragGhost, statusTxt
local sectionCollapsed = {}; local sortedList, rowPool, cellPool, optRowPool = {}, {}, {}, {}
local isOverMinimapBtn = false; local activeEditBox = nil; local isLayoutMode = false; local isDraggingCat = nil
local eventFrame = CreateFrame("Frame")
local PurgeExpiredData, RequestUpdate

local function OpenWeeklyRewards() if not C_AddOns.IsAddOnLoaded("Blizzard_WeeklyRewards") then C_AddOns.LoadAddOn("Blizzard_WeeklyRewards") end if WeeklyRewardsFrame then if WeeklyRewardsFrame:IsShown() then WeeklyRewardsFrame:Hide() else WeeklyRewardsFrame:Show() end end end
local function GetClassColor(classFile) if classFile and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classFile] then local c = RAID_CLASS_COLORS[classFile]; return c.r, c.g, c.b end return 0.8, 0.8, 0.8 end
local function GetClassColorHex(classFile) local r, g, b = GetClassColor(classFile); return string.format("%02x%02x%02x", r*255, g*255, b*255) end
local function GetCharKey() return UnitName("player").."-"..GetRealmName() end
local function GetCurrentIlvl() return math.floor(select(2, GetAverageItemLevel()) * 100 + 0.5) / 100 end
local function FormatGold(money) if not money then return "0" end return BreakUpLargeNumbers(math.floor(money / COPPER_PER_GOLD)) end
local function GetTotalGold() local total = DB.warbandMoney or 0; for k, v in pairs(DB.chars) do if type(v) == "table" and v.money then total = total + v.money end end return total end

local hasValidMoneyScan = false
local function SaveCharData() local key, name, realm, ilvl, _, classFile = GetCharKey(), UnitName("player"), GetRealmName(), GetCurrentIlvl(), UnitClass("player"); local currentCopper = GetMoney(); if currentCopper > 0 then hasValidMoneyScan = true end
    if not DB.chars[key] then DB.chars[key] = { name=name, realm=realm, ilvl=ilvl, hidden=false, classFile=classFile, money=currentCopper, lastUpdate=time() }
    else DB.chars[key].name = name; DB.chars[key].realm = realm; DB.chars[key].classFile = classFile; local oldMoney = DB.chars[key].money or 0; if currentCopper == 0 and oldMoney > 0 then currentCopper = oldMoney end
        DB.chars[key].money = currentCopper; if ilvl > 0 then DB.chars[key].ilvl = ilvl end DB.chars[key].lastUpdate = time() end end

-- ============================================================
--  1小时10次副本进入限制追踪逻辑 (移植自 SavedInstances)
-- ============================================================
local function GetInstanceHistory()
    local now = time()
    local count = 0
    local validList = {}
    if not DB or not DB.instHistory then return 0, {} end
    
    for k, v in pairs(DB.instHistory) do
        if now - v.time >= 3600 then
            DB.instHistory[k] = nil
        else
            count = count + 1
            table.insert(validList, v)
        end
    end
    table.sort(validList, function(a, b) return a.time > b.time end)
    return count, validList
end

local function UpdateInstanceHistory()
    if not DB or not DB.instHistory then return end
    local name, type, diffID, diffName, maxPlayers = GetInstanceInfo()
    if type == "none" or type == "arena" or type == "pvp" then return end
    if diffID == 208 then return end -- 排除地下堡
    if (IsInLFGDungeon() or IsInScenarioGroup()) and diffID ~= 19 and diffID ~= 17 then return end -- 排除随机/场景战役
    if C_Garrison and C_Garrison.IsOnGarrisonMap() then return end
    
    local locked = false
    for i = 1, GetNumSavedInstances() do
        local sName, _, _, _, sLocked = GetSavedInstanceInfo(i)
        if sName == name and sLocked then
            locked = true; break
        end
    end
    if diffID == 1 and maxPlayers == 5 then locked = false end -- 普通5人副本不计入锁定的例外处理
    
    local key = UnitName("player") .. "-" .. GetRealmName() .. ":" .. (name or "Unknown") .. ":" .. (diffID or 0)
    if not locked then
        key = key .. ":" .. (DB.instGen or 1)
    end
    
    if not DB.instHistory[key] then
        DB.instHistory[key] = { name = name, diff = diffName, time = time() }
    end
    
    if mainFrame and mainFrame.btnRecent then
        mainFrame.btnRecent:Update()
    end
end

local function CreateToggleSwitch(parent, onToggleCallback)
    local f = CreateFrame("Button", nil, parent); f:SetSize(36, 18); local border = f:CreateTexture(nil, "BACKGROUND"); border:SetAllPoints(); border:SetColorTexture(0.05, 0.05, 0.05, 1)
    local bg = f:CreateTexture(nil, "BORDER"); bg:SetPoint("TOPLEFT", 1, -1); bg:SetPoint("BOTTOMRIGHT", -1, 1); bg:SetColorTexture(0.3, 0.3, 0.3, 1); f.bg = bg
    local thumb = f:CreateTexture(nil, "ARTWORK"); thumb:SetSize(14, 14); thumb:SetColorTexture(0.9, 0.9, 0.9, 1); f.thumb = thumb
    f.SetState = function(self, isOn) self.isOn = isOn; self.thumb:ClearAllPoints(); if isOn then self.bg:SetColorTexture(0.3, 0.5, 0.7, 1); self.thumb:SetColorTexture(1.0, 1.0, 1.0, 1); self.thumb:SetPoint("RIGHT", self, "RIGHT", -3, 0) else self.bg:SetColorTexture(0.25, 0.25, 0.25, 1); self.thumb:SetColorTexture(0.55, 0.55, 0.55, 1); self.thumb:SetPoint("LEFT", self, "LEFT", 3, 0) end end
    f:SetScript("OnEnter", function(self) if parent and parent.LockHighlight then parent:LockHighlight() end if parent and parent:GetScript("OnEnter") then parent:GetScript("OnEnter")(parent) end end); f:SetScript("OnLeave", function(self) if parent and parent.UnlockHighlight then parent:UnlockHighlight() end if parent and parent:GetScript("OnLeave") then parent:GetScript("OnLeave")(parent) end end)
    f:SetScript("OnClick", function(self) if activeEditBox then activeEditBox:ClearFocus() end local newState = not self.isOn; self:SetState(newState); if self.OnToggle then self:OnToggle(newState) end end); return f
end

local function GetOrCreateRow(idx)
    if rowPool[idx] then return rowPool[idx] end; local f = CreateFrame("Button", nil, mainFrame.body); local bg = f:CreateTexture(nil, "BACKGROUND"); bg:SetAllPoints(); f.bg = bg
    local hl = f:CreateTexture(nil, "HIGHLIGHT"); hl:SetAllPoints(); hl:SetColorTexture(1, 1, 1, 0.08); f.hl = hl; local linkedHl = f:CreateTexture(nil, "OVERLAY"); linkedHl:SetAllPoints(); linkedHl:SetColorTexture(unpack(C.txtHeader)); linkedHl:SetAlpha(0); linkedHl:Hide(); f.linkedHl = linkedHl
    local animGroup = linkedHl:CreateAnimationGroup(); animGroup:SetLooping("BOUNCE"); local alphaAnim = animGroup:CreateAnimation("Alpha"); alphaAnim:SetFromAlpha(0.0); alphaAnim:SetToAlpha(0.25); alphaAnim:SetDuration(0.7); alphaAnim:SetSmoothing("IN_OUT"); linkedHl.anim = animGroup
    local lbl = f:CreateFontString(nil, "OVERLAY"); local fontFile, _, fontFlags = GameFontNormal:GetFont(); lbl:SetFont(fontFile, 13, fontFlags); lbl:SetJustifyH("LEFT"); f.lbl = lbl
    local accent = f:CreateTexture(nil, "ARTWORK"); accent:SetColorTexture(unpack(C.txtHeader)); f.accent = accent; local highlight = f:CreateTexture(nil, "ARTWORK"); highlight:SetColorTexture(unpack(C.sepHighlight)); highlight:SetHeight(1); highlight:SetPoint("TOPLEFT"); highlight:SetPoint("TOPRIGHT"); f.highlight = highlight
    local shadow = f:CreateTexture(nil, "ARTWORK"); shadow:SetColorTexture(unpack(C.sepShadow)); shadow:SetHeight(1); shadow:SetPoint("BOTTOMLEFT"); shadow:SetPoint("BOTTOMRIGHT"); f.shadow = shadow; local iconTx = f:CreateTexture(nil, "OVERLAY"); iconTx:SetSize(18, 18); f.iconTx = iconTx
    rowPool[idx] = f; return f
end

local function GetOrCreateCell(rowIdx, colIdx, isHeader)
    if not cellPool[rowIdx] then cellPool[rowIdx] = {} end; if cellPool[rowIdx][colIdx] then return cellPool[rowIdx][colIdx] end; local f = CreateFrame("Frame", nil, mainFrame.body); f:SetFrameLevel(mainFrame.body:GetFrameLevel() + 5); f:EnableMouse(true)
    local fontFile, _, fontFlags = GameFontNormal:GetFont(); local txt = f:CreateFontString(nil, "OVERLAY"); txt:SetJustifyH("CENTER"); local subTxt = f:CreateFontString(nil, "OVERLAY"); subTxt:SetJustifyH("LEFT")
    if isHeader then txt:SetFont(fontFile, 13, fontFlags); txt:SetPoint("TOP", f, "TOP", 0, -10); local ilvlTxt = f:CreateFontString(nil, "OVERLAY"); ilvlTxt:SetFont(fontFile, 14, fontFlags); ilvlTxt:SetPoint("TOP", txt, "BOTTOM", 0, -2); ilvlTxt:SetJustifyH("CENTER"); f.ilvlTxt = ilvlTxt; local bg = f:CreateTexture(nil, "BACKGROUND"); bg:SetAllPoints(); bg:SetColorTexture(unpack(C.sepBtn)); f.bg = bg; local hl = f:CreateTexture(nil, "OVERLAY"); hl:SetAllPoints(); hl:SetColorTexture(1, 1, 1, 0.08); hl:Hide(); f.hl = hl; local h = f:CreateTexture(nil, "ARTWORK"); h:SetColorTexture(unpack(C.sepHighlight)); h:SetHeight(1); h:SetPoint("TOPLEFT"); h:SetPoint("TOPRIGHT"); f.highlight = h; local s = f:CreateTexture(nil, "ARTWORK"); s:SetColorTexture(unpack(C.sepShadow)); s:SetHeight(1); s:SetPoint("BOTTOMLEFT"); s:SetPoint("BOTTOMRIGHT"); f.shadow = s; local lB = f:CreateTexture(nil, "ARTWORK"); lB:SetColorTexture(0, 0, 0, 0.7); lB:SetWidth(1); lB:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0); lB:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 0); f.leftBevel = lB
    else txt:SetFont(fontFile, 13, fontFlags); txt:SetSpacing(3); subTxt:SetFont(fontFile, 11, fontFlags); f.subTxt = subTxt; local bg = f:CreateTexture(nil, "BACKGROUND"); bg:SetAllPoints(); bg:SetColorTexture(0, 0, 0, 0); f.bg = bg end
    f.txt = txt; local vl = f:CreateTexture(nil, "ARTWORK"); vl:SetColorTexture(unpack(C.line)); vl:SetWidth(1); f.vl = vl
    local vIcon = f:CreateTexture(nil, "OVERLAY"); vIcon:SetSize(14, 14); vIcon:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -4); vIcon:SetTexture(6013777); vIcon:Hide(); f.vIcon = vIcon
    f:SetScript("OnEnter", nil); f:SetScript("OnLeave", function() GameTooltip:Hide() end); cellPool[rowIdx][colIdx] = f; return f
end

local RefreshMainFrame, RefreshOptFrame
local function SortChars(a, b) local oa, ob = tonumber(a.sortOrder) or 99999, tonumber(b.sortOrder) or 99999; if oa ~= ob then return oa < ob end return a.ilvl > b.ilvl end

RefreshMainFrame = function()
    if not mainFrame or not mainFrame:IsVisible() then return end; 
    if PurgeExpiredData and PurgeExpiredData() then 
        if RequestUpdate then RequestUpdate() end 
    end
    wipe(sortedList)
    for key, data in pairs(DB.chars) do if not data.hidden then table.insert(sortedList, { key=key, name=data.name, ilvl=data.ilvl, sortOrder=data.sortOrder, classFile=data.classFile, nightmareCount=data.nightmareCount, raids=data.raids, wbKills=data.wbKills, currencies=data.currencies, crests=data.crests, mythicplus=data.mythicplus, weeklies=data.weeklies, tierSetCount=data.tierSetCount, voidcore=data.voidcore, money=data.money, nullaeus=data.nullaeus, hasUnclaimedVault=data.hasUnclaimedVault }) end end table.sort(sortedList, SortChars)
    for _, r in ipairs(rowPool) do r:Hide(); r.highlight:Hide(); r.shadow:Hide(); r.hl:Hide(); if r.iconTx then r.iconTx:Hide() end; r.rawLabel = nil end
    for _, cp in pairs(cellPool) do for _, c in pairs(cp) do c:Hide(); if c.vIcon then c.vIcon:Hide() end; c:SetScript("OnEnter", nil); c:SetScript("OnLeave", function() GameTooltip:Hide() end); c:EnableMouse(true) end end
    local charCount = #sortedList; if charCount == 0 then return end; local totalW = PAD_L * 2 + NAME_W + (charCount * TOON_W)
    
    local ROWS = {}; local s = DB.settings; local showCap = not s.hideCap
    local catOrder = s.catOrder or {"summary", "raid", "mythicplus", "weekly", "delves", "currency"}
    local hasSummary = false; for _, v in ipairs(catOrder) do if v == "summary" then hasSummary = true break end end
    if not hasSummary then table.insert(catOrder, 1, "summary"); s.catOrder = catOrder end

    for _, catID in ipairs(catOrder) do
        if catID == "summary" and not s.hideSummary then local addedHeader = false; if not s.hideSummary_score then table.insert(ROWS, { type="sep", label="角色概要", field="divSummary" }); addedHeader=true; table.insert(ROWS, { type="info_score", label="大秘境评分" }) end; if not s.hideSummary_tier then if not addedHeader then table.insert(ROWS, { type="sep", label="角色概要", field="divSummary" }) end table.insert(ROWS, { type="info_tier", label="套装件数" }) end
        elseif catID == "raid" and not s.hideRaid then table.insert(ROWS, { type="sep", label="团队副本", field="divRaid" }); for _, raid in ipairs(RAID_LIST) do table.insert(ROWS, { type="raid", label=raid.label, key=raid.key, total=raid.total }) end
        elseif catID == "mythicplus" and not s.hideMythicPlus then local addedHeader = false; if not s.hideDungMP then table.insert(ROWS, { type="sep", label="大秘境", field="divDungMP" }); addedHeader=true; table.insert(ROWS, { type="dungMP", label="本周" }) end; if not s.hideKeystone then if not addedHeader then table.insert(ROWS, { type="sep", label="大秘境", field="divDungMP" }) end table.insert(ROWS, { type="keystone", label="持有钥石" }) end
        elseif catID == "weekly" and not s.hideWeekly then local addedHeader = false; local function addWeekly(key, typeStr, labelStr) if not s["hideWeekly_"..key] then if not addedHeader then table.insert(ROWS, { type="sep", label="每周活动", field="divWeekly" }); addedHeader=true end table.insert(ROWS, { type=typeStr, label=labelStr, key=key }) end end
            addWeekly("meta", "weekly_quest", "至暗之夜元任务"); addWeekly("saltheril", "weekly_quest", "联欢晚会"); addWeekly("abundant", "weekly_quest", "丰饶事件"); addWeekly("haranir", "weekly_quest", "哈兰迪尔遗忘的传说"); addWeekly("stormarion", "weekly_quest", "风暴巨灵突袭"); addWeekly("wb", "wb", "世界首领"); addWeekly("nightmarish", "weekly_quest", "狩猎周常任务"); addWeekly("prey", "prey", "狩猎"); addWeekly("world", "weekly_vault", "宏伟宝库 (世界)")
        elseif catID == "delves" and not s.hideCurr then local addedHeader = false; for _, curr in ipairs(CURRENCY_LIST) do if not s["hideCurr_"..curr.key] then if not addedHeader then table.insert(ROWS, { type="sep", label="地下堡", field="divCurr" }); addedHeader=true end local iconFileID = nil; if curr.type == "currency" then local info = C_CurrencyInfo.GetCurrencyInfo(curr.id); if info and info.iconFileID then iconFileID = info.iconFileID end else local iconID = select(5, GetItemInfoInstant(curr.id)); if iconID then iconFileID = iconID end end table.insert(ROWS, { type="curr", label=curr.label, iconID=iconFileID, key=curr.key, cType=curr.type, id=curr.id }) end end
            if not s.hideNullaeus then if not addedHeader then table.insert(ROWS, { type="sep", label="地下堡", field="divCurr" }) end table.insert(ROWS, { type="achieve", label="纳勒乌斯 ??", key="nullaeus" }) end
        elseif catID == "currency" and not s.hideCrest then local addedHeader = false; for _, cr in ipairs(CREST_LIST) do if not s["hideCrest_"..cr.key] then if not addedHeader then table.insert(ROWS, { type="sep", label="货币", field="divCrest" }); addedHeader=true end local info = C_CurrencyInfo.GetCurrencyInfo(cr.id); local iconFileID = info and info.iconFileID; table.insert(ROWS, { type="crest", label="|cff"..cr.color..cr.label.."|r", cleanLabel=cr.label, iconID=iconFileID, key=cr.key, rgb=cr.rgb, hexColor=cr.color, id=cr.id, cType="currency" }) end end
            if not s.hideVoidcore then if not addedHeader then table.insert(ROWS, { type="sep", label="货币", field="divCrest" }); addedHeader=true end table.insert(ROWS, { type="voidcore", label="|cff2445e9虚空之心|r", cleanLabel="虚空之心", iconID="Interface\\Buttons\\UI-GroupLoot-Dice-Up" }) end
            if not s.hideAscShard then if not addedHeader then table.insert(ROWS, { type="sep", label="货币", field="divCrest" }); addedHeader=true end table.insert(ROWS, { type="asc_shard", label="|cff5418c4飞升的虚空碎片|r", cleanLabel="飞升的虚空碎片", iconID=7658133 }) end
            if not s.hideAscBox then if not addedHeader then table.insert(ROWS, { type="sep", label="货币", field="divCrest" }) end table.insert(ROWS, { type="asc_box", label="|cff5418c4飞升的虚空核心宝箱|r", cleanLabel="飞升的虚空核心宝箱", iconID=7658136 }) end end end

    local curY, rowIdx = -PAD_T, 1; local actualHdrH = 48 
    local hdrRow = GetOrCreateRow(rowIdx); hdrRow:SetPoint("TOPLEFT", mainFrame.body, "TOPLEFT", PAD_L, curY); hdrRow:SetSize(totalW - PAD_L*2, actualHdrH); hdrRow.bg:SetColorTexture(0, 0, 0, 0); hdrRow.accent:Hide(); hdrRow.lbl:SetText(""); hdrRow:EnableMouse(false); hdrRow:Show()
    if not hdrRow.leftInfoContainer then hdrRow.leftInfoContainer = CreateFrame("Frame", nil, hdrRow); hdrRow.leftInfoContainer:SetPoint("LEFT", hdrRow, "LEFT", 10, 0); local fontFile, _, fontFlags = GameFontNormal:GetFont(); hdrRow.tgLabel = hdrRow.leftInfoContainer:CreateFontString(nil, "OVERLAY"); hdrRow.tgLabel:SetFont(fontFile, 12, fontFlags); hdrRow.tgLabel:SetJustifyH("LEFT"); hdrRow.tgLabel:SetJustifyV("MIDDLE"); hdrRow.tgLabel:SetText("总金币"); hdrRow.tgColon = hdrRow.leftInfoContainer:CreateFontString(nil, "OVERLAY"); hdrRow.tgColon:SetFont(fontFile, 12, fontFlags); hdrRow.tgColon:SetJustifyH("CENTER"); hdrRow.tgColon:SetJustifyV("MIDDLE"); hdrRow.tgColon:SetPoint("LEFT", hdrRow.tgLabel, "LEFT", 55, 0); hdrRow.tgColon:SetText(":"); hdrRow.tgVal = hdrRow.leftInfoContainer:CreateFontString(nil, "OVERLAY"); hdrRow.tgVal:SetFont(fontFile, 12, fontFlags); hdrRow.tgVal:SetJustifyH("LEFT"); hdrRow.tgVal:SetJustifyV("MIDDLE"); hdrRow.tgVal:SetPoint("LEFT", hdrRow.tgColon, "RIGHT", 4, 0); hdrRow.wbLabel = hdrRow.leftInfoContainer:CreateFontString(nil, "OVERLAY"); hdrRow.wbLabel:SetFont(fontFile, 12, fontFlags); hdrRow.wbLabel:SetJustifyH("LEFT"); hdrRow.wbLabel:SetJustifyV("MIDDLE"); hdrRow.wbLabel:SetText("战团"); hdrRow.wbColon = hdrRow.leftInfoContainer:CreateFontString(nil, "OVERLAY"); hdrRow.wbColon:SetFont(fontFile, 12, fontFlags); hdrRow.wbColon:SetJustifyH("CENTER"); hdrRow.wbColon:SetJustifyV("MIDDLE"); hdrRow.wbColon:SetPoint("LEFT", hdrRow.wbLabel, "LEFT", 55, 0); hdrRow.wbColon:SetText(":"); hdrRow.wbVal = hdrRow.leftInfoContainer:CreateFontString(nil, "OVERLAY"); hdrRow.wbVal:SetFont(fontFile, 12, fontFlags); hdrRow.wbVal:SetJustifyH("LEFT"); hdrRow.wbVal:SetJustifyV("MIDDLE"); hdrRow.wbVal:SetPoint("LEFT", hdrRow.wbColon, "RIGHT", 4, 0) end
    hdrRow.leftInfoContainer:SetSize(180, actualHdrH); hdrRow.tgLabel:Hide(); hdrRow.tgColon:Hide(); hdrRow.tgVal:Hide(); hdrRow.wbLabel:Hide(); hdrRow.wbColon:Hide(); hdrRow.wbVal:Hide()
    local showTotal = not s.hideTotalGold and not s.hideGold; local showWB = not s.hideWarbandGold and not s.hideGold
    if showTotal and showWB then hdrRow.tgLabel:SetPoint("LEFT", hdrRow.leftInfoContainer, "LEFT", 0, 8); hdrRow.wbLabel:SetPoint("LEFT", hdrRow.leftInfoContainer, "LEFT", 0, -8) elseif showTotal then hdrRow.tgLabel:SetPoint("LEFT", hdrRow.leftInfoContainer, "LEFT", 0, 0) elseif showWB then hdrRow.wbLabel:SetPoint("LEFT", hdrRow.leftInfoContainer, "LEFT", 0, 0) end
    if showTotal then hdrRow.tgVal:SetText(FormatGold(GetTotalGold()) .. HEADER_GOLD_TEX); hdrRow.tgLabel:Show(); hdrRow.tgColon:Show(); hdrRow.tgVal:Show() end
    if showWB then hdrRow.wbVal:SetText(FormatGold(DB.warbandMoney or 0) .. HEADER_GOLD_TEX); hdrRow.wbLabel:Show(); hdrRow.wbColon:Show(); hdrRow.wbVal:Show() end

    for col, t in ipairs(sortedList) do local r, g, b = GetClassColor(t.classFile); local cell = GetOrCreateCell(rowIdx, col, true); local xOff = NAME_W + (col - 1) * TOON_W; cell:SetPoint("TOPLEFT", hdrRow, "TOPLEFT", xOff, 0); cell:SetSize(TOON_W, actualHdrH); cell.txt:SetText(t.name); cell.txt:SetTextColor(r, g, b); cell.ilvlTxt:SetText("|cffffffff"..math.floor(t.ilvl).."|r"); cell.vl:ClearAllPoints(); cell.vl:SetPoint("TOPLEFT", cell, "TOPLEFT", 0, 0); cell.vl:SetHeight(actualHdrH); cell.vl:Show(); if cell.leftBevel then cell.leftBevel:Show() end
        cell:Show()
        cell:SetScript("OnEnter", function(self) if self.hl then self.hl:Show() end if not s.hideCharGold and not s.hideGold then GameTooltip:SetOwner(self, "ANCHOR_BOTTOM", 0, -5); GameTooltip:ClearLines(); local money = t.money or 0; GameTooltip:AddLine(FormatGold(money) .. CHAR_GOLD_TEX, 1, 1, 1); GameTooltip:Show() end end); cell:SetScript("OnLeave", function(self) if self.hl then self.hl:Hide() end; GameTooltip:Hide() end) end
    curY = curY - (actualHdrH + 8); rowIdx = rowIdx + 1

    local visRow, currentSection = 0, nil
    for _, rowDef in ipairs(ROWS) do
        if rowDef.type == "sep" then currentSection = rowDef.field; local sf = GetOrCreateRow(rowIdx); sf:SetPoint("TOPLEFT", mainFrame.body, "TOPLEFT", PAD_L, curY); sf:SetSize(totalW - PAD_L*2, ROW_H); sf.rawLabel = rowDef.cleanLabel or rowDef.label; local isCollapsed = sectionCollapsed[currentSection]; sf.bg:SetColorTexture(unpack(C.sepBtn)); sf.highlight:Show(); sf.shadow:Show(); sf.hl:Show(); if sf.iconTx then sf.iconTx:Hide() end; local defaultFont, _, fontFlags = GameFontNormal:GetFont(); sf.lbl:ClearAllPoints(); sf.lbl:SetPoint("TOPLEFT", sf, "TOPLEFT", 8, 0); sf.lbl:SetSize(NAME_W - 8, ROW_H); sf.lbl:SetJustifyH("LEFT"); sf.lbl:SetFont(defaultFont, 14, fontFlags); sf.lbl:SetText((isCollapsed and "+  " or "-  ") .. rowDef.label); sf.lbl:SetTextColor(unpack(C.txtHeader)); sf.accent:SetWidth(2); sf.accent:SetHeight(ROW_H); sf.accent:SetPoint("LEFT", sf, "LEFT", 0, 0); sf.accent:Show(); sf:EnableMouse(true); local targetField = rowDef.field; sf:SetScript("OnClick", function() if activeEditBox then activeEditBox:ClearFocus() end; sectionCollapsed[targetField] = not sectionCollapsed[targetField]; RefreshMainFrame() end); sf:Show(); curY = curY - ROW_H; rowIdx = rowIdx + 1
        elseif currentSection == nil or not sectionCollapsed[currentSection] then if currentSection ~= nil then visRow = visRow + 1 end local rowH = ROW_H
            if rowDef.type == "raid" then local activeRaidLines = 0; if not s.hideRaidLFR then activeRaidLines = activeRaidLines + 1 end if not s.hideRaidNormal then activeRaidLines = activeRaidLines + 1 end if not s.hideRaidHeroic then activeRaidLines = activeRaidLines + 1 end if not s.hideRaidMythic then activeRaidLines = activeRaidLines + 1 end if activeRaidLines == 0 then activeRaidLines = 1 end rowH = math.max(ROW_H, activeRaidLines * 19 + 2)
            elseif rowDef.type == "dungMP" or rowDef.type == "keystone" then rowH = DUNG_ROW_H elseif rowDef.type == "voidcore" or rowDef.type == "asc_shard" or rowDef.type == "asc_box" then rowH = VOID_H elseif rowDef.type == "curr" or rowDef.type == "crest" or rowDef.type == "achieve" then rowH = CURR_ROW_H end
            local rf = GetOrCreateRow(rowIdx); rf.rawLabel = rowDef.cleanLabel or rowDef.label; rf:SetPoint("TOPLEFT", mainFrame.body, "TOPLEFT", PAD_L, curY); rf:SetSize(totalW - PAD_L*2, rowH); if visRow % 2 == 0 then rf.bg:SetColorTexture(unpack(C.rowEven)) else rf.bg:SetColorTexture(unpack(C.rowOdd)) end rf.accent:Hide(); rf.hl:Hide(); local defaultFont, _, fontFlags = GameFontNormal:GetFont(); rf.lbl:SetFont(defaultFont, 13, fontFlags); rf.lbl:SetText(rowDef.label); rf.lbl:SetTextColor(unpack(C.txtNormal)); rf.lbl:ClearAllPoints(); rf.lbl:SetWidth(0); rf.lbl:SetHeight(rowH)
            if rowDef.iconID and rf.iconTx then rf.iconTx:SetTexture(rowDef.iconID); rf.iconTx:Show(); rf.lbl:SetPoint("CENTER", rf, "LEFT", (NAME_W / 2) + 11, 0); rf.iconTx:ClearAllPoints(); rf.iconTx:SetPoint("RIGHT", rf.lbl, "LEFT", -4, 0) else if rf.iconTx then rf.iconTx:Hide() end rf.lbl:SetPoint("CENTER", rf, "LEFT", NAME_W / 2, 0) end
            rf.lbl:SetJustifyH("CENTER"); rf.lbl:Show(); rf:EnableMouse(false); rf:SetScript("OnClick", nil); rf:Show()
            for col, t in ipairs(sortedList) do local cell = GetOrCreateCell(rowIdx, col, false); local xOff = NAME_W + (col - 1) * TOON_W; cell:SetPoint("TOPLEFT", rf, "TOPLEFT", xOff, 0); cell:SetSize(TOON_W, rowH); cell.vl:ClearAllPoints(); cell.vl:SetPoint("TOPLEFT", cell, "TOPLEFT", 0, 0); cell.vl:SetHeight(rowH); cell.vl:Show(); cell.bg:SetColorTexture(0, 0, 0, 0); cell:SetScript("OnMouseDown", nil); local valText, subText, txtColor = "-", nil, COLOR_ZERO; local isMaxed = false; local labelHexColor = "ffffffff"
                if rowDef.type == "achieve" then local isDone = t[rowDef.key]; if isDone then valText = "完成"; txtColor = COLOR_CAP_GRAY else valText = "-"; txtColor = COLOR_ZERO end cell:EnableMouse(true); cell:SetScript("OnEnter", function(self) GameTooltip:SetOwner(mainFrame, "ANCHOR_NONE"); GameTooltip:SetPoint("RIGHT", mainFrame, "LEFT", -5, 0); GameTooltip:ClearLines(); GameTooltip:AddLine("黑暗中的光 (功绩)", 1, 0.82, 0); GameTooltip:AddLine("纳勒乌斯 ?? 难度击杀情况", 1, 1, 1); GameTooltip:AddLine("基于成就，一旦达成将永久显示。", 0.5, 0.5, 0.5); GameTooltip:Show() end)
                elseif rowDef.type == "voidcore" then local vd = t.voidcore; labelHexColor = "2445e9"; if vd and (vd.amount > 0 or (vd.totalEarned and vd.totalEarned > 0)) then valText = tostring(vd.amount); if showCap and vd.seasonMax and vd.seasonMax > 0 then local earned = vd.totalEarned or vd.amount; if earned >= vd.seasonMax then subText = "|cff888888("..earned.."/"..vd.seasonMax..")|r"; isMaxed = true else subText = "|cff"..labelHexColor.."("..earned.."/"..vd.seasonMax..")|r" end end txtColor = (vd.amount > 0) and COLOR_WHITE or COLOR_CAP_GRAY else valText = "0"; txtColor = COLOR_CAP_GRAY end cell:EnableMouse(true); cell:SetScript("OnEnter", function(self) GameTooltip:SetOwner(mainFrame, "ANCHOR_NONE"); GameTooltip:SetPoint("RIGHT", mainFrame, "LEFT", -5, 0); GameTooltip:SetCurrencyByID(VOIDCORE_ID); GameTooltip:Show() end)
                elseif rowDef.type == "asc_shard" then local amount = t.currencies and t.currencies.asc_shard or 0; labelHexColor = "5418c4"; valText = tostring(amount); txtColor = (amount > 0) and COLOR_WHITE or COLOR_CAP_GRAY; cell:EnableMouse(true); cell:SetScript("OnEnter", function(self) GameTooltip:SetOwner(mainFrame, "ANCHOR_NONE"); GameTooltip:SetPoint("RIGHT", mainFrame, "LEFT", -5, 0); GameTooltip:SetItemByID(268650); GameTooltip:Show() end)
                elseif rowDef.type == "asc_box" then local earned = t.currencies and t.currencies.asc_box_earned or 0; local hold = t.currencies and t.currencies.asc_box or 0; local usedCount = earned - hold; if usedCount < 0 then usedCount = 0 end; labelHexColor = "5418c4"; valText = tostring(hold); if showCap then subText = "|cff888888已使用 : "..usedCount.."|r" end; txtColor = (hold > 0) and COLOR_WHITE or COLOR_CAP_GRAY; cell:EnableMouse(true); cell:SetScript("OnEnter", function(self) GameTooltip:SetOwner(mainFrame, "ANCHOR_NONE"); GameTooltip:SetPoint("RIGHT", mainFrame, "LEFT", -5, 0); GameTooltip:SetItemByID(268552); GameTooltip:Show() end)
                elseif rowDef.type == "info_score" then local score = t.mythicplus and t.mythicplus.score or 0; if score > 0 then local color = C_ChallengeMode and C_ChallengeMode.GetDungeonScoreRarityColor and C_ChallengeMode.GetDungeonScoreRarityColor(score); local hex = color and color:GenerateHexColor() or "ffffffff"; valText = string.format("|c%s%d|r", hex, score); txtColor = COLOR_WHITE else valText = "-"; txtColor = COLOR_ZERO end cell:EnableMouse(false)
                elseif rowDef.type == "info_tier" then local tier = t.tierSetCount or 0; if tier >= 4 then valText = "|cffa335ee" .. tier .. "件|r" elseif tier >= 2 then valText = "|cff0070dd" .. tier .. "件|r" elseif tier > 0 then valText = "|cff1eff00" .. tier .. "件|r" else valText = "0件" end txtColor = (tier > 0) and COLOR_WHITE or COLOR_ZERO; cell:EnableMouse(false)
                elseif rowDef.type == "raid" then local rd = t.raids and t.raids[rowDef.key] or {}; local tot = rowDef.total or 0; local function fmtDiff(k, name, colorCode) local clr = (k > 0) and ("FF" .. colorCode) or "03222222"; return string.format("|c%s%s %d/%d|r", clr, name, k, tot) end; local lines = {}; if not s.hideRaidLFR then table.insert(lines, fmtDiff(rd.lfr or 0, "随排", "40C040")) end; if not s.hideRaidNormal then table.insert(lines, fmtDiff(rd.normal or 0, "普通", "0070DD")) end; if not s.hideRaidHeroic then table.insert(lines, fmtDiff(rd.heroic or 0, "英雄", "A335EE")) end; if not s.hideRaidMythic then table.insert(lines, fmtDiff(rd.mythic or 0, "史诗", "FF8000")) end; if #lines > 0 then valText = table.concat(lines, "\n") else valText = "-" end txtColor = COLOR_WHITE; cell:EnableMouse(false)
                elseif rowDef.type == "dungMP" then local md = t.mythicplus; local runs = md and md.runCount or 0; local runList = md and md.runList or {}; local fullHistory = md and md.fullHistory or {}; local thresholds = md and md.thresholds or {1, 4, 8}; if runs > 0 or #fullHistory > 0 then local bestStr = string.format("|cffffffff%d次|r", runs); local v = {}; for i = 1, 3 do local th = thresholds[i] or (i==1 and 1 or i==2 and 4 or 8); local run = runList[th]; if run then v[i] = string.format("|cff4dff4d%s|r", tostring(run.level)) elseif th <= runs then v[i] = "|cffffffff?|r" else v[i] = "-" end end valText = bestStr .. "\n" .. string.format("%s / %s / %s", v[1], v[2], v[3]); txtColor = COLOR_WHITE else valText = "无记录"; txtColor = COLOR_ZERO end cell:EnableMouse(true); cell:SetScript("OnEnter", function(self) GameTooltip:SetOwner(mainFrame, "ANCHOR_NONE"); GameTooltip:SetPoint("RIGHT", mainFrame, "LEFT", -5, 0); GameTooltip:ClearLines(); GameTooltip:AddLine("本周", 1, 0.82, 0); local histToUse = (md and md.fullHistory and #md.fullHistory > 0) and md.fullHistory or (md and md.runList); if histToUse and #histToUse > 0 then local threshSet = {}; for _, th in ipairs(thresholds) do threshSet[th] = true end; for j = 1, math.min(8, #histToUse) do local run = histToUse[j]; local lvlStr = tostring(run.level); local txt = string.format("%s %s", lvlStr, run.name or "?"); local rColor, gColor, bColor = 1, 1, 1; if not run.completed then rColor, gColor, bColor = 0.6, 0.6, 0.6; txt = txt .. " (超时)" elseif threshSet[j] then rColor, gColor, bColor = 0.3, 1.0, 0.3 end GameTooltip:AddLine(txt, rColor, gColor, bColor) end else GameTooltip:AddLine("无详细记录", 0.5, 0.5, 0.5) end GameTooltip:Show() end)
                elseif rowDef.type == "keystone" then local md = t.mythicplus; local kl = md and md.keyLevel or 0; local kn = md and md.keyName; local kMapID = md and md.keyMapID or 0; local kLink = md and md.keyLink;
                    if t.hasUnclaimedVault then cell.vIcon:Show() end
                    if kl > 0 and kn and kn ~= "" then local shortName = DUNGEON_SHORT_NAMES[kn] or kn; valText = string.format("%d %s", kl, shortName); txtColor = COLOR_WHITE
                    else valText = "-"; txtColor = COLOR_ZERO end
                    if (kl > 0 and kn and kn ~= "") or t.hasUnclaimedVault then
                        cell:EnableMouse(true); cell:SetScript("OnEnter", function(self) self.bg:SetColorTexture(0.2, 0.35, 0.6, 0.4); GameTooltip:SetOwner(mainFrame, "ANCHOR_NONE"); GameTooltip:SetPoint("RIGHT", mainFrame, "LEFT", -5, 0); GameTooltip:ClearLines(); local added = false; if kl > 0 and kn and kn ~= "" then GameTooltip:AddLine("史诗钥石", 1, 0.82, 0); GameTooltip:AddLine(string.format("%s (%d层)", kn, kl), 1, 1, 1); GameTooltip:AddLine("<Shift+点击可链接到聊天框>", 0.2, 1, 0.2); added = true end if t.hasUnclaimedVault then if added then GameTooltip:AddLine(" ") end GameTooltip:AddLine("|T6013777:14|t 待领取的宏伟宝库奖励", 1, 0.82, 0) end GameTooltip:Show() end); cell:SetScript("OnLeave", function(self) self.bg:SetColorTexture(0, 0, 0, 0); GameTooltip:Hide() end); cell:SetScript("OnMouseDown", function(self, button) if IsShiftKeyDown() and button == "LeftButton" and kLink then if ChatEdit_GetActiveWindow() then ChatEdit_InsertLink(kLink) else ChatFrame_OpenChat(kLink) end end end)
                    else cell:EnableMouse(false); cell:SetScript("OnEnter", nil); cell:SetScript("OnLeave", nil); cell:SetScript("OnMouseDown", nil) end
                elseif rowDef.type == "weekly_quest" then if rowDef.key == "haranir" then local isDone = false; if DB and DB.chars then for _, charData in pairs(DB.chars) do if charData.weeklies and charData.weeklies.haranir then isDone = true break end end end if isDone then valText = ICON_CHECK; txtColor = COLOR_WHITE else valText = "-"; txtColor = COLOR_ZERO end elseif rowDef.key == "saltheril" then local count = t.weeklies and t.weeklies.saltherilCount or 0; if count >= 3 then valText = ICON_CHECK; txtColor = COLOR_WHITE elseif count > 0 then valText = count .. "/3"; txtColor = COLOR_WHITE else valText = "-"; txtColor = COLOR_ZERO end else local isDone = t.weeklies and t.weeklies[rowDef.key]; if isDone then valText = ICON_CHECK; txtColor = COLOR_WHITE else valText = "-"; txtColor = COLOR_ZERO end end cell:EnableMouse(false)
                elseif rowDef.type == "weekly_vault" then local wCount = t.weeklies and t.weeklies.worldCount or 0; local wList = t.weeklies and t.weeklies.worldList or {}; local wRuns = t.weeklies and t.weeklies.worldRuns or {}; if wCount > 0 or #wList > 0 then local v = {}; for i = 1, 3 do local act = wList[i]; if act then if wCount >= act.threshold then local lvl = (act.level and act.level > 0) and act.level or "O"; if type(lvl) == "number" and lvl >= 8 then v[i] = "|cff44ff44" .. lvl .. "|r" else v[i] = "|cffffffff" .. lvl .. "|r" end else v[i] = "-" end else v[i] = "-" end end valText = string.format("%s / %s / %s", v[1], v[2], v[3]); txtColor = COLOR_WHITE; cell:EnableMouse(true); cell:SetScript("OnEnter", function(self) GameTooltip:SetOwner(mainFrame, "ANCHOR_NONE"); GameTooltip:SetPoint("RIGHT", mainFrame, "LEFT", -5, 0); GameTooltip:ClearLines(); GameTooltip:AddLine("宏伟宝库 - 世界 (地下堡及野外)", 1, 0.82, 0); if #wList > 0 then for i, act in ipairs(wList) do local isDone = wCount >= act.threshold; local prog = math.min(wCount, act.threshold); local tierStr = (act.level and act.level > 0) and (act.level.."层") or "未知"; if isDone then GameTooltip:AddLine(string.format("%d格: |cff44ff44%d / %d|r - 可获得 %s 奖励", i, prog, act.threshold, tierStr), 1, 1, 1) else GameTooltip:AddLine(string.format("%d格: |cffaaaaaa%d / %d|r - 未解锁", i, prog, act.threshold), 0.7, 0.7, 0.7) if i == 1 or (wList[i-1] and wCount >= wList[i-1].threshold) then GameTooltip:AddLine(string.format("  再完成 %d 次即可解锁奖励。", act.threshold - prog), 0.26, 1.0, 0.26) end end end if #wRuns > 0 then GameTooltip:AddLine(" "); GameTooltip:AddLine(string.format("本周最高记录 %d 个", #wRuns), 1, 1, 1); local runCounts = {}; local sortedLevels = {}; for _, lvl in ipairs(wRuns) do if not runCounts[lvl] then runCounts[lvl] = 0; table.insert(sortedLevels, lvl) end runCounts[lvl] = runCounts[lvl] + 1 end table.sort(sortedLevels, function(a, b) return a > b end); for _, lvl in ipairs(sortedLevels) do local count = runCounts[lvl]; if lvl <= 1 then GameTooltip:AddLine(string.format("- 1 层及野外活动 (%d)", count), 1, 1, 1) else GameTooltip:AddLine(string.format("- %d 层 (%d)", lvl, count), 1, 1, 1) end end end else GameTooltip:AddLine("没有进行中的记录。", 0.5, 0.5, 0.5) end GameTooltip:Show() end) else valText = "无记录"; txtColor = COLOR_ZERO; cell:EnableMouse(true); cell:SetScript("OnEnter", function(self) GameTooltip:SetOwner(mainFrame, "ANCHOR_NONE"); GameTooltip:SetPoint("RIGHT", mainFrame, "LEFT", -5, 0); GameTooltip:ClearLines(); GameTooltip:AddLine("宏伟宝库 - 世界 (地下堡及野外)", 1, 0.82, 0); GameTooltip:AddLine("没有进行中的记录。", 0.5, 0.5, 0.5); GameTooltip:Show() end) end
                elseif rowDef.type == "crest" then local qty = t.crests and t.crests[rowDef.key] or 0; local sMax = t.crests and t.crests[rowDef.key.."_seasonMax"]; local earned = t.crests and t.crests[rowDef.key.."_totalEarned"] or qty; labelHexColor = rowDef.hexColor; if qty > 0 or earned > 0 then valText = tostring(qty); if showCap and sMax then if earned >= sMax then subText = "|cff888888("..earned.."/"..sMax..")|r"; isMaxed = true else subText = "|cff"..labelHexColor.."("..earned.."/"..sMax..")|r" end end txtColor = (qty > 0) and rowDef.rgb or COLOR_CAP_GRAY else valText = "0"; txtColor = COLOR_CAP_GRAY end cell:EnableMouse(true); cell:SetScript("OnEnter", function(self) GameTooltip:SetOwner(mainFrame, "ANCHOR_NONE"); GameTooltip:SetPoint("RIGHT", mainFrame, "LEFT", -5, 0); GameTooltip:SetCurrencyByID(rowDef.id); GameTooltip:Show() end)
                elseif rowDef.type == "prey" then local nm = t.nightmareCount or 0; if nm >= 4 then valText = ICON_CHECK; txtColor = COLOR_WHITE else valText = nm.."/4"; txtColor = (nm > 0) and COLOR_WHITE or COLOR_ZERO end cell:EnableMouse(false)
                elseif rowDef.type == "wb" then local killed = false; if t.wbKills then for _, wb in ipairs(WB_LIST) do if t.wbKills[wb.key] then killed = true break end end end if killed then valText = ICON_CHECK; txtColor = COLOR_WHITE else valText = "-"; txtColor = COLOR_ZERO end cell:EnableMouse(false)
                elseif rowDef.type == "curr" then local amount = t.currencies and t.currencies[rowDef.key] or 0; if rowDef.key == "trovebounty" then local used = t.currencies and t.currencies.trovebountyUsed; if used == true then valText = ICON_CHECK; txtColor = COLOR_WHITE elseif used == false then valText = "可使用"; txtColor = COLOR_WHITE else valText = "-"; txtColor = COLOR_ZERO end cell:EnableMouse(true); cell:SetScript("OnEnter", function(self) GameTooltip:SetOwner(mainFrame, "ANCHOR_NONE"); GameTooltip:SetPoint("RIGHT", mainFrame, "LEFT", -5, 0); GameTooltip:ClearLines(); GameTooltip:AddLine("寻宝者的恩惠", 1, 0.82, 0); GameTooltip:AddLine(" "); GameTooltip:AddLine(t.name .. " 角色信息", 0.4, 0.7, 1); local troveCnt = t.currencies and t.currencies.trovebounty or 0; GameTooltip:AddLine("持有量: |cffffffff" .. troveCnt .. "个|r", 1, 1, 1); if used == true then GameTooltip:AddLine("状态: |cff26ff4c本周已使用|r", 1, 1, 1) elseif used == false then GameTooltip:AddLine("状态: |cffffd114本周可使用|r", 1, 1, 1) else GameTooltip:AddLine("状态: 未知", 0.5, 0.5, 0.5) end GameTooltip:Show() end) else local sMax = t.currencies and t.currencies[rowDef.key.."_weekMax"]; local earn = t.currencies and t.currencies[rowDef.key.."_weekEarn"] or 0; if amount > 0 or earn > 0 then valText = tostring(amount); if showCap and sMax then if earn >= sMax then subText = "|cff888888("..earn.."/"..sMax..")|r"; isMaxed = true else subText = "|cffffffff("..earn.."/"..sMax..")|r" end end txtColor = (amount > 0) and COLOR_WHITE or COLOR_CAP_GRAY else valText = "0"; txtColor = COLOR_CAP_GRAY end cell:EnableMouse(true); cell:SetScript("OnEnter", function(self) GameTooltip:SetOwner(mainFrame, "ANCHOR_NONE"); GameTooltip:SetPoint("RIGHT", mainFrame, "LEFT", -5, 0); GameTooltip:SetCurrencyByID(rowDef.id); GameTooltip:Show() end) end end
                if rowDef.type ~= "crest" and rowDef.type ~= "curr" and rowDef.type ~= "voidcore" and rowDef.type ~= "asc_shard" and rowDef.type ~= "asc_box" then cell.txt:ClearAllPoints(); cell.txt:SetJustifyH("CENTER"); cell.txt:SetJustifyV("MIDDLE"); if cell.subTxt then cell.subTxt:ClearAllPoints(); cell.subTxt:SetJustifyH("LEFT") end if subText and cell.subTxt then cell.txt:SetText(valText); cell.subTxt:SetText(subText); cell.txt:SetPoint("BOTTOM", cell, "CENTER", 0, 1); cell.subTxt:SetPoint("TOP", cell, "CENTER", 0, -1); cell.subTxt:SetTextColor(unpack(txtColor)); cell.subTxt:Show() else if rowDef.type == "raid" or rowDef.type == "dungMP" or rowDef.type == "keystone" then cell.txt:SetAllPoints() else cell.txt:SetPoint("CENTER", cell, "CENTER", 0, 0) end if cell.subTxt then cell.subTxt:Hide() end cell.txt:SetText(valText) end cell.txt:SetTextColor(unpack(txtColor)); cell:Show() else cell.txt:ClearAllPoints(); cell.txt:SetJustifyH("CENTER"); cell.txt:SetJustifyV("MIDDLE"); if cell.subTxt then cell.subTxt:ClearAllPoints(); cell.subTxt:SetJustifyH("CENTER"); cell.subTxt:SetJustifyV("MIDDLE") end if subText and cell.subTxt then cell.txt:SetText(valText); cell.subTxt:SetText(subText); cell.txt:SetPoint("BOTTOM", cell, "CENTER", 0, 1); cell.subTxt:SetPoint("TOP", cell, "CENTER", 0, -1); local fontFile = GameFontNormal:GetFont(); cell.subTxt:SetFont(fontFile, 10, ""); cell.subTxt:SetAlpha(1.0); cell.subTxt:Show() else cell.txt:SetPoint("CENTER", cell, "CENTER", 0, 0); if cell.subTxt then cell.subTxt:Hide(); cell.subTxt:SetAlpha(1.0) end cell.txt:SetText(valText) end cell.txt:SetTextColor(unpack(txtColor)); cell:Show() end end curY = curY - rowH; rowIdx = rowIdx + 1 end end mainFrame:SetSize(totalW, math.abs(curY) + PAD_L); mainFrame.body:SetSize(totalW, math.abs(curY) + PAD_L)
    if mainFrame.btnRecent then mainFrame.btnRecent:Update() end
end

-- ============================================================
--  独立自定义选项面板 (现代 iPhone 风格 UI)
-- ============================================================
local function RebuildSortOrders(targetKey, newVal)
    if not DB or not DB.chars then return end; local ordered = {}; for k, data in pairs(DB.chars) do if data.sortOrder and k ~= targetKey then table.insert(ordered, {key = k, order = data.sortOrder}) end end table.sort(ordered, function(a, b) return a.order < b.order end); if newVal and newVal > 0 then local insertIdx = math.floor(newVal); if insertIdx > #ordered + 1 then insertIdx = #ordered + 1 end; if insertIdx < 1 then insertIdx = 1 end; table.insert(ordered, insertIdx, {key = targetKey, order = newVal}) end
    for i, item in ipairs(ordered) do DB.chars[item.key].sortOrder = i end; if (not newVal or newVal <= 0) and targetKey and DB.chars[targetKey] then DB.chars[targetKey].sortOrder = nil end
end

local OPT_LAYOUT = {
    { type="cat", id="char", label="角色设置", noMaster=true, fixed=true },
    { type="toggle", cat="char", label="实例绑定框", db="hideInstBox", invert=true },
    { type="cat", id="gold", label="金币", db="hideGold", invert=true, fixed=true },
    { type="toggle", cat="gold", label="总金币", db="hideTotalGold", invert=true },
    { type="toggle", cat="gold", label="战团金币", db="hideWarbandGold", invert=true },
    { type="toggle", cat="gold", label="按角色鼠标悬停", db="hideCharGold", invert=true },
    { type="cat", id="summary", label="角色概要", db="hideSummary", invert=true },
    { type="toggle", cat="summary", label="大秘境评分", db="hideSummary_score", invert=true },
    { type="toggle", cat="summary", label="套装件数", db="hideSummary_tier", invert=true },
    { type="cat", id="raid", label="团队副本", db="hideRaid", invert=true },
    { type="toggle", cat="raid", label="随机", db="hideRaidLFR", invert=true },
    { type="toggle", cat="raid", label="普通", db="hideRaidNormal", invert=true },
    { type="toggle", cat="raid", label="英雄", db="hideRaidHeroic", invert=true },
    { type="toggle", cat="raid", label="史诗", db="hideRaidMythic", invert=true },
    { type="cat", id="mythicplus", label="大秘境", db="hideMythicPlus", invert=true },
    { type="toggle", cat="mythicplus", label="本周", db="hideDungMP", invert=true },
    { type="toggle", cat="mythicplus", label="持有钥石", db="hideKeystone", invert=true },
    { type="cat", id="weekly", label="每周活动", db="hideWeekly", invert=true },
    { type="toggle", cat="weekly", label="至暗之夜元贡献度", db="hideWeekly_meta", invert=true },
    { type="toggle", cat="weekly", label="联欢晚会", db="hideWeekly_saltheril", invert=true },
    { type="toggle", cat="weekly", label="丰饶事件", db="hideWeekly_abundant", invert=true },
    { type="toggle", cat="weekly", label="哈赖尼尔遗忘的传说", db="hideWeekly_haranir", invert=true },
    { type="toggle", cat="weekly", label="斯托玛兰突袭战", db="hideWeekly_stormarion", invert=true },
    { type="toggle", cat="weekly", label="世界首领", db="hideWeekly_wb", invert=true },
    { type="toggle", cat="weekly", label="狩猎周常任务", db="hideWeekly_nightmarish", invert=true },
    { type="toggle", cat="weekly", label="狩猎", db="hideWeekly_prey", invert=true },
    { type="toggle", cat="weekly", label="宏伟宝库 (世界)", db="hideWeekly_world", invert=true },
    { type="cat", id="delves", label="地下堡", db="hideCurr", invert=true },
    { type="toggle", cat="delves", label="修复的宝箱钥匙", db="hideCurr_cofferkey", invert=true },
    { type="toggle", cat="delves", label="宝箱钥匙碎片", db="hideCurr_keyshard", invert=true },
    { type="toggle", cat="delves", label="寻宝者的恩惠", db="hideCurr_trovebounty", invert=true },
    { type="toggle", cat="delves", label="纳勒乌斯 ??", db="hideNullaeus", invert=true },
    { type="cat", id="currency", label="货币", db="hideCrest", invert=true },
    { type="toggle", cat="currency", label="闪耀的火花粉尘", db="hideCrest_sparkdust", invert=true },
    { type="toggle", cat="currency", label="套装转换", db="hideCrest_manaflux", invert=true },
    { type="toggle", cat="currency", label="冒险者", db="hideCrest_adventurer", invert=true },
    { type="toggle", cat="currency", label="老兵", db="hideCrest_veteran", invert=true },
    { type="toggle", cat="currency", label="勇士", db="hideCrest_champion", invert=true },
    { type="toggle", cat="currency", label="英雄", db="hideCrest_hero", invert=true },
    { type="toggle", cat="currency", label="传说", db="hideCrest_myth", invert=true },
    { type="toggle", cat="currency", label="血腥纹章", db="hideCrest_bloody", invert=true },
    { type="toggle", cat="currency", label="荣誉点数", db="hideCrest_honor", invert=true },
    { type="toggle", cat="currency", label="征服点数", db="hideCrest_conquest", invert=true },
    { type="toggle", cat="currency", label="虚空之心", db="hideVoidcore", invert=true },
    { type="toggle", cat="currency", label="飞升的虚空碎片", db="hideAscShard", invert=true },
    { type="toggle", cat="currency", label="飞升的虚空核心宝箱", db="hideAscBox", invert=true },
    { type="toggle", cat="currency", label="钝顿碎片", db="hideCrest_dundun", invert=true },
    { type="toggle", cat="currency", label="显示上限", db="hideCap", invert=true },
}

local function GetOrCreateOptRow(idx)
    if optRowPool[idx] then return optRowPool[idx] end
    local f = CreateFrame("Button", nil, optFrame.body); local bg = f:CreateTexture(nil, "BACKGROUND"); bg:SetAllPoints(); f.bg = bg; local hl = f:CreateTexture(nil, "HIGHLIGHT"); hl:SetAllPoints(); hl:SetColorTexture(1, 1, 1, 0.08); f.hl = hl; f:HookScript("OnMouseDown", function() if activeEditBox then activeEditBox:ClearFocus() end end)
    local catBorder = CreateFrame("Frame", nil, f, "BackdropTemplate"); catBorder:SetAllPoints(); catBorder:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 }); catBorder:SetBackdropBorderColor(0, 0, 0, 0); f.catBorder = catBorder; local lbl = f:CreateFontString(nil, "OVERLAY", "GameFontNormal"); f.lbl = lbl
    f.orderNum = f:CreateFontString(nil, "OVERLAY", "GameFontNormal"); f.orderNum:SetPoint("LEFT", f, "LEFT", 8, 0); f.orderNum:SetTextColor(1, 0.82, 0); f.orderNum:Hide()
    f:SetScript("OnEnter", function(self) if self.rawLabel then for _, r in ipairs(rowPool) do if r:IsShown() and r.rawLabel == self.rawLabel then r.linkedHl:Show(); r.linkedHl.anim:Play() break end end end end)
    f:SetScript("OnLeave", function(self) for _, r in ipairs(rowPool) do if r.linkedHl and r.linkedHl.anim and r.linkedHl.anim:IsPlaying() then r.linkedHl.anim:Stop(); r.linkedHl:SetAlpha(0); r.linkedHl:Hide() end end end)
    optRowPool[idx] = f; return f
end

RefreshOptFrame = function()
    if not optFrame or not optFrame:IsVisible() then return end
    if not dragGhost then dragGhost = CreateFrame("Frame", nil, optFrame, "BackdropTemplate"); dragGhost:SetSize(280, 24); dragGhost:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 }); dragGhost:SetBackdropColor(unpack(C.muteBlue)); dragGhost:SetBackdropBorderColor(1, 1, 1, 0.8); dragGhost:SetFrameStrata("TOOLTIP"); dragGhost.lbl = dragGhost:CreateFontString(nil, "OVERLAY", "GameFontNormal"); dragGhost.lbl:SetPoint("LEFT", dragGhost, "LEFT", 26, 0); dragGhost.num = dragGhost:CreateFontString(nil, "OVERLAY", "GameFontNormal"); dragGhost.num:SetPoint("LEFT", dragGhost, "LEFT", 8, 0); dragGhost.num:SetTextColor(1, 0.82, 0); dragGhost:Hide() end
    for _, r in ipairs(optRowPool) do r:Hide(); r:SetScript("OnClick", nil); r:SetScript("OnDragStart", nil); r:SetScript("OnDragStop", nil); r:RegisterForDrag(); r.catBorder:SetBackdropBorderColor(0, 0, 0, 0); r.rawLabel = nil; r.orderNum:Hide(); r:SetAlpha(1.0); if r.sw then r.sw:Hide() end; if r.expandLbl then r.expandLbl:Hide() end; if r.charUI then r.charUI:Hide() end; if r.hdrUI then r.hdrUI:Hide() end; if r.hintLbl then r.hintLbl:Hide() end end
    
    local curY, rowIdx = -74, 1; local s = DB.settings
    local currentOrder = s.catOrder or {"summary", "raid", "mythicplus", "weekly", "delves", "currency"}
    local hasSummary = false; for _, v in ipairs(currentOrder) do if v == "summary" then hasSummary = true break end end
    if not hasSummary then table.insert(currentOrder, 1, "summary") end
    local movableOrder = {}; for _, v in ipairs(currentOrder) do if v ~= "char" and v ~= "gold" then table.insert(movableOrder, v) end end; s.catOrder = movableOrder
    
    local FINAL_LAYOUT = {}
    if isLayoutMode then
        for _, l in ipairs(OPT_LAYOUT) do if l.fixed and l.type == "cat" then table.insert(FINAL_LAYOUT, l) end end
        for _, id in ipairs(movableOrder) do for _, l in ipairs(OPT_LAYOUT) do if l.type == "cat" and l.id == id then table.insert(FINAL_LAYOUT, l) break end end end
    else
        for _, l in ipairs(OPT_LAYOUT) do if l.fixed then table.insert(FINAL_LAYOUT, l) end end
        for _, id in ipairs(movableOrder) do for _, l in ipairs(OPT_LAYOUT) do if l.id == id or l.cat == id then table.insert(FINAL_LAYOUT, l) end end end
    end

    local movableIndex = 1
    for _, layout in ipairs(FINAL_LAYOUT) do
        if layout.type == "cat" then
            local f = GetOrCreateOptRow(rowIdx); f.rawLabel = layout.label; f.isFixed = layout.fixed; f:SetPoint("TOPLEFT", optFrame.body, "TOPLEFT", 10, curY); f:SetSize(280, 24); f.bg:SetColorTexture(0.12, 0.12, 0.14, 1.0); f.catBorder:SetBackdropBorderColor(0.25, 0.25, 0.28, 1.0)
            
            local hasChildren = false; if layout.id == "char" then hasChildren = true else for _, sub in ipairs(OPT_LAYOUT) do if sub.cat == layout.id then hasChildren = true; break end end end
            if not f.expandLbl then f.expandLbl = f:CreateFontString(nil, "OVERLAY", "GameFontNormal"); f.expandLbl:SetPoint("LEFT", f, "LEFT", 8, 0) end
            local isExpanded = not isLayoutMode and DB.optExpanded and DB.optExpanded[layout.id] == true; 
            if hasChildren then f.expandLbl:SetText(isExpanded and "-" or "+"); f.expandLbl:Show() else f.expandLbl:Hide() end
            f.lbl:SetPoint("LEFT", f, "LEFT", 26, 0); f.lbl:SetText(layout.label)
            
            if isLayoutMode then
                f.expandLbl:Hide()
                if layout.fixed then
                    f.bg:SetColorTexture(0.04, 0.04, 0.04, 0.8); f.lbl:SetTextColor(0.4, 0.4, 0.4); f:EnableMouse(false)
                else
                    f.bg:SetColorTexture(unpack(C.muteBlue)); f:EnableMouse(true); f:RegisterForDrag("LeftButton"); f.catID = layout.id; f.orderNum:SetText(movableIndex); f.orderNum:Show(); movableIndex = movableIndex + 1; f.lbl:SetTextColor(1, 1, 1)
                    if isDraggingCat == layout.id then f:SetAlpha(0.0) else f:SetAlpha(1.0) end
                    f:SetScript("OnDragStart", function() isDraggingCat = layout.id; f:SetAlpha(0.0); dragGhost.lbl:SetText(layout.label); dragGhost.num:SetText(f.orderNum:GetText()); dragGhost:Show(); optFrame:SetScript("OnUpdate", function() local scale = optFrame:GetEffectiveScale(); local x, y = GetCursorPosition(); dragGhost:ClearAllPoints(); dragGhost:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x/scale, y/scale); local _, gy = dragGhost:GetCenter(); local swapped = false
                        for i, row in ipairs(optRowPool) do if row:IsShown() and row.catID and row.catID ~= isDraggingCat then local _, ry = row:GetCenter(); if math.abs(gy - ry) < 12 then local oldI, newI; for idx, v in ipairs(movableOrder) do if v == isDraggingCat then oldI = idx end; if v == row.catID then newI = idx end end; if oldI and newI then table.remove(movableOrder, oldI); table.insert(movableOrder, newI, isDraggingCat); s.catOrder = movableOrder; swapped = true; break end end end end if swapped then local cy = -74; for _, r in ipairs(optRowPool) do if r:IsShown() and r.isFixed then r:SetPoint("TOPLEFT", optFrame.body, "TOPLEFT", 10, cy) cy = cy - 24 end end; local mIdx = 1; for _, id in ipairs(movableOrder) do for _, r in ipairs(optRowPool) do if r:IsShown() and r.catID == id then r:SetPoint("TOPLEFT", optFrame.body, "TOPLEFT", 10, cy); r.orderNum:SetText(mIdx); if isDraggingCat == id then dragGhost.num:SetText(mIdx) end cy = cy - 24; mIdx = mIdx + 1; break end end end RefreshMainFrame() end end) end)
                    f:SetScript("OnDragStop", function() optFrame:SetScript("OnUpdate", nil); isDraggingCat = nil; dragGhost:Hide(); f:SetAlpha(1.0); RefreshOptFrame(); RefreshMainFrame() end)
                end
            else
                f:EnableMouse(true); 
                if hasChildren then
                    f:SetScript("OnClick", function() if activeEditBox then activeEditBox:ClearFocus() end; if not DB.optExpanded then DB.optExpanded = {} end; DB.optExpanded[layout.id] = not DB.optExpanded[layout.id]; RefreshOptFrame() end)
                    if not f.hintLbl then f.hintLbl = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); f.hintLbl:SetText("[点击进行详细设置]"); f.hintLbl:SetTextColor(0.4, 0.4, 0.4, 1.0) end; f.hintLbl:ClearAllPoints(); f.hintLbl:SetPoint("RIGHT", f, "RIGHT", -60, 0); f.hintLbl:Show()
                else
                    f:SetScript("OnClick", nil)
                    if f.hintLbl then f.hintLbl:Hide() end
                end
                
                local isOn = true; if not layout.noMaster then if not f.sw then f.sw = CreateToggleSwitch(f); f.sw:SetPoint("RIGHT", f, "RIGHT", -10, 0) end; if layout.invert then isOn = not DB.settings[layout.db] else isOn = (DB.settings[layout.db] == true) end; f.sw:SetState(isOn); f.sw.OnToggle = function(self, state) if layout.invert then DB.settings[layout.db] = not state else DB.settings[layout.db] = state end; for _, sub in ipairs(OPT_LAYOUT) do if sub.cat == layout.id and sub.db then if sub.invert then DB.settings[sub.db] = not state else DB.settings[sub.db] = state end end end; if layout.id == "currency" then if not DB.settings.crestEnabled then DB.settings.crestEnabled = {} end; for _, cr in ipairs(CREST_LIST) do DB.settings.crestEnabled[cr.key] = state end; DB.settings.hideVoidcore = not state; DB.settings.hideAscShard = not state; DB.settings.hideAscBox = not state; DB.settings.hideCap = not state end; RefreshMainFrame(); RefreshOptFrame() end; f.sw:Show() else if f.sw then f.sw:Hide() end end
                f.lbl:SetTextColor(isOn and C.txtHeader[1] or 0.3, isOn and C.txtHeader[2] or 0.3, isOn and C.txtHeader[3] or 0.3); if hasChildren then f.expandLbl:SetTextColor(isOn and C.txtHeader[1] or 0.3, isOn and C.txtHeader[2] or 0.3, isOn and C.txtHeader[3] or 0.3) end
            end
            f:Show(); curY = curY - 24; rowIdx = rowIdx + 1
            if isExpanded and hasChildren then
                if layout.id == "char" then
                    local allChars = {}; for key, data in pairs(DB.chars) do table.insert(allChars, { key=key, name=data.name, ilvl=data.ilvl, hidden=data.hidden, sortOrder=data.sortOrder, classFile=data.classFile }) end; table.sort(allChars, SortChars); local myKey = GetCharKey()
                    if #allChars > 0 then local hdr = GetOrCreateOptRow(rowIdx); hdr:SetPoint("TOPLEFT", optFrame.body, "TOPLEFT", 10, curY); hdr:SetSize(280, 16); hdr.bg:SetColorTexture(0,0,0,0); hdr.catBorder:SetBackdropBorderColor(0,0,0,0); hdr.lbl:SetText(""); if hdr.expandLbl then hdr.expandLbl:Hide() end; if hdr.sw then hdr.sw:Hide() end; if not hdr.hdrUI then local ui = CreateFrame("Frame", nil, hdr); ui:SetAllPoints(); ui.ilvlLbl = ui:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); ui.ilvlLbl:SetText("装等"); ui.ilvlLbl:SetTextColor(0.6, 0.6, 0.6); ui.ilvlLbl:SetJustifyH("LEFT"); ui.ilvlLbl:SetPoint("LEFT", ui, "LEFT", 130, 0); ui.orderLbl = ui:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); ui.orderLbl:SetText("顺序"); ui.orderLbl:SetTextColor(0.6, 0.6, 0.6); ui.orderLbl:SetJustifyH("CENTER"); ui.orderLbl:SetPoint("CENTER", ui, "LEFT", 177, 0); hdr.hdrUI = ui end; hdr.hdrUI:Show(); hdr:Show(); curY = curY - 16; rowIdx = rowIdx + 1 end
                    for i, entry in ipairs(allChars) do local cr = GetOrCreateOptRow(rowIdx); cr:SetPoint("TOPLEFT", optFrame.body, "TOPLEFT", 10, curY); cr:SetSize(280, 24); cr.bg:SetColorTexture(0,0,0,0)
                        if not cr.charUI then local ui = CreateFrame("Frame", nil, cr); ui:SetAllPoints(); ui.cl = ui:CreateTexture(nil, "OVERLAY"); ui.cl:SetWidth(2); ui.cl:SetPoint("TOPLEFT", ui, "TOPLEFT", 20, -4); ui.cl:SetPoint("BOTTOMLEFT", ui, "BOTTOMLEFT", 20, 4); ui.name = ui:CreateFontString(nil, "OVERLAY", "GameFontNormal"); ui.name:SetPoint("LEFT", ui.cl, "RIGHT", 6, 0); ui.name:SetWidth(100); ui.name:SetJustifyH("LEFT"); ui.ilvl = ui:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); ui.ilvl:SetPoint("LEFT", ui.name, "RIGHT", 2, 0); ui.ilvl:SetWidth(30); ui.ilvl:SetJustifyH("LEFT"); ui.eb = CreateFrame("EditBox", nil, ui, "BackdropTemplate"); ui.eb:SetSize(24, 18); ui.eb:SetAutoFocus(false); ui.eb:SetNumeric(true); ui.eb:SetMaxLetters(3); ui.eb:SetFontObject("GameFontNormalSmall"); ui.eb:SetJustifyH("CENTER"); ui.eb:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8x8", edgeFile="Interface\\Buttons\\WHITE8x8", edgeSize=1}); ui.eb:SetBackdropColor(unpack(C.bg)); ui.eb:SetBackdropBorderColor(unpack(C.border)); ui.eb:SetPoint("LEFT", ui.ilvl, "RIGHT", 5, 0); ui.sw = CreateToggleSwitch(ui); ui.sw:SetPoint("LEFT", ui.eb, "RIGHT", 15, 0); ui.del = CreateFrame("Button", nil, ui, "BackdropTemplate"); ui.del:SetSize(18, 18); ui.del:SetPoint("RIGHT", ui, "RIGHT", -10, 0); ui.del:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8x8"}); ui.del:SetBackdropColor(0.25, 0.05, 0.05, 0.9); local dTxt = ui.del:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); dTxt:SetAllPoints(); dTxt:SetText("|cffff4444X|r"); cr.charUI = ui; ui.eb:SetScript("OnEditFocusGained", function(self) activeEditBox = self; self:HighlightText() end); ui.eb:SetScript("OnMouseUp", function(self) self:HighlightText() end); ui.eb:SetScript("OnEscapePressed", function(self) self:ClearFocus() end); ui.eb:SetScript("OnEnterPressed", function(self) self:ClearFocus() end); ui.eb:SetScript("OnEnter", function() cr:LockHighlight() end); ui.eb:SetScript("OnLeave", function() cr:UnlockHighlight() end); ui.del:SetScript("OnEnter", function() cr:LockHighlight(); ui.del:SetBackdropColor(0.8, 0.2, 0.2, 0.9) end); ui.del:SetScript("OnLeave", function() cr:UnlockHighlight(); ui.del:SetBackdropColor(0.25, 0.05, 0.05, 0.9) end) end
                        cr.lbl:SetText(""); if cr.expandLbl then cr.expandLbl:Hide() end; if cr.sw then cr.sw:Hide() end; local isCharOn = not entry.hidden; local hex = GetClassColorHex(entry.classFile); local r, g, b = GetClassColor(entry.classFile); cr.charUI.cl:SetColorTexture(isCharOn and r or 0.3, isCharOn and g or 0.3, isCharOn and b or 0.3, 1); local txtColorStr = isCharOn and ("|cff"..hex) or "|cff4d4d4d"; cr.charUI.name:SetText((entry.key == myKey) and (txtColorStr..entry.name.."|r |cff555555[当前]|r") or (txtColorStr..entry.name.."|r")); cr.charUI.ilvl:SetText(isCharOn and "|cffffffff"..math.floor(entry.ilvl).."|r" or "|cff4d4d4d"..math.floor(entry.ilvl).."|r"); cr.charUI.eb.charKey = entry.key; if not cr.charUI.eb:HasFocus() then cr.charUI.eb:SetText(entry.sortOrder and tostring(entry.sortOrder) or ""); cr.charUI.eb:SetCursorPosition(0) end
                        cr.charUI.eb:SetScript("OnEditFocusLost", function(self) self:HighlightText(0,0); if activeEditBox == self then activeEditBox = nil end; local val = tonumber(self:GetText()); local oldVal = DB.chars[self.charKey].sortOrder; if val ~= oldVal then RebuildSortOrders(self.charKey, val); RefreshMainFrame(); C_Timer.After(0.01, RefreshOptFrame) else self:SetText(oldVal and tostring(oldVal) or ""); self:SetCursorPosition(0) end end); cr.charUI.sw:SetState(isCharOn); cr.charUI.sw.OnToggle = function(self, state) DB.chars[entry.key].hidden = not state; RefreshMainFrame(); RefreshOptFrame() end; cr.charUI.del:SetScript("OnClick", function() if activeEditBox then activeEditBox:ClearFocus() end DB.chars[entry.key] = nil; RefreshMainFrame(); RefreshOptFrame() end); cr.charUI:Show(); cr:Show(); curY = curY - 24; rowIdx = rowIdx + 1
                    end
                else
                    for _, sub in ipairs(OPT_LAYOUT) do
                        if sub.type == "toggle" and sub.cat == layout.id then local sr = GetOrCreateOptRow(rowIdx); 
                            if sub.cat == "raid" then sr.rawLabel = nil else sr.rawLabel = sub.label end
                            sr:SetPoint("TOPLEFT", optFrame.body, "TOPLEFT", 10, curY); sr:SetSize(280, 24); sr.bg:SetColorTexture(0,0,0,0); if sr.expandLbl then sr.expandLbl:Hide() end; if sr.charUI then sr.charUI:Hide() end; local isOn; if sub.invert then isOn = not DB.settings[sub.db] else isOn = (DB.settings[sub.db] == true) end; sr.lbl:SetPoint("LEFT", sr, "LEFT", 30, 0); sr.lbl:SetText(sub.label); sr.lbl:SetTextColor(isOn and 1.0 or 0.3, isOn and 1.0 or 0.3, isOn and 1.0 or 0.3); if not sr.sw then sr.sw = CreateToggleSwitch(sr); sr.sw:SetPoint("RIGHT", sr, "RIGHT", -10, 0) end; sr.sw:SetState(isOn); sr.sw.OnToggle = function(self, state) if sub.invert then DB.settings[sub.db] = not state else DB.settings[sub.db] = state end; RefreshMainFrame(); RefreshOptFrame() end; sr.sw:Show(); sr:Show(); curY = curY - 24; rowIdx = rowIdx + 1 end
                    end
                end
            end
        end
    end
    optFrame:SetHeight(math.abs(curY) + 20)
end

-- ============================================================
--  BuildMainFrame
-- ============================================================
local function UpdatePosition(f) if not DB or not DB.settings then return end; local offsetX = DB.settings.offsetX; local offsetY = DB.settings.offsetY; local s = DB.settings.uiScale or 1.0; f:SetScale(s); if optFrame then optFrame:SetScale(1.0); optFrame:ClearAllPoints(); optFrame:SetPoint("TOPRIGHT", f, "TOPLEFT", -5, 0) end; f:ClearAllPoints(); if offsetX and offsetY then f:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", offsetX, offsetY) else f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) end end

local function BuildMainFrame()
    local f = CreateFrame("Frame", "Sey_WeeklyFrame", UIParent, "BackdropTemplate"); f:SetFrameStrata("HIGH"); f:SetMovable(true); f:EnableMouse(true); f:SetScript("OnMouseDown", function(self, button) if activeEditBox then activeEditBox:ClearFocus() end if button == "LeftButton" then self:StartMoving() end end); f:SetScript("OnMouseUp", function(self, button) if button == "LeftButton" then self:StopMovingOrSizing(); if DB and DB.settings then DB.settings.offsetX = self:GetLeft(); DB.settings.offsetY = self:GetTop() end UpdatePosition(self) end end); f:SetClampedToScreen(true); f:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8" }); f:SetBackdropColor(unpack(C.bg))
    local titleBar = CreateFrame("Frame", nil, f); titleBar:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0); titleBar:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0); titleBar:SetHeight(24); local titleBg = titleBar:CreateTexture(nil, "BACKGROUND"); titleBg:SetColorTexture(unpack(C.titleBg)); titleBg:SetAllPoints(); local titleTxt = titleBar:CreateFontString(nil, "OVERLAY"); local success = pcall(function() titleTxt:SetFont(STANDARD_TEXT_FONT, 16, "OUTLINE") end); if not success then titleTxt:SetFont(select(1, GameFontNormal:GetFont()), 16, "OUTLINE") end titleTxt:SetText("[周常]账号信息"); titleTxt:SetTextColor(0.92, 0.92, 0.95); titleTxt:SetPoint("CENTER", titleBar, "CENTER", 8, 0); local titleIcon = titleBar:CreateTexture(nil, "ARTWORK"); titleIcon:SetTexture(ICON_PATH); titleIcon:SetSize(16, 16); titleIcon:SetPoint("RIGHT", titleTxt, "LEFT", -6, 0)

    optFrame = CreateFrame("Frame", "Sey_WeeklyOptFrame", UIParent, "BackdropTemplate"); optFrame:SetFrameStrata("HIGH"); optFrame:SetWidth(300); optFrame:SetHeight(400); optFrame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8" }); optFrame:SetBackdropColor(unpack(C.bg)); optFrame.body = CreateFrame("Frame", nil, optFrame); optFrame.body:SetAllPoints(); optFrame:SetScript("OnMouseDown", function() if activeEditBox then activeEditBox:ClearFocus() end end); optFrame:Hide()
    local scaleContainer = CreateFrame("Frame", nil, optFrame.body, "BackdropTemplate"); scaleContainer:SetPoint("TOPLEFT", optFrame.body, "TOPLEFT", 10, -10); scaleContainer:SetSize(280, 24); local scBg = scaleContainer:CreateTexture(nil, "BACKGROUND"); scBg:SetAllPoints(); scBg:SetColorTexture(0.12, 0.12, 0.14, 1.0); scaleContainer:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 }); scaleContainer:SetBackdropBorderColor(0.25, 0.25, 0.28, 1.0)
    local scaleContent = CreateFrame("Frame", nil, scaleContainer); scaleContent:SetSize(260, 24); scaleContent:SetPoint("CENTER", scaleContainer, "CENTER", 0, 0); local sLbl1 = scaleContent:CreateFontString(nil, "OVERLAY", "GameFontNormal"); sLbl1:SetText("UI 大小"); sLbl1:SetTextColor(unpack(C.txtNormal)); sLbl1:SetPoint("LEFT", scaleContent, "LEFT", 15, 0); local slider = CreateFrame("Slider", "SeyWeeklyScaleSlider", scaleContent); slider:SetSize(160, 12); slider:SetPoint("RIGHT", scaleContent, "RIGHT", -15, 0); slider:SetMinMaxValues(0.7, 1.5); slider:SetValueStep(0.01); slider:SetObeyStepOnDrag(false); slider:EnableMouse(true); slider:SetOrientation("HORIZONTAL"); local track = slider:CreateTexture(nil, "BACKGROUND"); track:SetPoint("LEFT", slider, "LEFT", 0, 0); track:SetPoint("RIGHT", slider, "RIGHT", 0, 0); track:SetHeight(4); track:SetColorTexture(0.05, 0.05, 0.06, 1); local fill = slider:CreateTexture(nil, "BORDER"); fill:SetPoint("LEFT", track, "LEFT", 0, 0); fill:SetHeight(4); fill:SetColorTexture(0.35, 0.50, 0.65, 1); local thumb = slider:CreateTexture(nil, "ARTWORK"); thumb:SetSize(8, 14); thumb:SetColorTexture(0.85, 0.85, 0.90, 1); slider:SetThumbTexture(thumb)
    slider:SetScript("OnValueChanged", function(self, val) local min, max = self:GetMinMaxValues(); local pct = (val - min) / (max - min); fill:SetWidth(math.max(0.1, pct * self:GetWidth())); if activeEditBox then activeEditBox:ClearFocus() end if not DB or not DB.settings then return end local current = DB.settings.uiScale or 1.0; if math.abs(current - val) > 0.005 then if DB.settings.offsetX and DB.settings.offsetY then local screenLeft = DB.settings.offsetX * current; local screenTop = DB.settings.offsetY * current; DB.settings.offsetX = screenLeft / val; DB.settings.offsetY = screenTop / val end DB.settings.uiScale = val; UpdatePosition(f) end end)

    local btnLayout = CreateFrame("Button", nil, optFrame.body, "BackdropTemplate"); btnLayout:SetSize(120, 24); btnLayout:SetPoint("TOP", optFrame.body, "TOP", 0, -42); btnLayout:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 }); btnLayout:SetBackdropColor(unpack(C.sepBtn)); btnLayout:SetBackdropBorderColor(0.25, 0.25, 0.28, 1.0)
    local lTxt = btnLayout:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); lTxt:SetPoint("CENTER", btnLayout, "CENTER", 0, 0); lTxt:SetText("调整排列顺序"); lTxt:SetTextColor(0.65, 0.80, 0.95); btnLayout:SetScript("OnEnter", function() btnLayout:SetBackdropColor(0.35, 0.50, 0.65, 0.9); lTxt:SetTextColor(1, 1, 1) end); btnLayout:SetScript("OnLeave", function() btnLayout:SetBackdropColor(unpack(C.sepBtn)); lTxt:SetTextColor(0.65, 0.80, 0.95) end)
    statusTxt = optFrame.body:CreateFontString(nil, "OVERLAY", "GameFontNormal"); statusTxt:SetPoint("LEFT", btnLayout, "RIGHT", 10, 0); statusTxt:SetText("|cff4dff4dLOCK|r")
    btnLayout:SetScript("OnClick", function() isLayoutMode = not isLayoutMode; if isLayoutMode then DB.optExpanded = {}; statusTxt:SetText("|cffff4d4dUNLOCK|r") else statusTxt:SetText("|cff4dff4dLOCK|r") end; RefreshOptFrame() end)

    local btnOpt = CreateFrame("Button", nil, f); btnOpt:SetSize(62, 24); btnOpt:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0); btnOpt:SetFrameLevel(f:GetFrameLevel() + 20); local optBg = btnOpt:CreateTexture(nil, "BACKGROUND"); optBg:SetColorTexture(0.65, 0.15, 0.15, 0.9); optBg:SetAllPoints(); local oHl = btnOpt:CreateTexture(nil, "ARTWORK"); oHl:SetColorTexture(unpack(C.sepHighlight)); oHl:SetHeight(1); oHl:SetPoint("TOPLEFT"); oHl:SetPoint("TOPRIGHT"); local oSh = btnOpt:CreateTexture(nil, "ARTWORK"); oSh:SetColorTexture(unpack(C.sepShadow)); oSh:SetHeight(1); oSh:SetPoint("BOTTOMLEFT"); oSh:SetPoint("BOTTOMRIGHT"); local optTex = btnOpt:CreateTexture(nil, "ARTWORK"); optTex:SetSize(16, 16); optTex:SetPoint("LEFT", btnOpt, "LEFT", 8, 0); optTex:SetTexture("Interface\\Buttons\\UI-OptionsButton"); optTex:SetDesaturated(true); local optTxt = btnOpt:CreateFontString(nil, "OVERLAY", "GameFontNormal"); optTxt:SetText("设置"); optTxt:SetTextColor(0.95, 0.95, 0.95); optTxt:SetPoint("LEFT", optTex, "RIGHT", 4, 0)
    btnOpt:SetScript("OnEnter", function() optBg:SetColorTexture(0.95, 0.3, 0.3, 0.9); optTxt:SetTextColor(1, 1, 1) end); btnOpt:SetScript("OnLeave", function() optBg:SetColorTexture(0.65, 0.15, 0.15, 0.9); optTxt:SetTextColor(0.95, 0.95, 0.95) end); btnOpt:SetScript("OnClick", function() if activeEditBox then activeEditBox:ClearFocus() end; DB.settings.optOpen = not DB.settings.optOpen; if DB.settings.optOpen then optFrame:Show(); RefreshOptFrame() else optFrame:Hide() end end)
    
    -- 最近副本全局按钮
    local btnRecent = CreateFrame("Button", nil, f)
    btnRecent:SetSize(90, 24)
    btnRecent:SetPoint("LEFT", btnOpt, "RIGHT", 4, 0)
    btnRecent:SetFrameLevel(f:GetFrameLevel() + 20)
    
    local rBg = btnRecent:CreateTexture(nil, "BACKGROUND")
    rBg:SetColorTexture(unpack(C.sepBtn))
    rBg:SetAllPoints()
    local rHl = btnRecent:CreateTexture(nil, "ARTWORK"); rHl:SetColorTexture(unpack(C.sepHighlight)); rHl:SetHeight(1); rHl:SetPoint("TOPLEFT"); rHl:SetPoint("TOPRIGHT")
    local rSh = btnRecent:CreateTexture(nil, "ARTWORK"); rSh:SetColorTexture(unpack(C.sepShadow)); rSh:SetHeight(1); rSh:SetPoint("BOTTOMLEFT"); rSh:SetPoint("BOTTOMRIGHT")
    
    local rTxt = btnRecent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    rTxt:SetPoint("CENTER", btnRecent, "CENTER", 0, 0)
    rTxt:SetText("最近副本 : 0")
    rTxt:SetTextColor(0.95, 0.95, 0.95)
    
    btnRecent.Update = function()
        local count, _ = GetInstanceHistory()
        rTxt:SetText("最近副本 : " .. count)
        if count >= 10 then
            rTxt:SetTextColor(1, 0.2, 0.2)
        elseif count >= 8 then
            rTxt:SetTextColor(1, 0.6, 0.2)
        else
            rTxt:SetTextColor(0.95, 0.95, 0.95)
        end
    end
    f.btnRecent = btnRecent
    
    btnRecent:SetScript("OnEnter", function(self)
        rBg:SetColorTexture(0.3, 0.4, 0.5, 0.9)
        rTxt:SetTextColor(1, 1, 1)
        
        local count, list = GetInstanceHistory()
        GameTooltip:SetOwner(self, "ANCHOR_BOTTOM", 0, -5)
        GameTooltip:ClearLines()
        GameTooltip:AddLine("最近副本 (账号共享)", 1, 0.82, 0)
        GameTooltip:AddLine("每个账号1小时内进入限制(10次)所包含的副本\n以及距离到期的剩余时间。", 0.8, 0.8, 0.8, true)
        if count > 0 then
            GameTooltip:AddLine(" ")
            for _, v in ipairs(list) do
                local timeLeft = 3600 - (time() - v.time)
                local timeStr = SecondsToTime(timeLeft, false, false, 2)
                GameTooltip:AddDoubleLine(v.name .. " [" .. (v.diff or "") .. "]", timeStr, 1, 1, 1, 0.8, 0.8, 0.8)
            end
        else
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine("最近1小时内没有进入记录。", 0.5, 0.5, 0.5)
        end
        GameTooltip:Show()
    end)
    btnRecent:SetScript("OnLeave", function(self)
        rBg:SetColorTexture(unpack(C.sepBtn))
        btnRecent:Update()
        GameTooltip:Hide()
    end)

    local close = CreateFrame("Button", nil, f); close:SetSize(24, 24); close:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0); close:SetFrameLevel(f:GetFrameLevel() + 20); local closeBg = close:CreateTexture(nil, "BACKGROUND"); closeBg:SetColorTexture(unpack(C.sepBtn)); closeBg:SetAllPoints(); local cHl = close:CreateTexture(nil, "ARTWORK"); cHl:SetColorTexture(unpack(C.sepHighlight)); cHl:SetHeight(1); cHl:SetPoint("TOPLEFT"); cHl:SetPoint("TOPRIGHT"); local cSh = close:CreateTexture(nil, "ARTWORK"); cSh:SetColorTexture(unpack(C.sepShadow)); cSh:SetHeight(1); cSh:SetPoint("BOTTOMLEFT"); cSh:SetPoint("BOTTOMRIGHT"); local closeTxt = close:CreateFontString(nil, "OVERLAY", "GameFontNormal"); closeTxt:SetText("X"); closeTxt:SetTextColor(0.7, 0.7, 0.7); closeTxt:SetPoint("CENTER", close, "CENTER", 0, 0)
    close:SetScript("OnEnter", function() closeBg:SetColorTexture(0.8, 0.2, 0.2, 0.9); closeTxt:SetTextColor(1, 1, 1) end); close:SetScript("OnLeave", function() closeBg:SetColorTexture(unpack(C.sepBtn)); closeTxt:SetTextColor(0.7, 0.7, 0.7) end); close:SetScript("OnClick", function() if activeEditBox then activeEditBox:ClearFocus() end f:Hide(); f.isLocked = false end)
    local body = CreateFrame("Frame", nil, f); body:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0); f.body = body

    -- ============================================================
    --[[  修补程序节点按钮和动画框架（主窗口左下角）
    -- ============================================================
    local btnPatch = CreateFrame("Button", nil, f, "BackdropTemplate")
    btnPatch:SetSize(60, 24)
    btnPatch:SetPoint("TOPLEFT", f, "BOTTOMLEFT", 0, -5)
    btnPatch:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 })
    btnPatch:SetBackdropColor(unpack(C.sepBtn))
    btnPatch:SetBackdropBorderColor(0.25, 0.25, 0.28, 1.0)
    
    local btnPatchTxt = btnPatch:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    btnPatchTxt:SetPoint("CENTER", btnPatch, "CENTER", 0, 0)
    btnPatchTxt:SetText("补丁说明")
    btnPatchTxt:SetTextColor(0.8, 0.8, 0.8)
    
    btnPatch:SetScript("OnEnter", function() btnPatch:SetBackdropColor(0.3, 0.4, 0.5, 0.9); btnPatchTxt:SetTextColor(1, 1, 1) end)
    btnPatch:SetScript("OnLeave", function() btnPatch:SetBackdropColor(unpack(C.sepBtn)); btnPatchTxt:SetTextColor(0.8, 0.8, 0.8) end)
    
    local patchFrame = CreateFrame("Frame", nil, f, "BackdropTemplate")
    patchFrame:SetPoint("LEFT", btnPatch, "RIGHT", 5, 0)
    patchFrame:SetHeight(60)
    patchFrame:SetWidth(1) -- 시작 시 최소 너비
    patchFrame:SetClipsChildren(true) -- 핵심: 너비가 작을 때 글자가 삐져나오지 않고 종이처럼 잘리게 함
    patchFrame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 })
    patchFrame:SetBackdropColor(unpack(C.bg))
    patchFrame:SetBackdropBorderColor(0.25, 0.25, 0.28, 1.0)
    patchFrame:Hide()
    
    local pfTitle = patchFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    pfTitle:SetPoint("TOPLEFT", patchFrame, "TOPLEFT", 10, -10)
    pfTitle:SetText("|cffF58CBASey Weekly 更新|r")
    
    local pfContent = patchFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    pfContent:SetPoint("TOPLEFT", pfTitle, "BOTTOMLEFT", 0, -8)
    pfContent:SetJustifyH("LEFT")
    pfContent:SetText("26.07.06\n· 添加顿顿碎片")
    pfContent:SetTextColor(0.9, 0.9, 0.9)
    
    local patchTargetWidth = 220
    local isPatchExpanding = false
    
    local function UpdatePatchFrameAnim(self, elapsed)
        local cur = self:GetWidth()
        if isPatchExpanding then
            local diff = patchTargetWidth - cur
            if diff > 1 then
                self:SetWidth(cur + diff * 12 * elapsed)
            else
                self:SetWidth(patchTargetWidth)
                self:SetScript("OnUpdate", nil)
            end
        else
            local diff = cur - 1
            if diff > 1 then
                self:SetWidth(cur - diff * 15 * elapsed)
            else
                self:SetWidth(1)
                self:Hide()
                self:SetScript("OnUpdate", nil)
            end
        end
    end
    
    btnPatch:SetScript("OnClick", function()
        if activeEditBox then activeEditBox:ClearFocus() end
        isPatchExpanding = not isPatchExpanding
        if isPatchExpanding then
            patchFrame:Show()
        end
        patchFrame:SetScript("OnUpdate", UpdatePatchFrameAnim)
    end)
    -- ============================================================]]

    f:SetScript("OnShow", function() if DB and DB.settings then local initScale = DB.settings.uiScale or 1.0; if SeyWeeklyScaleSlider then SeyWeeklyScaleSlider:SetValue(initScale) end if not DB.settings.offsetX then DB.settings.offsetX = nil; DB.settings.offsetY = nil end UpdatePosition(f); if DB.settings.optOpen then optFrame:Show(); RefreshOptFrame() end end RefreshMainFrame() end); f:SetScript("OnHide", function(self) self.isLocked = false if optFrame then optFrame:Hide() end end); f:Hide()
    f:SetScript("OnEnter", function(self) self.isMouseOver = true end); f:SetScript("OnLeave", function(self) self.isMouseOver = false; if not self.isLocked and not isOverMinimapBtn then self:Hide() end end)
    return f
end

-- ============================================================
--  小地图及扫描逻辑
-- ============================================================
local function SetupMinimapButton()
    local LDB = LibStub and LibStub("LibDataBroker-1.1", true); local DBIcon = LibStub and LibStub("LibDBIcon-1.0", true)
    if LDB and DBIcon then
        local ldbObj = LDB:NewDataObject("Sey_Weekly", { 
            type="launcher", icon=ICON_PATH, label="Sey Weekly", 
            OnClick = function(_, button) if button == "RightButton" then OpenWeeklyRewards() else if mainFrame then mainFrame.isLocked = not mainFrame.isLocked if mainFrame.isLocked then mainFrame:Show() else mainFrame:Hide() end end end end, 
            OnEnter = function(anchor) isOverMinimapBtn = true; if mainFrame then mainFrame:Show() end GameTooltip:SetOwner(anchor, "ANCHOR_LEFT"); GameTooltip:AddLine("Sey Weekly", 1, 0.82, 0); GameTooltip:AddLine("左键点击: 固定/取消固定窗口", 0.8, 0.8, 0.8); GameTooltip:AddLine("右键点击: 宏伟宝库", 0.8, 0.8, 0.8); GameTooltip:Show() end, 
            OnLeave = function() isOverMinimapBtn = false; GameTooltip:Hide() if mainFrame and not mainFrame.isLocked and not mainFrame.isMouseOver then mainFrame:Hide() end end 
        })
        DBIcon:Register("Sey_Weekly", ldbObj, DB)
    end
end

PurgeExpiredData = function()
    local lastWeeklyReset = GetLastWeeklyReset()
    local purgedAny = false
    for charKey, data in pairs(DB.chars) do 
        local needPurge = false; 
        if not data.lastUpdate or (data.lastUpdate < lastWeeklyReset) then needPurge = true end
        if needPurge then 
            purgedAny = true
            local earned = false; 
            if data.mythicplus and data.mythicplus.runCount and data.mythicplus.runCount >= 1 then earned = true end; 
            if data.weeklies and data.weeklies.worldCount then local wTh = (data.weeklies.worldList and data.weeklies.worldList[1] and data.weeklies.worldList[1].threshold) or 2; if data.weeklies.worldCount >= wTh then earned = true end end; 
            if data.raids then local k = 0; for _, diffs in pairs(data.raids) do for _, kills in pairs(diffs) do k = k + kills end end; if k >= 2 then earned = true end end; 
            if earned then data.hasUnclaimedVault = true end
            data.raids = { ["voidspire"] = {}, ["dreamrift"] = {}, ["quelndanas"] = {}, ["fungal"] = {} }; data.raidExpires = {}; for k, v in pairs(data) do if type(k) == "string" and k:find("^prev_") then data[k] = nil end end; data.wbKills = {}; data.nightmareCount = 0; data.weeklies = nil; if data.mythicplus then data.mythicplus.runCount = 0; data.mythicplus.runList = {}; data.mythicplus.fullHistory = {}; data.mythicplus.keyLevel = 0; data.mythicplus.keyMapID = 0; data.mythicplus.keyName = nil; data.mythicplus.keyLink = nil else data.mythicplus = { runCount = 0, runList = {}, fullHistory = {}, keyLevel = 0, keyMapID = 0, keyLink = nil } end
            if data.currencies then data.currencies.trovebountyUsed = false; data.currencies.asc_box_last = data.currencies.asc_box or 0 end data.lastUpdate = time() 
        end 
        if data.savedInstances then
            local newList = {}
            for _, inst in ipairs(data.savedInstances) do
                if inst.unlockTime and inst.unlockTime > time() then
                    table.insert(newList, inst)
                end
            end
            data.savedInstances = newList
        end
    end
    return purgedAny
end

local function ScanEquipment() local key = GetCharKey(); if not DB.chars[key] then return end; local char = DB.chars[key]; local setCounts = {}; local maxSetCount = 0; local slots = {1, 3, 5, 7, 10} for _, slot in ipairs(slots) do local link = GetInventoryItemLink("player", slot); if link then local setID = select(16, GetItemInfo(link)); if setID and setID > 0 then setCounts[setID] = (setCounts[setID] or 0) + 1; if setCounts[setID] > maxSetCount then maxSetCount = setCounts[setID] end end end end char.tierSetCount = maxSetCount; char.ilvl = GetCurrentIlvl() end

local function ScanRaidLockouts() 
    local key = GetCharKey(); if not DB.chars[key] then return end; 
    local char = DB.chars[key]; 
    local numSaved = GetNumSavedInstances(); 
    char.raids = { ["voidspire"] = {}, ["dreamrift"] = {}, ["quelndanas"] = {}, ["fungal"] = {} }; 
    char.raidExpires = {}; 
    char.savedInstances = {};
    for k, v in pairs(char) do if type(k) == "string" and k:find("^prev_") then char[k] = nil end end; 
    if numSaved == 0 then return end 
    
    for i = 1, numSaved do 
        local name, id, expires, difficultyID, locked, extended, instanceIDMostSig, isRaid, maxPlayers, difficultyName, numEncounters, encounterProgress = GetSavedInstanceInfo(i); 
        
        if name and (locked or extended or (expires and expires > 0)) then
            if expires and expires > 0 then
                table.insert(char.savedInstances, {
                    name = name,
                    diff = difficultyName or "",
                    unlockTime = time() + expires,
                    progress = encounterProgress or 0,
                    total = numEncounters or 0
                })
            end
        end

        if isRaid and id and id > 0 and expires and expires > 0 then 
            local rk = INSTANCE_TO_RAID_NORM[normName(name)] or INSTANCE_TO_RAID_NORM[name]; 
            local dk = RAID_DIFF_MAP[difficultyID] or "mythic"; 
            
            if rk and dk then 
                local kills, bossIdx = 0, 1; 
                while true do 
                    local bossName, _, isKilled = GetSavedInstanceEncounterInfo(i, bossIdx); 
                    if not bossName then break end 
                    if isKilled then kills = kills + 1 end 
                    bossIdx = bossIdx + 1 
                end 
                if kills > 0 then 
                    char.raids[rk] = char.raids[rk] or {}; 
                    char.raids[rk][dk] = (char.raids[rk][dk] or 0) + kills 
                end 
                char.raidExpires[rk.."_"..dk] = time() + expires 
            end 
        end 
    end 
end

local function ScanCurrencies() local key = GetCharKey(); if not DB.chars[key] then return end; local char = DB.chars[key]; if not char.currencies then char.currencies = {} end; local curr = char.currencies; for _, cd in ipairs(CURRENCY_LIST) do if cd.type == "currency" then local data = C_CurrencyInfo.GetCurrencyInfo(cd.id); if data then curr[cd.key] = data.quantity or 0; curr[cd.key.."_weekMax"] = (data.maxWeeklyQuantity and data.maxWeeklyQuantity > 0) and data.maxWeeklyQuantity or nil; curr[cd.key.."_weekEarn"] = (data.quantityEarnedThisWeek and data.quantityEarnedThisWeek > 0) and data.quantityEarnedThisWeek or nil end end end; if not char.crests then char.crests = {} end for _, cr in ipairs(CREST_LIST) do local data = C_CurrencyInfo.GetCurrencyInfo(cr.id); if data and data.discovered then char.crests[cr.key] = data.quantity or 0; char.crests[cr.key.."_seasonMax"] = (data.maxQuantity and data.maxQuantity > 0) and data.maxQuantity or nil; if data.useTotalEarnedForMaxQty and data.totalEarned and data.totalEarned > 0 then char.crests[cr.key.."_totalEarned"] = data.totalEarned else char.crests[cr.key.."_totalEarned"] = nil end end end local troveCnt = GetItemCount(252415) or 0; curr.trovebounty = troveCnt; local questDone = C_QuestLog.IsQuestFlaggedCompleted(86371); if questDone then curr.trovebountyUsed = (troveCnt == 0) else curr.trovebountyUsed = false end local voidData = C_CurrencyInfo.GetCurrencyInfo(VOIDCORE_ID); if voidData then char.voidcore = { amount = voidData.quantity or 0, seasonMax = (voidData.maxQuantity and voidData.maxQuantity > 0) and voidData.maxQuantity or nil, totalEarned = (voidData.useTotalEarnedForMaxQty and voidData.totalEarned and voidData.totalEarned > 0) and voidData.totalEarned or nil } else char.voidcore = { amount = 0 } end
    curr.asc_shard = GetItemCount(268650, true) or 0
    local curBox = GetItemCount(268552, true) or 0; local oldBox = curr.asc_box_last or curBox; local eBox = curr.asc_box_earned or 0
    if curBox > oldBox then eBox = eBox + (curBox - oldBox) end
    curr.asc_box_last = curBox; curr.asc_box_earned = eBox; curr.asc_box = curBox
end
local function ScanMythicPlus() 
    local key = GetCharKey(); if not DB.chars[key] then return end; local char = DB.chars[key]; if not char.mythicplus then char.mythicplus = {} end; local mp = char.mythicplus; 
    mp.score = C_ChallengeMode and C_ChallengeMode.GetOverallDungeonScore and C_ChallengeMode.GetOverallDungeonScore() or 0; 
    
    local isActive = C_ChallengeMode and C_ChallengeMode.IsChallengeModeActive and C_ChallengeMode.IsChallengeModeActive()
    local hasActiveKey = C_ChallengeMode and C_ChallengeMode.HasActiveKeystoneInfo and C_ChallengeMode.HasActiveKeystoneInfo()
    local _, _, diffID = GetInstanceInfo()
    local isMPlusInstance = (diffID == 8)
    
    if not isActive and not hasActiveKey and not isMPlusInstance then
        local keyLevel = C_MythicPlus.GetOwnedKeystoneLevel and C_MythicPlus.GetOwnedKeystoneLevel() or 0; local keyMapID = C_MythicPlus.GetOwnedKeystoneChallengeModeID and C_MythicPlus.GetOwnedKeystoneChallengeModeID() or 0; local keyName = nil; local keyLink = nil; 
        if keyMapID > 0 and C_ChallengeMode and C_ChallengeMode.GetMapUIInfo then keyName = C_ChallengeMode.GetMapUIInfo(keyMapID) end 
        for bag = 0, 4 do local numSlots = C_Container and C_Container.GetContainerNumSlots and C_Container.GetContainerNumSlots(bag) or 0 for slot = 1, numSlots do local itemID = C_Container.GetContainerItemID(bag, slot); if itemID == 180653 then local link = C_Container.GetContainerItemLink(bag, slot); if link then local mID, lvl = link:match("keystone:%d+:(%d+):(%d+)"); if mID and lvl then keyLink = link; keyMapID = tonumber(mID); keyLevel = tonumber(lvl); if C_ChallengeMode and C_ChallengeMode.GetMapUIInfo then keyName = C_ChallengeMode.GetMapUIInfo(keyMapID) end break end end end end if keyLink then break end end 
        mp.keyLevel = keyLevel; mp.keyMapID = keyMapID; mp.keyName = keyName; mp.keyLink = keyLink; 
    end
    
    local runCount = 0; if C_WeeklyRewards and C_WeeklyRewards.GetActivities then local activities = C_WeeklyRewards.GetActivities(1); if activities then mp.thresholds = {} for _, act in ipairs(activities) do if act.progress and act.progress > runCount then runCount = act.progress end table.insert(mp.thresholds, act.threshold or 0) end table.sort(mp.thresholds) end end if not mp.thresholds or #mp.thresholds == 0 then mp.thresholds = {1, 4, 8} end local history = C_MythicPlus.GetRunHistory(false, true); local historyRuns = {}; if history then for _, run in ipairs(history) do if run.level and run.level > 0 then local name = "?"; if run.mapChallengeModeID then name = C_ChallengeMode.GetMapUIInfo(run.mapChallengeModeID) or "?" end table.insert(historyRuns, {level=run.level, name=name, completed=run.completed}) end end table.sort(historyRuns, function(a, b) if a.level ~= b.level then return a.level > b.level end if a.completed ~= b.completed then return a.completed end return false end) end local newRunList = {}; for _, th in ipairs(mp.thresholds) do if historyRuns[th] then newRunList[th] = historyRuns[th] end end mp.runList = newRunList; mp.fullHistory = historyRuns; mp.runCount = math.max(runCount, #historyRuns) 
end
local function ScanWeeklies() local key = GetCharKey(); if not DB.chars[key] then return end; local char = DB.chars[key]; char.weeklies = char.weeklies or {}; local wasEarnedByMe = select(13, GetAchievementInfo(61798)); char.nullaeus = wasEarnedByMe or false; char.weeklies.nightmarish = C_QuestLog.IsQuestFlaggedCompleted(94446); char.weeklies.stormarion = C_QuestLog.IsQuestFlaggedCompleted(90962); local metaQuests = {93766, 93767, 93769, 93889, 93890, 93891, 93892, 93909, 93910, 93911, 93912, 93913, 94457, 95842, 95843}; char.weeklies.meta = false; for _, qid in ipairs(metaQuests) do if C_QuestLog.IsQuestFlaggedCompleted(qid) then char.weeklies.meta = true break end end char.weeklies.haranir = false; if C_WeeklyRewards and C_WeeklyRewards.GetActivities then local worldActivities = C_WeeklyRewards.GetActivities(Enum.WeeklyRewardChestThresholdType.World); if worldActivities then for _, act in ipairs(worldActivities) do if act.progress and act.progress > 0 then char.weeklies.haranir = true break end end end end local saltherilQuests = {89276, 89277, 89278, 91971, 91972, 91973, 91974, 91975, 91976, 91977, 91978, 91979, 91983, 91984, 91985, 91986, 91987, 91988, 91989, 91990, 91991, 91992, 91993, 91994, 91995, 91996, 91997, 91999, 92000, 92001, 92002, 92003, 92004, 92005, 92006, 92007}; local salCount = 0; for _, qid in ipairs(saltherilQuests) do if C_QuestLog.IsQuestFlaggedCompleted(qid) then salCount = salCount + 1 end end char.weeklies.saltherilCount = salCount; char.weeklies.abundant = C_QuestLog.IsQuestFlaggedCompleted(89507); local worldCount = 0; local worldList = {}; local worldRuns = {}; if C_WeeklyRewards and C_WeeklyRewards.GetActivities then local activities = C_WeeklyRewards.GetActivities(Enum.WeeklyRewardChestThresholdType.World); if activities then local highestIndex = 1; for _, act in ipairs(activities) do if act.progress and act.progress > worldCount then worldCount = act.progress end if act.index and act.index > highestIndex then highestIndex = act.index end table.insert(worldList, { id = act.id, index = act.index, progress = act.progress or 0, threshold = act.threshold or 0, level = act.level or 0 }) end table.sort(worldList, function(a, b) return a.threshold < b.threshold end); if worldCount > 0 then local encounters = C_WeeklyRewards.GetActivityEncounterInfo(Enum.WeeklyRewardChestThresholdType.World, highestIndex); if encounters then local validRuns = math.min(worldCount, #encounters); for i = 1, validRuns do local enc = encounters[i]; if enc then local lvl = enc.bestLevel or enc.level or enc.tier or 0; if lvl == 0 then lvl = 1 end; table.insert(worldRuns, lvl) end end end end end end char.weeklies.worldCount = worldCount; char.weeklies.worldList = worldList; char.weeklies.worldRuns = worldRuns; char.wbKills = char.wbKills or {}; for _, wb in ipairs(WB_LIST) do char.wbKills[wb.key] = C_QuestLog.IsQuestFlaggedCompleted(wb.questID) end local nmCount = 0; for i = 91211, 91269 do if C_QuestLog.IsQuestFlaggedCompleted(i) then nmCount = nmCount + 1 end end char.nightmareCount = nmCount; local hasVault = C_WeeklyRewards and C_WeeklyRewards.HasAvailableRewards and C_WeeklyRewards.HasAvailableRewards(); char.hasUnclaimedVault = hasVault end

-- ============================================================
--  智能扫描逻辑
-- ============================================================
local scanFlags = { raid=true, mythic=true, weekly=true, curr=true, equip=true, gold=true }
local scanTimer = nil
local function DoScans() scanTimer = nil; if InCombatLockdown() then if not eventFrame:IsEventRegistered("PLAYER_REGEN_ENABLED") then eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED") end return end; if not DB then return end; PurgeExpiredData(); if scanFlags.equip then ScanEquipment(); scanFlags.equip = false end; if scanFlags.curr then ScanCurrencies(); scanFlags.curr = false end; if scanFlags.raid then ScanRaidLockouts(); scanFlags.raid = false end; if scanFlags.mythic then ScanMythicPlus(); scanFlags.mythic = false end; if scanFlags.weekly then ScanWeeklies(); scanFlags.weekly = false end; if scanFlags.gold then if C_Bank and C_Bank.FetchDepositedMoney then local wbMoney = C_Bank.FetchDepositedMoney(Enum.BankType.Account); if wbMoney then DB.warbandMoney = wbMoney end end scanFlags.gold = false end SaveCharData(); RefreshMainFrame(); if optFrame and optFrame:IsVisible() then RefreshOptFrame() end end

RequestUpdate = function(flag) if flag then scanFlags[flag] = true else for k in pairs(scanFlags) do scanFlags[k] = true end end if not scanTimer then scanTimer = C_Timer.NewTimer(2.0, DoScans) end end

-- ============================================================
--  持有钥石聊天队伍通知功能 (/dol, /partydol, 密语回复)
-- ============================================================
C_ChatInfo.RegisterAddonMessagePrefix("SeyWeekly")

local function GetPartyChannel()
    return IsInGroup(2) and "INSTANCE_CHAT" or "PARTY"
end

local function AnnounceAllKeystones()
    if not DB or not DB.chars then return end
    local list = {}
    for k, v in pairs(DB.chars) do
        if not v.hidden and v.mythicplus and v.mythicplus.keyLevel and v.mythicplus.keyLevel > 0 and v.mythicplus.keyName then
            table.insert(list, v)
        end
    end
    if #list == 0 then return end
    table.sort(list, SortChars)
    
    local isGroup = IsInGroup()
    local delay = 0
    for _, char in ipairs(list) do
        local shortName = DUNGEON_SHORT_NAMES[char.mythicplus.keyName] or char.mythicplus.keyName
        if isGroup then
            local outMsg = string.format("[%s] %d %s", char.name, char.mythicplus.keyLevel, shortName)
            C_Timer.After(delay, function() SendChatMessage(outMsg, GetPartyChannel()) end)
            delay = delay + 0.05
        else
            local hex = GetClassColorHex(char.classFile)
            local coloredName = string.format("|cff%s%s|r", hex, char.name)
            local outMsg = string.format("[%s] %d %s", coloredName, char.mythicplus.keyLevel, shortName)
            print("|cffF58CBASey Weekly:|r (全部预览) " .. outMsg)
        end
    end
end

local function AnnounceCurrentKeystone()
    if not DB or not DB.chars then return end
    local currentKey = GetCharKey()
    local char = DB.chars[currentKey]
    
    if not char or char.hidden or not char.mythicplus or not char.mythicplus.keyLevel or char.mythicplus.keyLevel <= 0 or not char.mythicplus.keyName then
        if not IsInGroup() then
            print("|cffF58CBASey Weekly:|r 当前登录的角色没有钥石。")
        end
        return 
    end

    local isGroup = IsInGroup()
    local shortName = DUNGEON_SHORT_NAMES[char.mythicplus.keyName] or char.mythicplus.keyName
    
    if isGroup then
        local outMsg = string.format("[%s] %d %s", char.name, char.mythicplus.keyLevel, shortName)
        SendChatMessage(outMsg, GetPartyChannel())
    else
        local hex = GetClassColorHex(char.classFile)
        local coloredName = string.format("|cff%s%s|r", hex, char.name)
        local outMsg = string.format("[%s] %d %s", coloredName, char.mythicplus.keyLevel, shortName)
        print("|cffF58CBASey Weekly:|r (当前角色预览) " .. outMsg)
    end
end

local function ReplyWhisperKeystones(target)
    if not DB or not DB.chars then return end
    local list = {}
    for k, v in pairs(DB.chars) do
        if not v.hidden and v.mythicplus and v.mythicplus.keyLevel and v.mythicplus.keyLevel > 0 and v.mythicplus.keyName then
            table.insert(list, v)
        end
    end
    if #list == 0 then return end
    table.sort(list, SortChars)
    
    local delay = 0
    for _, char in ipairs(list) do
        local shortName = DUNGEON_SHORT_NAMES[char.mythicplus.keyName] or char.mythicplus.keyName
        local outMsg = string.format("[%s] %d %s", char.name, char.mythicplus.keyLevel, shortName)
        C_Timer.After(delay, function() SendChatMessage(outMsg, "WHISPER", nil, target) end)
        delay = delay + 0.05
    end
end

-- ============================================================
--  事件处理器
-- ============================================================
eventFrame:RegisterEvent("PLAYER_LOGIN"); eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD"); eventFrame:RegisterEvent("PLAYER_LOGOUT"); eventFrame:RegisterEvent("QUEST_TURNED_IN"); eventFrame:RegisterEvent("QUEST_LOG_UPDATE"); eventFrame:RegisterEvent("UPDATE_INSTANCE_INFO"); eventFrame:RegisterEvent("CURRENCY_DISPLAY_UPDATE"); eventFrame:RegisterEvent("BAG_UPDATE_DELAYED"); eventFrame:RegisterEvent("CHALLENGE_MODE_MAPS_UPDATE"); eventFrame:RegisterEvent("WEEKLY_REWARDS_UPDATE"); eventFrame:RegisterEvent("CHALLENGE_MODE_COMPLETED"); eventFrame:RegisterEvent("MYTHIC_PLUS_NEW_WEEKLY_RECORD"); eventFrame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED"); eventFrame:RegisterEvent("PLAYER_MONEY"); eventFrame:RegisterEvent("BANKFRAME_OPENED"); eventFrame:RegisterEvent("ACCOUNT_MONEY"); eventFrame:RegisterEvent("CHAT_MSG_ADDON"); eventFrame:RegisterEvent("CHAT_MSG_WHISPER")
eventFrame:SetScript("OnEvent", function(self, event, ...)
    if event == "PLAYER_LOGIN" then local isNewUser = not SeyWeeklyDB; SeyWeeklyDB = SeyWeeklyDB or {}; DB = SeyWeeklyDB; DB.chars = DB.chars or {}; DB.settings = DB.settings or {}; DB.collapsed = DB.collapsed or {}; DB.optExpanded = DB.optExpanded or {}; DB.instHistory = DB.instHistory or {}; DB.instGen = DB.instGen or 1
        if isNewUser then 
            for _, layout in ipairs(OPT_LAYOUT) do
                if layout.db then
                    DB.settings[layout.db] = true
                end
            end
            DB.settings.optOpen = false; DB.settings.isLocked = true 
        end
        sectionCollapsed = DB.collapsed; if DB.settings.crestEnabled then for _, cr in ipairs(CREST_LIST) do if DB.settings.crestEnabled[cr.key] == false then DB.settings["hideCrest_"..cr.key] = true end end DB.settings.crestEnabled = nil end
        mainFrame = BuildMainFrame(); --SetupMinimapButton(); 
        if isNewUser then 
            mainFrame.isLocked = true; 
            mainFrame:Show(); 
            if optFrame then optFrame:Hide() end; 
            RefreshOptFrame() 
        end
        _G["Sey_WeeklyFrame"] = mainFrame; tinsert(UISpecialFrames, "Sey_WeeklyFrame")
    elseif event == "PLAYER_ENTERING_WORLD" then UpdateInstanceHistory(); for k in pairs(scanFlags) do scanFlags[k] = true end RequestUpdate(); C_Timer.After(2, function() if C_MythicPlus and C_MythicPlus.RequestMapInfo then C_MythicPlus.RequestMapInfo() end if C_MythicPlus and C_MythicPlus.RequestRewards then C_MythicPlus.RequestRewards() end RequestUpdate() end)
    elseif event == "PLAYER_LOGOUT" then PurgeExpiredData(); SaveCharData(); SeyWeeklyDB = DB
    elseif event == "PLAYER_REGEN_ENABLED" then self:UnregisterEvent("PLAYER_REGEN_ENABLED") RequestUpdate()
    elseif event == "PLAYER_EQUIPMENT_CHANGED" then RequestUpdate("equip")
    elseif event == "UPDATE_INSTANCE_INFO" then RequestUpdate("raid")
    elseif event == "QUEST_TURNED_IN" then RequestUpdate("weekly"); RequestUpdate("curr")
    elseif event == "QUEST_LOG_UPDATE" then RequestUpdate("weekly")
    elseif event == "CURRENCY_DISPLAY_UPDATE" or event == "BAG_UPDATE_DELAYED" then RequestUpdate("curr"); RequestUpdate("mythic")
    elseif event == "WEEKLY_REWARDS_UPDATE" or event == "CHALLENGE_MODE_COMPLETED" or event == "MYTHIC_PLUS_NEW_WEEKLY_RECORD" then RequestUpdate("mythic"); RequestUpdate("weekly")
    elseif event == "PLAYER_MONEY" or event == "BANKFRAME_OPENED" or event == "ACCOUNT_MONEY" then RequestUpdate("gold") 
    elseif event == "CHAT_MSG_ADDON" then
        local prefix, text, channel, sender = ...
        if prefix == "SeyWeekly" then
            local playerName = UnitName("player")
            local senderName = strsplit("-", sender)
            if senderName ~= playerName then
                if text == "REQ_ALL_KEYS" then
                    AnnounceAllKeystones()
                elseif text == "REQ_CURRENT_KEY" then
                    AnnounceCurrentKeystone()
                end
            end
        end
    elseif event == "CHAT_MSG_WHISPER" then
        local text, sender = ...
        if text == "队伍钥石" then
            ReplyWhisperKeystones(sender)
        end
    end end)

SLASH_SEYWEEKLY1 = "/周常"; SLASH_SEYWEEKLY2 = "/seyweekly"; SLASH_SEYWEEKLY3 = "/sw"
SlashCmdList["SEYWEEKLY"] = function(msg) local cmd = msg:lower():match("^%s*(.-)%s*$")
    if cmd == "opt" then if activeEditBox then activeEditBox:ClearFocus() end if optFrame then if optFrame:IsShown() then optFrame:Hide() else optFrame:Show(); RefreshOptFrame() end end
    else if activeEditBox then activeEditBox:ClearFocus() end if mainFrame then mainFrame.isLocked = not mainFrame.isLocked; if mainFrame.isLocked then mainFrame:Show() else mainFrame:Hide() end end end end

SLASH_SEYWEEKLY_STONE1 = "/钥石"
SlashCmdList["SEYWEEKLY_STONE"] = function() 
    if IsInGroup() then 
        C_ChatInfo.SendAddonMessage("SeyWeekly", "REQ_CURRENT_KEY", GetPartyChannel())
    end
    AnnounceCurrentKeystone() 
end

SLASH_SEYWEEKLY_PARTYSTONE1 = "/队伍钥石"
SlashCmdList["SEYWEEKLY_PARTYSTONE"] = function() 
    if IsInGroup() then 
        C_ChatInfo.SendAddonMessage("SeyWeekly", "REQ_ALL_KEYS", GetPartyChannel())
    end 
    AnnounceAllKeystones()
end

-- ============================================================
--  副本重置检测 (世代更替) 及自动通知
-- ============================================================
hooksecurefunc("ResetInstances", function()
    if DB then DB.instGen = (DB.instGen or 1) + 1 end
    if IsInGroup() then
        SendChatMessage("[2ui]副本已重置", GetPartyChannel())
    end
end)