WoWInterface SVN Critline

[/] [tags/] [4.0.3/] [filters.lua] - Rev 26

Go to most recent revision | Compare with Previous | Blame | View Log

local addonName, addon = ...

local L = LibStub("AceLocale-3.0"):GetLocale(addonName)
local templates = addon.templates

local IsSpellKnown = IsSpellKnown
local UnitAura = UnitAura
local UnitName = UnitName
local UnitGUID = UnitGUID
local CombatLog_Object_IsA = CombatLog_Object_IsA
local band = bit.band

local COMBATLOG_FILTER_ME = COMBATLOG_FILTER_ME
local COMBATLOG_OBJECT_REACTION_FRIENDLY = COMBATLOG_OBJECT_REACTION_FRIENDLY

-- mobs whose received hits won't be tracked due to various vulnerabilities
local specialMobs = {
        [12460] = true, -- Death Talon Wyrmguard
        [12461] = true, -- Death Talon Overseer
        [14020] = true, -- Chromaggus
        [15339] = true, -- Ossirian the Unscarred
        [15928] = true, -- Thaddius
        [16803] = true, -- Death Knight Understudy
        [22841] = true, -- Shade of Akama
        [33329] = true, -- Heart of the Deconstructor
        [33670] = true, -- Aerial Command Unit
        [34496] = true, -- Eydis Darkbane
        [34497] = true, -- Fjola Lightbane
        [38567] = true, -- Phantom Hallucination
        [42347] = true, -- Exposed Head of Magmaw (Point of Vulnerability [79011]) ?
        [42803] = true, -- Drakeadon Mongrel (Brood Power: Red/Green/Black/Blue/Bronze [80368+80369+80370+80371+80372])
        [46083] = true, -- Drakeadon Mongrel (Brood Power: Red/Green/Black/Blue/Bronze [80368+80369+80370+80371+80372])
        [46273] = true, -- Debilitated Apexar
        [48270] = true, -- Exposed Head of Magmaw
}

-- auras that when gained will suppress record tracking
local specialAuras = {
        [18173] = true, -- Burning Adrenaline (Vaelastrasz the Corrupt)
        [41337] = true, -- Aura of Anger (Reliquary of Souls)
        [41350] = true, -- Aura of Desire (Reliquary of Souls)
        [44335] = true, -- Energy Feedback (Vexallus)
        [44406] = true, -- Energy Infusion (Vexallus)
        [53642] = true, -- Might of Mograine (Light's Hope Chapel)
        [55849] = true, -- Power Spark (Malygos)
        [56330] = true, -- Iron's Bane (Storm Peaks quest)
        [56648] = true, -- Potent Fungus (Amanitar)
        [57524] = true, -- Metanoia (Valkyrion Aspirant)
        [58026] = true, -- Blessing of the Crusade (Icecrown quest)
        [58361] = true, -- Might of Mograine (Patchwerk)
        [58549] = true, -- Tenacity (Lake Wintergrasp)
        [59641] = true, -- Warchief's Blessing (The Battle For The Undercity)
        [60964] = true, -- Strength of Wrynn (The Battle For The Undercity)
        [61888] = true, -- Overwhelming Power (Assembly of Iron - 25 man)
        [62243] = true, -- Unstable Sun Beam (Elder Brightleaf)
        [62650] = true, -- Fortitude of Frost (Yogg-Saron)
        [62670] = true, -- Resilience of Nature (Yogg-Saron)
        [62671] = true, -- Speed of Invention (Yogg-Saron)
        [62702] = true, -- Fury of the Storm (Yogg-Saron)
        [63277] = true, -- Shadow Crash (General Vezax)
        [63711] = true, -- Storm Power (Hodir - 10 man)
        [64320] = true, -- Rune of Power (Assembly of Iron)
        [64321] = true, -- Potent Pheromones (Freya)
        [64637] = true, -- Overwhelming Power (Assembly of Iron - 10 man)
        [65134] = true, -- Storm Power (Hodir - 25 man)
        [70867] = true, -- Essence of the Blood Queen (Blood Queen Lana'thel)
        [70879] = true, -- Essence of the Blood Queen (Blood Queen Lana'thel, bitten by a player)
        [72219] = true, -- Gastric Bloat (Festergut)
        [73822] = true, -- Hellscream's Warsong (Icecrown Citadel)
        [73828] = true, -- Strength of Wrynn (Icecrown Citadel) 
        [76133] = true, -- Tidal Surge (Neptulon)
        [76155] = true, -- Tidal Surge (Neptulon)
        [76159] = true, -- Pyrogenics (Sun-Touched Spriteling)
        [76355] = true, -- Blessing of the Sun (Rajh)
        [76693] = true, -- Empowering Twilight (Crimsonborne Warlord)
        [79624] = true, -- Power Generator (Arcanotron) ?
        [81096] = true, -- Red Mist (Red Mist)
        [86622] = true, -- Engulfing Magic (Theralion) ?
        [86872] = true, -- Frothing Rage (Thundermar Ale)
        [89879] = true, -- Blessing of the Sun (Rajh - heroic)
        [90933] = true, -- Ragezone (Defias Blood Wizard)
        [91871] = true, -- Lightning Charge (Siamat)
        [93777] = true, -- Invocation of Flame (Skullcrusher the Mountain)
        [95639] = true, -- Engulfing Magic (Theralion) ?
        [95640] = true, -- Engulfing Magic (Theralion) ?
        [95641] = true, -- Engulfing Magic (Theralion) ?
}

-- these are auras that increases the target's damage or healing received
local targetAuras = {
        [64436] = true, -- Magnetic Core (Aerial Command Unit) ?
        [65280] = true, -- Singed (Hodir)
        [66758] = true, -- Staggered Daze (Icehowl) ?
        [75664] = true, -- Shadow Gale (Erudax) ?
        [75846] = true, -- Superheated Quicksilver Armor (Karsh Steelbender) ?
        [76015] = true, -- Superheated Quicksilver Armor (Karsh Steelbender) ?
        [76232] = true, -- Storm's Fury (Ragnaros - Mount Hyjal) ?
        [77717] = true, -- Vertigo (Atramedes)
        [80164] = true, -- Chemical Cloud (Toxitron)
        [87683] = true, -- Dragon's Vengeance (Halfus Wyrmbreaker)
        [87904] = true, -- Feedback (Al'Akir)
        [90933] = true, -- Ragezone (Defias Blood Wizard) ?
        [91086] = true, -- Shadow Gale (Erudax - heroic)
        [92390] = true, -- Vertigo (Atramedes) ?
        [92910] = true, -- Debilitating Slime (Maloriak) ?
        [93567] = true, -- Superheated Quicksilver Armor (Karsh Steelbender) ?
        [95723] = true, -- Storm's Fury (Ragnaros - Mount Hyjal) ?
}

-- these heals are treated as periodic, but has no aura associated with them, or is associated to an aura with a different name, need to add exceptions for them to filter properly
local directHoTs = {
        [54172] = true, -- Divine Storm
        -- [63106] = "Corruption", -- Siphon Life
}

local activeAuras = {}
local corruptSpells = {}
local corruptTargets = {}

local playerAuras = {
        session = {},
        instance = {},
        lastFight = {},
}
local enemyAuras = {
        session = {},
        instance = {},
        lastFight = {},
}

-- name of current instance
local currentInstance = L["n/a"]

-- amount of buttons in the spell, mob and aura filter scroll lists
local NUMSPELLBUTTONS = 8
local SPELLBUTTONHEIGHT = 22
local NUMFILTERBUTTONS = 10
local FILTERBUTTONHEIGHT = 16


local filters = templates:CreateConfigFrame(FILTERS, addonName, true)
filters:SetScript("OnEvent", function(self, event, ...)
        return self[event] and self[event](self, ...)
end)
addon.filters = filters


local function filterButtonOnClick(self)
        local module = self.module
        local scrollFrame = module.scrollFrame
        local offset = FauxScrollFrame_GetOffset(scrollFrame)
        local id = self:GetID()
        
        local selection = scrollFrame.selected
        if selection then
                if selection - offset == id then
                        -- clicking the selected button, clear selection
                        self:UnlockHighlight()
                        selection = nil
                else
                        -- clear selection if visible, and set new selection
                        local prevHilite = scrollFrame.buttons[selection - offset]
                        if prevHilite then
                                prevHilite:UnlockHighlight()
                        end
                        self:LockHighlight()
                        selection = id + offset
                end
        else
                -- no previous selection, just set new and lock highlight
                self:LockHighlight()
                selection = id + offset
        end
        
        -- enable/disable "Delete" button depending on if selection exists
        if selection then
                module.delete:Enable()
        else
                module.delete:Disable()
        end
        scrollFrame.selected = selection
end

-- template function for mob filter buttons
local function createFilterButton(parent)
        local btn = CreateFrame("Button", nil, parent)
        btn:SetHeight(FILTERBUTTONHEIGHT)
        btn:SetPoint("LEFT")
        btn:SetPoint("RIGHT")
        btn:SetNormalFontObject("GameFontNormal")
        btn:SetHighlightFontObject("GameFontHighlight")
        btn:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
        btn:SetPushedTextOffset(0, 0)
        btn:SetScript("OnClick", filterButtonOnClick)
        return btn
end

local function createFilterButtons(parent, onEnter)
        local buttons = {}
        for i = 1, NUMFILTERBUTTONS do
                local btn = createFilterButton(parent)
                if i == 1 then
                        btn:SetPoint("TOP")
                else
                        btn:SetPoint("TOP", buttons[i - 1], "BOTTOM")
                end
                btn:SetID(i)
                if onEnter then
                        btn:SetScript("OnEnter", onEnter)
                        btn:SetScript("OnLeave", GameTooltip_Hide)
                end
                btn.module = parent
                buttons[i] = btn
        end
        parent.scrollFrame.buttons = buttons
end

local function resetScroll(self)
        FauxScrollFrame_SetOffset(self, 0)
        self.scrollBar:SetValue(0)
        self:Update()
end

local function onVerticalScroll(self, offset)
        FauxScrollFrame_OnVerticalScroll(self, offset, self.buttonHeight, self.Update)
end

local function filterFrameOnShow(self)
        local scrollFrame = self.scrollFrame
        if scrollFrame.selected then
                local prevHilite = scrollFrame.buttons[scrollFrame.selected - FauxScrollFrame_GetOffset(scrollFrame)]
                if prevHilite then
                        prevHilite:UnlockHighlight()
                end
                scrollFrame.selected = nil
                self.delete:Disable()
        end
end

local function addButtonOnClick(self)
        StaticPopup_Show(self.popup)
end

local function deleteButtonOnClick(self)
        local scrollFrame = self.scrollFrame
        local filterName = scrollFrame.filter
        local selection = scrollFrame.selected
        if selection then
                local filter = filters.db.global[filterName]
                local selectedEntry = filter[selection]
                tremove(filter, selection)
                local prevHighlight = scrollFrame.buttons[selection - FauxScrollFrame_GetOffset(scrollFrame)]
                if prevHighlight then
                        prevHighlight:UnlockHighlight()
                end
                scrollFrame.selected = nil
                scrollFrame:Update()
                self:Disable()
                addon:Message(self.msg:format(GetSpellInfo(selectedEntry) or selectedEntry))
                if self.func then
                        self.func(selectedEntry)
                end
        end
end

local function createFilterFrame(name, parent, numButtons, buttonHeight)
        local frame = CreateFrame("Frame", nil, parent)
        frame:SetHeight(numButtons * buttonHeight)
        parent[name] = frame

        local scrollName = "CritlineFilters"..name.."ScrollFrame"
        local scrollFrame = CreateFrame("ScrollFrame", scrollName, frame, "FauxScrollFrameTemplate")
        scrollFrame:SetAllPoints()
        scrollFrame:SetScript("OnShow", resetScroll)
        scrollFrame:SetScript("OnVerticalScroll", onVerticalScroll)
        scrollFrame.scrollBar = _G[scrollName.."ScrollBar"]
        scrollFrame.buttons = frame.buttons
        scrollFrame.numButtons = numButtons
        scrollFrame.buttonHeight = buttonHeight
        scrollFrame.filter = name
        frame.scrollFrame = scrollFrame
        
        if name ~= "spell" then
                frame:SetScript("OnShow", filterFrameOnShow)

                local add = templates:CreateButton(frame)
                add:SetScript("OnClick", addButtonOnClick)
                frame.add = add

                local delete = templates:CreateButton(frame)
                delete:Disable()
                delete:SetScript("OnClick", deleteButtonOnClick)
                delete.scrollFrame = scrollFrame
                frame.delete = delete
        end
        
        return frame
end


do
        local options = {}
        filters.options = options

        local checkButtons = {
                {
                        text = L["Filter new spells"],
                        tooltipText = L["Enable to filter out new spell entries by default."],
                        setting = "filterNew",
                },
                {
                        text = L["Ignore mob filter"],
                        tooltipText = L["Enable to ignore integrated mob filter."],
                        setting = "ignoreMobFilter",
                },
                {
                        text = L["Ignore aura filter"],
                        tooltipText = L["Enable to ignore integrated aura filter."],
                        setting = "ignoreAuraFilter",
                },
                {
                        text = L["Only known spells"],
                        tooltipText = L["Enable to ignore spells that are not in your (or your pet's) spell book."],
                        setting = "onlyKnown",
                },
                {
                        text = L["Suppress mind control"],
                        tooltipText = L["Suppress all records while mind controlled."],
                        setting = "suppressMC",
                        newColumn = true,
                },
                {
                        text = L["Don't filter magic"],
                        tooltipText = L["Enable to let magical damage ignore the level filter."],
                        setting = "dontFilterMagic",
                },
        }

        options.checkButtons = checkButtons
        
        local columnEnd = #checkButtons

        for i, v in ipairs(checkButtons) do
                local btn = templates:CreateCheckButton(filters, v)
                if i == 1 then
                        btn:SetPoint("TOPLEFT", filters.title, "BOTTOMLEFT", -2, -16)
                elseif btn.newColumn then
                        btn:SetPoint("TOPLEFT", filters.title, "BOTTOM", 0, -16)
                        columnEnd = i - 1
                else
                        btn:SetPoint("TOP", checkButtons[i - 1], "BOTTOM", 0, -8)
                end
                btn.module = filters
                checkButtons[i] = btn
        end
        
        local slider = templates:CreateSlider(filters, {
                text = L["Level filter"],
                tooltipText = L["If level difference between you and the target is greater than this setting, records will not be registered."],
                minValue = -1,
                maxValue = 10,
                valueStep = 1,
                minText = OFF,
                maxText = 10,
                func = function(self)
                        local value = self:GetValue()
                        self.value:SetText(value == -1 and OFF or value)
                        filters.profile.levelFilter = value
                end,
        })
        slider:SetPoint("TOPLEFT", checkButtons[#checkButtons], "BOTTOMLEFT", 4, -24)
        options.slider = slider
        
        local filterTypes = {}

        -- spell filter frame
        local spellFilter = createFilterFrame("spell", filters, NUMSPELLBUTTONS, SPELLBUTTONHEIGHT)
        spellFilter:SetPoint("TOP", checkButtons[columnEnd], "BOTTOM", 0, -48)
        spellFilter:SetPoint("LEFT", 48, 0)
        spellFilter:SetPoint("RIGHT", -48, 0)
        filterTypes.spell = spellFilter
        
        do      -- spell filter buttons
                local function spellButtonOnClick(self)
                        local checked = self:GetChecked() == 1
                        PlaySound(checked and "igMainMenuOptionCheckBoxOn" or "igMainMenuOptionCheckBoxOff")
                        filters:FilterSpell(not checked, filters.spell.tree:GetSelectedValue(), self.data)
                end
                
                local function spellButtonOnEnter(self)
                        -- prevent records being added twice
                        GameTooltip.Critline = true
                        GameTooltip:SetOwner(self, "ANCHOR_LEFT")
                        GameTooltip:SetSpellByID(self.data.spellID)
                        GameTooltip:AddLine(" ")
                        addon:AddTooltipLine(self.data)
                        GameTooltip:Show()
                end
                
                local buttons = {}
                for i = 1, NUMSPELLBUTTONS do
                        local btn = templates:CreateCheckButton(spellFilter)
                        if i == 1 then
                                btn:SetPoint("TOPLEFT")
                        else
                                btn:SetPoint("TOP", buttons[i - 1], "BOTTOM", 0, 4)
                        end
                        btn:SetScript("OnClick", spellButtonOnClick)
                        btn:SetScript("OnEnter", spellButtonOnEnter)
                        buttons[i] = btn
                end
                spellFilter.scrollFrame.buttons = buttons
        end
        
        -- spell filter scroll frame
        local spellScrollFrame = spellFilter.scrollFrame

        -- spell filter tree dropdown
        local menu = {
                {text = L["Damage"],    value = "dmg"},
                {text = L["Healing"],   value = "heal"},
                {text = L["Pet"],               value = "pet"},
        }

        local spellFilterTree = templates:CreateDropDownMenu("CritlineSpellFilterTree", spellFilter, menu)
        spellFilterTree:SetFrameWidth(120)
        spellFilterTree:SetPoint("BOTTOMRIGHT", spellFilter, "TOPRIGHT", 16, 0)
        spellFilterTree:SetSelectedValue("dmg")
        spellFilterTree.onClick = function(self)
                self.owner:SetSelectedValue(self.value)
                FauxScrollFrame_SetOffset(spellScrollFrame, 0)
                spellScrollFrame.scrollBar:SetValue(0)
                spellScrollFrame:Update()
        end
        spellFilter.tree = spellFilterTree
        spellScrollFrame.tree = spellFilter.tree
        
        do      -- mob filter frame
                local mobFilter = createFilterFrame("mobs", filters, NUMFILTERBUTTONS, FILTERBUTTONHEIGHT)
                mobFilter:SetPoint("TOP", spellFilter)
                mobFilter:SetPoint("LEFT", spellFilter)
                mobFilter:SetPoint("RIGHT", spellFilter)
                mobFilter:Hide()
                filterTypes.mobs = mobFilter
                
                createFilterButtons(mobFilter)
                
                local addTarget = templates:CreateButton(mobFilter)
                addTarget:SetSize(96, 22)
                addTarget:SetPoint("TOPLEFT", mobFilter, "BOTTOMLEFT", 0, -8)
                addTarget:SetText(L["Add target"])
                addTarget:SetScript("OnClick", function()
                        local targetName = UnitName("target")
                        if targetName then
                                -- we don't want to add PCs to the filter
                                if UnitIsPlayer("target") then
                                        addon:Message(L["Cannot add players to mob filter."])
                                else
                                        filters:AddMob(targetName)
                                end
                        else
                                addon:Message(L["No target selected."])
                        end
                end)
                
                local add = mobFilter.add
                add:SetSize(96, 22)
                add:SetPoint("TOP", mobFilter, "BOTTOM", 0, -8)
                add:SetText(L["Add by name"])
                add.popup = "CRITLINE_ADD_MOB_BY_NAME"
                
                local delete = mobFilter.delete
                delete:SetSize(96, 22)
                delete:SetPoint("TOPRIGHT", mobFilter, "BOTTOMRIGHT", 0, -8)
                delete:SetText(L["Delete mob"])
                delete.msg = L["%s removed from mob filter."]
        end
        
        do      -- aura filter frame
                local auraFilter = createFilterFrame("auras", filters, NUMFILTERBUTTONS, FILTERBUTTONHEIGHT)
                auraFilter:SetPoint("TOP", spellFilter)
                auraFilter:SetPoint("LEFT", spellFilter)
                auraFilter:SetPoint("RIGHT", spellFilter)
                auraFilter:Hide()
                filterTypes.auras = auraFilter

                createFilterButtons(auraFilter, function(self)
                        GameTooltip:SetOwner(self, "ANCHOR_TOPLEFT")
                        GameTooltip:SetHyperlink("spell:"..self.spellID)
                end)
                
                local add = auraFilter.add
                add:SetSize(128, 22)
                add:SetPoint("TOPLEFT", auraFilter, "BOTTOMLEFT", 0, -8)
                add:SetText(L["Add by spell ID"])
                add.popup = "CRITLINE_ADD_AURA_BY_ID"
                
                -- local addAura = templates:CreateButton(auraFilter)
                -- addAura:SetSize(48, 22)
                -- addAura:SetPoint("TOP", auraFilter, "BOTTOM")
                -- addAura:SetText("Add")
                -- addAura:SetScript("OnClick", function() if auraList:IsShown() then auraList:Hide() else auraList:Show() end end)

                local delete = auraFilter.delete
                delete:SetSize(128, 22)
                delete:SetPoint("TOPRIGHT", auraFilter, "BOTTOMRIGHT", 0, -8)
                delete:SetText(L["Delete aura"])
                delete.msg = L["%s removed from aura filter."]
                delete.func = function(spellID)
                        activeAuras[spellID] = nil
                        if not filters:IsEmpowered() then
                                addon:Debug("No filtered aura detected. Resuming record tracking.")
                        end
                end
        end
        
        do      -- filter tree dropdown
                local menu = {
                        {
                                text = L["Spell filter"],
                                value = "spell",
                        },
                        {
                                text = L["Mob filter"],
                                value = "mobs",
                        },
                        {
                                text = L["Aura filter"],
                                value = "auras",
                        },
                }
                
                local filterType = templates:CreateDropDownMenu("CritlineFilterType", filters, menu)
                filterType:SetPoint("BOTTOMLEFT", spellFilter, "TOPLEFT", -16, 0)
                filterType:SetFrameWidth(120)
                filterType:SetSelectedValue("spell")
                filterType.onClick = function(self)
                        self.owner:SetSelectedValue(self.value)
                        for k, v in pairs(filterTypes) do
                                if k == self.value then
                                        v:Show()
                                else
                                        v:Hide()
                                end
                        end
                end
                filters.type = filterType
        end
end


do
        local auraList = CreateFrame("Frame", nil, UIParent)
        auraList:SetFrameStrata("DIALOG")
        auraList:EnableMouse(true)
        auraList:SetSize(320, 360)
        auraList:SetPoint("CENTER")
        auraList:SetBackdrop({
                bgFile = [[Interface\ChatFrame\ChatFrameBackground]],
                edgeFile = [[Interface\Tooltips\UI-Tooltip-Border]],
                edgeSize = 16,
                insets = {left = 4, right = 4, top = 4, bottom = 4},
        })
        auraList:SetBackdropColor(0, 0, 0)
        auraList:SetBackdropBorderColor(0.5, 0.5, 0.5)
        auraList:Hide()

        local closeButton = CreateFrame("Button", nil, auraList, "UIPanelCloseButton")
        closeButton:SetPoint("TOPRIGHT")

        Critline.SlashCmdHandlers["aura"] = function() auraList:Show() end

        local currentFilter = playerAuras.session

        local function auraSort(a, b)
                return currentFilter[a].spellName < currentFilter[b].spellName
        end
        
        local function sourceSort(a, b)
                a, b = currentFilter[a], currentFilter[b]
                if a.source == b.source then
                        return a.spellName < b.spellName
                else
                        return a.source < b.source
                end
        end
        
        local auraFilters = {
                BUFF = true,
                DEBUFF = true,
                targetAffiliation = playerAuras,
                sourceType = "npc",
                sort = auraSort,
        }

        local function onClick(self, text)
                self.owner:SetSelectedValue(self.value)
                self.owner:SetText(text)
                currentFilter = auraFilters.targetAffiliation[self.value]
                CritlineAuraListScrollFrame:Update()
        end

        local menuList = {
                {
                        text = L["Current fight"],
                        value = "lastFight",
                },
                {
                        text = L["Current instance (%s)"],
                        value = "instance",
                },
                {
                        text = L["Current session"],
                        value = "session",
                },
        }
        
        local auraListFilter = templates:CreateDropDownMenu("CritlineAuraListFilter", auraList)
        auraListFilter:SetPoint("TOP", 0, -16)
        auraListFilter:SetFrameWidth(220)
        auraListFilter:JustifyText("LEFT")
        auraListFilter:SetSelectedValue("session")
        auraListFilter:SetText(L["Current session"])
        auraListFilter.initialize = function(self)
                for i, v in ipairs(menuList) do
                        local info = UIDropDownMenu_CreateInfo()
                        info.text = format(v.text, currentInstance)
                        info.value = v.value
                        info.func = onClick
                        info.owner = self
                        info.arg1 = info.text
                        UIDropDownMenu_AddButton(info)
                end
        end

        local auraListAuraType = templates:CreateDropDownMenu("CritlineAuraListAuraType", auraList)
        auraListAuraType:SetPoint("TOPLEFT", auraListFilter, "BOTTOMLEFT")
        auraListAuraType:SetFrameWidth(96)
        auraListAuraType:JustifyText("LEFT")
        auraListAuraType:SetText(L["Aura type"])

        do
                local function onClick(self)
                        auraFilters[self.value] = self.checked
                        CritlineAuraListScrollFrame:Update()
                end

                local menuList = {
                        {
                                text = L["Buffs"],
                                value = "BUFF",
                        },
                        {
                                text = L["Debuffs"],
                                value = "DEBUFF",
                        },
                }
                
                auraListAuraType.initialize = function(self)
                        for i, v in ipairs(menuList) do
                                local info = UIDropDownMenu_CreateInfo()
                                info.text = v.text
                                info.value = v.value
                                info.func = onClick
                                info.checked = auraFilters[v.value]
                                info.isNotRadio = true
                                info.keepShownOnClick = true
                                UIDropDownMenu_AddButton(info)
                        end
                end
        end

        local auraListFilters = templates:CreateDropDownMenu("CritlineAuraListFilters", auraList)
        auraListFilters:SetPoint("TOPRIGHT", auraListFilter, "BOTTOMRIGHT")
        auraListFilters:SetFrameWidth(96)
        auraListFilters:JustifyText("LEFT")
        auraListFilters:SetText(FILTERS)

        do
                local function onClick(self, key)
                        auraFilters[key] = self.value
                        self.owner:Refresh()
                        self.owner:SetText(FILTERS)
                        currentFilter = auraFilters.targetAffiliation[auraListFilter:GetSelectedValue()]
                        CritlineAuraListScrollFrame:Update()
                end

                local function checked(self)
                        return auraFilters[self.arg1] == self.value
                end

                local menuList = {
                        {
                                text = L["Show auras cast on me"],
                                value = playerAuras,
                                arg1 = "targetAffiliation",
                        },
                        {
                                text = L["Show auras cast on hostile NPCs"],
                                value = enemyAuras,
                                arg1 = "targetAffiliation",
                        },
                        {
                                text = L["Show auras cast by NPCs"],
                                value = "npc",
                                arg1 = "sourceType",
                        },
                        {
                                text = L["Show auras cast by players"],
                                value = "pvp",
                                arg1 = "sourceType",
                        },
                        {
                                text = L["Sort by aura name"],
                                value = auraSort,
                                arg1 = "sort",
                        },
                        {
                                text = L["Sort by source name"],
                                value = sourceSort,
                                arg1 = "sort",
                        },
                }
                
                auraListFilters.initialize = function(self)
                        for i, v in ipairs(menuList) do
                                local info = UIDropDownMenu_CreateInfo()
                                info.text = v.text
                                info.value = v.value
                                info.func = onClick
                                info.checked = checked
                                info.owner = self
                                info.keepShownOnClick = true
                                info.arg1 = v.arg1
                                UIDropDownMenu_AddButton(info)
                        end
                end
        end

        local search = templates:CreateEditBox(auraList)
        search:SetPoint("TOPLEFT", auraListAuraType, "BOTTOMLEFT", 18, -8)
        search:SetPoint("TOPRIGHT", auraListFilters, "BOTTOMRIGHT", -18, -8)
        search:SetWidth(192)
        search:SetScript("OnTextChanged", function() CritlineAuraListScrollFrame:Update() end)
        search:SetScript("OnEscapePressed", search.ClearFocus)

        local label = search:CreateFontString(nil, nil, "GameFontNormalSmall")
        label:SetPoint("BOTTOMLEFT", search, "TOPLEFT")
        label:SetText(L["Text filter"])

        local NUM_BUTTONS = 6
        local BUTTON_HEIGHT = 36
        
        local auraListScrollFrame = CreateFrame("ScrollFrame", "CritlineAuraListScrollFrame", auraList, "FauxScrollFrameTemplate")
        auraListScrollFrame:SetHeight(NUM_BUTTONS * BUTTON_HEIGHT)
        auraListScrollFrame:SetPoint("TOP", search, "BOTTOM", 0, -8)
        auraListScrollFrame:SetPoint("LEFT", 32, 0)
        auraListScrollFrame:SetPoint("RIGHT", -32, 0)
        auraListScrollFrame:SetScript("OnVerticalScroll", function(self, offset) FauxScrollFrame_OnVerticalScroll(self, offset, BUTTON_HEIGHT, self.Update) end)
        
        local sortedAuras = {}
        
        function auraListScrollFrame:Update()
                if not auraList:IsShown() then
                        self.doUpdate = true
                        return
                end
                
                self.doUpdate = nil
                
                wipe(sortedAuras)
                
                local n = 0
                local search = search:GetText():lower()
                for spellID, v in pairs(currentFilter) do
                        if auraFilters[v.type] and v.sourceType == auraFilters.sourceType and (v.spellName:lower():find(search, nil, true) or v.sourceName:lower():find(search, nil, true)) then
                                n = n + 1
                                sortedAuras[n] = spellID
                        end
                end
                
                sort(sortedAuras, auraFilters.sort)
                
                FauxScrollFrame_Update(self, n, NUM_BUTTONS, BUTTON_HEIGHT)
                
                local offset = FauxScrollFrame_GetOffset(self)
                local buttons = self.buttons
                for line = 1, NUM_BUTTONS do
                        local button = buttons[line]
                        local lineplusoffset = line + offset
                        if lineplusoffset <= n then
                                local spellID = sortedAuras[lineplusoffset]
                                button:SetFormattedText("%s (%d)", currentFilter[spellID].spellName, spellID)
                                button.source:SetText(currentFilter[spellID].source)
                                button.icon:SetTexture(addon:GetSpellTexture(spellID))
                                button.spellID = spellID
                                -- local disabled = filters:IsFilteredAura(spellID)
                                -- button.icon:SetDesaturated(disabled)
                                -- button.text:SetFontObject(disabled and "GameFontDisable" or "GameFontNormal")
                                button:Show()
                        else
                                button:Hide()
                        end
                end
        end
        
        auraList:SetScript("OnShow", function(self)
                if auraListScrollFrame.doUpdate then
                        auraListScrollFrame:Update()
                end
        end)
        
        local auraListButtons = {}
        auraListScrollFrame.buttons = auraListButtons
        
        -- local function onClick(self)
                -- local disabled = filters:IsFilteredAura(self.spellID)
                -- if disabled then
                        -- if specialAuras[self.spellID] then
                                -- addon:Message("Cannot delete integrated auras.")
                                -- return
                        -- else
                                -- local t = filters.db.global.auras
                                -- for i = 1, #t do
                                        -- if t[i] == self.spellID then
                                                -- tremove(t, i)
                                                -- addon:Message(format("Removed aura (%s) from filter.", GetSpellInfo(self.spellID)))
                                                -- break
                                        -- end
                                -- end
                        -- end
                -- else
                        -- filters:AddAura(self.spellID)
                -- end
                -- disabled = not disabled
                -- self.icon:SetDesaturated(disabled)
                -- self.text:SetFontObject(disabled and "GameFontDisable" or "GameFontNormal")
        -- end
        
        local function onEnter(self)
                GameTooltip:SetOwner(self, "ANCHOR_LEFT")
                GameTooltip:SetSpellByID(self.spellID)
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine(format(L["Spell ID: |cffffffff%d|r"], self.spellID))
                GameTooltip:Show()
        end

        for i = 1, NUM_BUTTONS do
                local btn = CreateFrame("Button", nil, auraList)
                btn:SetHeight(BUTTON_HEIGHT)
                if i == 1 then
                        btn:SetPoint("TOP", auraListScrollFrame)
                else
                        btn:SetPoint("TOP", auraListButtons[i - 1], "BOTTOM")
                end
                btn:SetPoint("LEFT", auraListScrollFrame)
                btn:SetPoint("RIGHT", auraListScrollFrame)
                btn:SetPushedTextOffset(0, 0)
                -- btn:SetScript("OnClick", onClick)
                btn:SetScript("OnEnter", onEnter)
                btn:SetScript("OnLeave", GameTooltip_Hide)
                
                if i % 2 == 0 then
                        local bg = btn:CreateTexture(nil, "BACKGROUND")
                        bg:SetAllPoints()
                        bg:SetTexture(1, 1, 1, 0.1)
                end
                
                local icon = btn:CreateTexture()
                icon:SetSize(32, 32)
                icon:SetPoint("LEFT")
                btn.icon = icon
                
                local text = btn:CreateFontString(nil, nil, "GameFontNormal")
                text:SetPoint("TOPLEFT", icon, "TOPRIGHT", 4, -4)
                text:SetPoint("RIGHT")
                text:SetJustifyH("LEFT")
                btn:SetFontString(text)
                btn.text = text
                
                local source = btn:CreateFontString(nil, nil, "GameFontHighlightSmall")
                source:SetPoint("BOTTOMLEFT", icon, "BOTTOMRIGHT", 4, 4)
                source:SetPoint("RIGHT")
                source:SetJustifyH("LEFT")
                btn.source = source
                
                auraListButtons[i] = btn
        end
end


StaticPopupDialogs["CRITLINE_ADD_MOB_BY_NAME"] = {
        text = L["Enter mob name:"],
        button1 = OKAY,
        button2 = CANCEL,
        hasEditBox = true,
        OnAccept = function(self)
                local name = self.editBox:GetText():trim()
                if not name:match("%S+") then
                        addon:Message(L["Invalid mob name."])
                        return
                end
                filters:AddMob(name)
        end,
        EditBoxOnEnterPressed = function(self)
                local name = self:GetText():trim()
                if not name:match("%S+") then
                        addon:Message(L["Invalid mob name."])
                        return
                end
                filters:AddMob(name)
                self:GetParent():Hide()
        end,
        EditBoxOnEscapePressed = function(self)
                self:GetParent():Hide()
        end,
        OnShow = function(self)
                self.editBox:SetFocus()
        end,
        whileDead = true,
        timeout = 0,
}

StaticPopupDialogs["CRITLINE_ADD_AURA_BY_ID"] = {
        text = L["Enter spell ID:"],
        button1 = OKAY,
        button2 = CANCEL,
        hasEditBox = true,
        OnAccept = function(self)
                local id = tonumber(self.editBox:GetText())
                if not id then
                        addon:Message(L["Invalid input. Please enter a spell ID."])
                        return
                elseif not GetSpellInfo(id) then
                        addon:Message(L["Invalid spell ID. No such spell."])
                        return
                end
                filters:AddAura(id)
        end,
        EditBoxOnEnterPressed = function(self)
                local id = tonumber(self:GetText())
                if not id then
                        addon:Message(L["Invalid input. Please enter a spell ID."])
                        return
                elseif not GetSpellInfo(id) then
                        addon:Message(L["Invalid spell ID. No such spell exists."])
                        return
                end
                filters:AddAura(id)
                self:GetParent():Hide()
        end,
        EditBoxOnEscapePressed = function(self)
                self:GetParent():Hide()
        end,
        OnShow = function(self)
                self.editBox:SetFocus()
        end,
        whileDead = true,
        timeout = 0,
}


local function updateSpellFilter(self)
        local selectedTree = self.tree:GetSelectedValue()
        local spells = addon:GetSpellArray(selectedTree)
        local size = #spells
        
        FauxScrollFrame_Update(self, size, self.numButtons, self.buttonHeight)
        
        local offset = FauxScrollFrame_GetOffset(self)
        local buttons = self.buttons
        for line = 1, NUMSPELLBUTTONS do
                local button = buttons[line]
                local lineplusoffset = line + offset
                if lineplusoffset <= size then
                        local data = spells[lineplusoffset]
                        button.data = data
                        button:SetText(addon:GetFullSpellName(data.spellID, data.periodic))
                        button:SetChecked(not data.filtered)
                        button:Show()
                else
                        button:Hide()
                end
        end
end

local function updateFilter(self)
        local filter = filters.db.global[self.filter]
        local size = #filter
        
        FauxScrollFrame_Update(self, size, self.numButtons, self.buttonHeight)
        
        local offset = FauxScrollFrame_GetOffset(self)
        local buttons = self.buttons
        for line = 1, self.numButtons do
                local button = buttons[line]
                local lineplusoffset = line + offset
                if lineplusoffset <= size then
                        if self.selected then
                                if self.selected - offset == line then
                                        button:LockHighlight()
                                else
                                        button:UnlockHighlight()
                                end
                        end
                        local entry = filter[lineplusoffset]
                        button.spellID = entry
                        button:SetText(type(entry) == "number" and GetSpellInfo(entry) or entry)
                        button:Show()
                else
                        button:Hide()
                end
        end
end


local defaults = {
        profile = {
                filterNew = false,
                onlyKnown = false,
                ignoreMobFilter = false,
                ignoreAuraFilter = false,
                suppressMC = true,
                dontFilterMagic = false,
                levelFilter = -1,
        },
        global = {
                mobs = {},
                auras = {},
        },
}

function filters:AddonLoaded()
        self.db = addon.db:RegisterNamespace("filters", defaults)
        addon.RegisterCallback(self, "SettingsLoaded", "LoadSettings")
        addon.RegisterCallback(self, "PerCharSettingsLoaded", "UpdateSpellFilter")
        addon.RegisterCallback(self, "SpellsChanged", "UpdateSpellFilter")
        
        self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
        self:RegisterEvent("PLAYER_REGEN_DISABLED")
        self:RegisterEvent("PLAYER_ENTERING_WORLD")
        self:RegisterEvent("PLAYER_LOGIN")
        self:RegisterEvent("UNIT_NAME_UPDATE")
        self:RegisterEvent("PLAYER_CONTROL_LOST")
        self:RegisterEvent("PLAYER_CONTROL_GAINED")
        
        -- mix in scroll frame update functions
        self.spell.scrollFrame.Update = updateSpellFilter
        self.mobs.scrollFrame.Update = updateFilter
        self.auras.scrollFrame.Update = updateFilter
end

addon.RegisterCallback(filters, "AddonLoaded")


function filters:COMBAT_LOG_EVENT_UNFILTERED(timestamp, eventType, hideCaster, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, spellID, spellName, spellSchool, auraType)
        if eventType == "SPELL_AURA_APPLIED" or eventType == "SPELL_AURA_REFRESH" then
                if targetAuras[spellID] then
                        corruptTargets[destGUID] = corruptTargets[destGUID] or {}
                        corruptTargets[destGUID][spellID] = true
                        addon:Debug(format("Target (%s) gained filtered aura. (%s) Ignore received damage.", destName, spellID))
                end
                if CombatLog_Object_IsA(destFlags, COMBATLOG_FILTER_ME) then
                        self:RegisterAura(playerAuras, sourceName, sourceGUID, spellID, spellName, auraType)
                        if self:IsFilteredAura(spellID) then
                                -- if we gain any aura in the filter we can just stop tracking records
                                if not (self:IsEmpowered() or self.profile.ignoreAuraFilter) then
                                        addon:Debug(format("Filtered aura gained. (%s) Disabling combat log tracking.", spellName))
                                end
                                activeAuras[spellID] = true
                        end
                else
                        if CombatLog_Object_IsA(sourceFlags, COMBATLOG_FILTER_ME) then
                                corruptSpells[spellID] = corruptSpells[spellID] or {}
                                corruptSpells[spellID][destGUID] = self:IsEmpowered() or self:IsVulnerableTarget(destGUID)
                        end
                        -- only non friendly NPC units
                        local unitType = band(destGUID:sub(1, 5), 0x007)
                        if (unitType ~= 0 and unitType ~= 4) and (band(destFlags, COMBATLOG_OBJECT_REACTION_FRIENDLY) == 0) then
                                self:RegisterAura(enemyAuras, sourceName, sourceGUID, spellID, spellName, auraType)
                        end
                end
        elseif (eventType == "SPELL_AURA_REMOVED" or eventType == "SPELL_AURA_BROKEN" or eventType == "SPELL_AURA_BROKEN_SPELL" or eventType == "SPELL_AURA_STOLEN") then
                if targetAuras[spellID] then
                        corruptTargets[destGUID] = corruptTargets[destGUID] or {}
                        corruptTargets[destGUID][spellID] = nil
                        addon:Debug(format("Filtered aura (%s) faded from %s.", spellName, destName))
                end
                if CombatLog_Object_IsA(destFlags, COMBATLOG_FILTER_ME) then
                        if self:IsFilteredAura(spellID) then
                                addon:Debug(format("Filtered aura (%s) faded from player.", spellName))
                                -- if we lost a special aura we have to check if any other filtered auras remain
                                activeAuras[spellID] = nil
                                if not filters:IsEmpowered() then
                                        addon:Debug("No filtered aura detected. Resuming record tracking.")
                                end
                        -- elseif CombatLog_Object_IsA(sourceFlags, COMBATLOG_FILTER_ME) then
                                -- corruptSpells[spellID] = corruptSpells[spellID] or {}
                                -- corruptSpells[spellID][destGUID] = nil
                        end
                -- else
                end
        end
end


-- reset current fight auras upon entering combat
function filters:PLAYER_REGEN_DISABLED()
        wipe(playerAuras.lastFight)
        wipe(enemyAuras.lastFight)
        CritlineAuraListScrollFrame:Update()
end


function filters:PLAYER_ENTERING_WORLD()
        -- wipe instance buff data when entering a new instance
        local instanceName = GetInstanceInfo()
        if IsInInstance() and instanceName ~= currentInstance then
                wipe(playerAuras.instance)
                wipe(enemyAuras.instance)
                currentInstance = instanceName
                if CritlineAuraListFilter:GetSelectedValue() == "instance" then
                        CritlineAuraListFilter:SetText(format(L["Current instance (%s)"], currentInstance))
                end
                CritlineAuraListScrollFrame:Update()
        end
end


function filters:PLAYER_LOGIN()
        self:ScanAuras()
end


function filters:UNIT_NAME_UPDATE()
        self:ScanAuras()
        self:UnregisterEvent("UNIT_NAME_UPDATE")
end


function filters:PLAYER_CONTROL_LOST()
        self.inControl = false
        addon:Debug("Lost control. Disabling combat log tracking.")
end


function filters:PLAYER_CONTROL_GAINED()
        self.inControl = true
        addon:Debug("Regained control. Resuming combat log tracking.")
end


function filters:LoadSettings()
        self.profile = self.db.profile
        
        for i, v in ipairs(self.options.checkButtons) do
                v:LoadSetting()
        end
        
        self.options.slider:SetValue(self.profile.levelFilter)
end


local auraTypes = {
        BUFF = "HELPFUL",
        DEBUFF = "HARMFUL",
}

function filters:ScanAuras()
        local auras = {}
        for auraType, filter in pairs(auraTypes) do
                for i = 1, 40 do
                        local spellName, _, _, _, _, _, _, source, _, _, spellID = UnitAura("player", i, filter)
                        if not spellID then break end
                        auras[spellID] = true
                        if specialAuras[spellID] then
                                activeAuras[spellID] = true
                        end
                        self:RegisterAura(playerAuras, source and UnitName(source), source and UnitGUID(source), spellID, spellName, auraType)
                end
        end
        CritlineAuraListScrollFrame:Update()
        if next(auras) then
                self:UnregisterEvent("UNIT_NAME_UPDATE")
        end
        for i, v in ipairs(self.db.global.auras) do
                activeAuras[v] = auras[v]
        end
        if next(activeAuras) then
                addon:Debug("Filtered aura detected. Disabling combat log tracking.")
        end
        self.inControl = HasFullControl()
        if not self.inControl then
                addon:Debug("Lost control. Disabling combat log tracking.")
        end
end


function filters:UpdateSpellFilter()
        self.spell.scrollFrame:Update()
end


function filters:UpdateFilter()
        self[self.type:GetSelectedValue()].scrollFrame:Update()
end


function filters:FilterSpell(filter, tree, data)
        data.filtered = filter
        addon:GetSpellInfo(tree, data.spellID, data.periodic).filtered = filter
        addon:UpdateTopRecords(tree)
        addon:UpdateRecords(tree)
end


-- adds a mob to the mob filter
function filters:AddMob(name)
        if self:IsFilteredMob(name) then
                addon:Message(L["%s is already in mob filter."]:format(name))
        else
                tinsert(self.db.global.mobs, name)
                self:UpdateFilter()
                addon:Message(L["%s added to mob filter."]:format(name))
        end
end


-- adds an aura to the aura filter
function filters:AddAura(spellID)
        local spellName = GetSpellInfo(spellID)
        if self:IsFilteredAura(spellID) then
                addon:Message(L["%s is already in aura filter."]:format(spellName))
        else
                tinsert(self.db.global.auras, spellID)
                -- after we add an aura to the filter; check if we have it
                for i = 1, 40 do
                        local buffID = select(11, UnitBuff("player", i))
                        local debuffID = select(11, UnitDebuff("player", i))
                        if not (buffID or debuffID) then
                                break
                        else
                                for _, v in ipairs(self.db.global.auras) do
                                        if v == buffID then
                                                activeAuras[buffID] = true
                                                break
                                        elseif v == debuffID then
                                                activeAuras[debuffID] = true
                                                break
                                        end
                                end
                        end
                end
                self:UpdateFilter()
                addon:Message(L["%s added to aura filter."]:format(spellName))
        end
end


-- check if a spell passes the filter settings
function filters:SpellPassesFilters(tree, spellName, spellID, isPeriodic, destGUID, destName, school, targetLevel)
        if spellID and not IsSpellKnown(spellID, tree == "pet") and self.profile.onlyKnown then
                addon:Debug(format("%s is not in your%s spell book. Return.", spellName, tree == "pet" and " pet's" or ""))
                return
        end
        
        if ((corruptSpells[spellID] and corruptSpells[spellID][destGUID]) or (self:IsEmpowered() and (not isPeriodic or directHoTs[spellID]))) and not self.profile.ignoreAuraFilter then
                addon:Debug(format("Spell (%s) was cast under the influence of a filtered aura. Return.", spellName))
                return
        end
        
        if self:IsVulnerableTarget(destGUID) and not self.profile.ignoreAuraFilter then
                addon:Debug("Target is vulnerable. Return.")
                return
        end
        
        local levelDiff = 0
        if (targetLevel > 0) and (targetLevel < UnitLevel("player")) then
                levelDiff = (UnitLevel("player") - targetLevel)
        end
        
        -- ignore level adjustment if magic damage and the setting is enabled
        if not isHeal and (self.profile.levelFilter >= 0) and (self.profile.levelFilter < levelDiff) and (school == 1 or not self.profile.dontFilterMagic) then
                -- target level is too low to pass level filter
                addon:Debug(format("Target (%s) level too low (%d) and damage school is filtered. Return.", destName, targetLevel))
                return
        end
        
        local filteredMob = self:IsFilteredMob(destName, destGUID)
        if filteredMob then
                addon:Debug(format("Target (%s) is in %s target filter.", mobName, filteredMob))
                return
        end
        
        return true, self:IsFilteredSpell(tree, spellID, isPeriodic and 2 or 1), targetLevel
end


-- check if a spell will be filtered out
function filters:IsFilteredSpell(tree, spellID, periodic)
        local spell = addon:GetSpellInfo(tree, spellID, periodic)
        return (not spell and self.db.profile.filterNew) or (spell and spell.filtered)
end


-- scan for filtered auras from the specialAuras table
function filters:IsEmpowered()
        if next(activeAuras) or not self.inControl then
                return true
        end
end


-- checks if a target is affected by any vulnerability auras
function filters:IsVulnerableTarget(guid)
        local corruptTarget = corruptTargets[guid]
        if corruptTarget and next(corruptTarget) then
                return true
        end
end


function filters:IsFilteredMob(mobName, guid)
        -- GUID is provided if the function was called from the combat event handler
        if guid and not self.profile.ignoreMobFilter and specialMobs[tonumber(guid:sub(7, 10), 16)] then
                return "default"
        end
        for _, v in ipairs(self.db.global.mobs) do
                if v:lower() == mobName:lower() then
                        return "custom"
                end
        end
end


function filters:IsFilteredAura(spellID)
        if specialAuras[spellID] then
                return true
        end
        for _, v in ipairs(self.db.global.auras) do
                if v == spellID then
                        return true
                end
        end
end


function filters:RegisterAura(auraTable, sourceName, sourceGUID, spellID, spellName, auraType)
        local session = auraTable.session
        if session[spellID] or IsSpellKnown(spellID) or not sourceName then
                return 
        end

        local source = L["n/a"]
        local sourceType
        
        local unitType = bit.band(sourceGUID:sub(1, 5), 0x007)
        if unitType == 0 or unitType == 4 then
                -- this is a player or a player's permanent pet
                source = PVP
                sourceType = "pvp"
        else
                source = tonumber(sourceGUID:sub(7, 10), 16)
                sourceType = "npc"
        end
        
        local aura = {
                source = format("%s (%s)", sourceName, source),
                sourceName = sourceName,
                spellName = spellName,
                sourceType = sourceType,
                type = auraType,
        }
        auraTable.lastFight[spellID] = aura
        if IsInInstance() then
                auraTable.instance[spellID] = aura
        end
        session[spellID] = aura
        CritlineAuraListScrollFrame:Update()
end

Go to most recent revision | Compare with Previous | Blame