WoWInterface SVN Ara_Broker_Guild_Friends

[/] [trunk/] [Ara_Broker_Guild_Friends.lua] - Rev 28

Compare with Previous | Blame | View Log

local   BUTTON_HEIGHT,  ICON_SIZE,      GAP,    TEXT_OFFSET,    MAX_ENTRIES =
        15,             13,             10,     5

local f = CreateFrame( "Frame", "AraBrokerGuildFriends", UIParent )
local t = CreateFrame"Frame"

local dontShow, block, horde, config, isGuild, tip, tipShown = true
local defaultConfig = { showGuildNotes = true, showGuildName = true, sortType = "class", sortDESC = false, scale = 1 }
local guildEntries, friendEntries, motd, slider, nbEntries = {}, {}
local sliderValue, hasSlider, ShowTablet = 0
local RAID_CLASS_COLORS = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
local L = {}

local format, strfind, GetGuildRosterInfo, GetFriendInfo, CLASS_BUTTONS, GetDifficultyColor =
        format, strfind, GetGuildRosterInfo, GetFriendInfo, CLASS_BUTTONS, GetQuestDifficultyColor

local orgReloadUI = ReloadUI
ReloadUI = function(...)
        config.reloading = true
        orgReloadUI(...)
end

local tables = {}

local new = function( ... )
        local table = tremove(tables) or {}
        for i=1, select("#",...) do table[i] = select(i,...) end
        return table
end

local del = function( table )
        tables[ #tables + 1 ] = wipe(table)
end


local friendOnline, friendOffline = gsub(ERR_FRIEND_ONLINE_SS,"\124Hplayer:%%s\124h%[%%s%]\124h",""), gsub(ERR_FRIEND_OFFLINE_S,"%%s","")
function f:CHAT_MSG_SYSTEM( event, msg )
        if strfind( msg, friendOnline ) or strfind( msg, friendOffline ) then ShowFriends() end
end

function f:FRIENDLIST_UPDATE()
        for k,v in next,friendEntries do del(v) friendEntries[k]=nil end
        local total, online = GetNumFriends(), 0
        for i = 1, total do
                local name, level, class, zone, connected, status, note = GetFriendInfo(i)
                if connected then
                        if online == 0 then
                                friendEntries[1] = new( nil, "Name", "", "Lv", "Zone", "Notes" )
                        end
                        online = online + 1
                        friendEntries[online+1] = new( name, status ~= "" and format("%s %s", tostring(status), tostring(name)) or name, L[class], level, zone or "Unknown", note and format("[%s]",note) or "-" )
                end
        end
        f.FriendsBlock.text = format(config.hideTotalFriends and " %d" or " %d/%d", online, total)
        if not isGuild and block then f:FriendsOnEnter(block) end
end

function f:GUILD_ROSTER_UPDATE()
        dontShow = not(isGuild and block)
        f.GuildOnEnter(block)
end

function f:PLAYER_GUILD_UPDATE(event, unit)
        if unit and unit ~= "player" then return end
        return IsInGuild() and GuildRoster()
end

local hordeZones = "Orgrimmar,Undercity,Thunder Bluff,Silvermoon City,Durotar,Tirisfal Glades,The Barrens,Silverpine Forest,Mulgore,The Sepulcher,Eversong Woods,Ghostlands,"
local allianceZones = "Ironforge,Stormwind City,Darnassus,Azuremyst Isle,Bloodmyst Isle,Darkshore,Dun Morogh,Loch Modan,Wetlands,Elwynn Forest,Redridge Mountains,Westfall,Teldrassil,Duskwood,Darshire,Thelsamar,Deeprun Tram,The Exodar,Auberdine,Menethil Harbor,Theramore Isle,"
local function GetZoneColor(zone)
        if strfind( hordeZones   , zone.."," ) then if horde then return 0,1,0 else return 1,0,0 end end
        if strfind( allianceZones, zone.."," ) then if horde then return 1,0,0 else return 0,1,0 end end
        return 1,1,0
end

local function ShowBlockHints()
        local showBelow = UIParent:GetHeight()-f:GetTop()*f:GetScale() < f:GetBottom()*f:GetScale()
        tip:SetOwner(f, "ANCHOR_NONE")
        tip:SetPoint(showBelow and "TOP" or "BOTTOM", f, showBelow and "BOTTOM" or "TOP")
        tip:AddLine"Hints [|cffffffffBlock|r]"
        tip:AddLine("|cffff8020Click|r to open panel.", .2,1,.2)
        tip:AddLine("|cffff8020Ctrl+Click|r to toggle hints.", .2,1,.2)
        tip:AddLine("|cffff8020Shift+RightClick|r to toggle total number.", .2,1,.2)
        if isGuild then
                tip:AddLine("|cffff8020Shift+Click|r to toggle guild name.", .2,1,.2)
                tip:AddLine("|cffff8020RightClick|r to toggle note column.", .2,1,.2)
        else
                tip:AddLine("|cffff8020EverythingElse|r to add a friend.", .2,1,.2)
        end
        tip:Show()
end

local function ShowHints(btn)
        if not config.hideHints and btn and btn.unit then
                local showBelow = UIParent:GetHeight()-f:GetTop()*f:GetScale() < f:GetBottom()*f:GetScale()
                tip:SetOwner(f, "ANCHOR_NONE")
                tip:SetPoint(showBelow and "TOP" or "BOTTOM", f, showBelow and "BOTTOM" or "TOP")
                tip:AddLine"Hints"
                tip:AddLine("|cffff8020Click|r to whisper.", .2,1,.2)
                tip:AddLine("|cffff8020Alt+Click|r to invite.", .2,1,.2)
                tip:AddLine("|cffff8020Shift+Click|r to query guild.", .2, 1, .2)
                if not isGuild or CanEditPublicNote() then tip:AddLine("|cffff8020Ctrl+Click|r to edit note.", .2, 1, .2) end
                if isGuild then
                        if CanEditOfficerNote() then tip:AddLine("|cffff8020Ctrl+RightClick|r to edit officer note.", .2, 1, .2) end
                        tip:AddLine("|cffff8020RightClick|r to sort closest column.", .2, 1, .2)
                else
                        tip:AddLine("|cffff8020MiddleClick|r to remove friend.", .2, 1, .2)
                end
                tip:AddLine("|cffff8020Ctrl+MouseWheel|r to resize tooltip.", .2, 1, .2)
                tip:Show()
                tipShown = true
        end
end

local highlight = f:CreateTexture()
highlight:SetTexture"Interface\\QuestFrame\\UI-QuestTitleHighlight"
highlight:SetBlendMode"ADD"
highlight:SetAlpha(0)

local function Menu_OnEnter(b)
        if b and b.index then
                highlight:SetAllPoints(b)
                if b.index > 1 then
                        highlight:SetAlpha(1)
                        ShowHints(b)
                end
        end
end

local function Menu_OnLeave(b)
        highlight:ClearAllPoints()
        tip:Hide()
        tipShown = false
        if b and b.index and b.index > 1 then highlight:SetAlpha(0) end
        if not MouseIsOver(f) then
                sliderValue = 0
                block = nil
                f:Hide()
        end
end

local function CreateFS( index, parent, justify, anchor )
        local fs = parent:CreateFontString( nil, "OVERLAY", "SystemFont_Shadow_Med1" )
        fs:SetJustifyH( justify )
        fs:SetTextColor( 1, index == 1 and .8 or 1, 0 )
        if anchor then fs:SetPoint( "LEFT", anchor, "RIGHT", GAP, 0 ) end
        return fs
end

local function EditMOTD()
        f:Hide()
        block = nil
        GuildMOTDEditButton:Click()
end

local cols, colspace = { "class", "name", "level", "zone", "note", "rank" }, { ICON_SIZE }

local function OnGuildmateClick( self, button )
        if not( self and self.unit ) then return end
        if isGuild and button ~= "LeftButton" and not IsModifierKeyDown() then -- sort columns
                local prevDist = GetCursorPosition() / self:GetEffectiveScale() - self:GetLeft()
                local colpos, dist = colspace[1] - prevDist
                for i=2, #colspace do
                        colpos = colpos + colspace[i] + (i==2 and TEXT_OFFSET or GAP)
                        dist = abs( colpos - colspace[i]*.5 )
                        if dist > prevDist then dist, prevDist = i-1, -1 break else prevDist = dist end
                end
                colpos = prevDist < 0 and dist or #colspace
                colpos = not config.showGuildNotes and colpos == 5 and "rank" or cols[colpos]
                if config.sortType == colpos then
                        config.sortDESC = not config.sortDESC
                else
                        config.sortType, config.sortDESC = colpos, false
                end
                SortGuildRoster( colpos )
        elseif button == "MiddleButton" and not isGuild then
                RemoveFriend( self.unit )
        elseif IsAltKeyDown() then
                InviteUnit( self.unit )
        elseif IsControlKeyDown() then
                if not isGuild then
                        FriendsFrame.NotesID = self.index - 1
                        StaticPopup_Show( "SET_FRIENDNOTE", self.unit )
                elseif button == "LeftButton" and CanEditPublicNote() or button ~= "LeftButton" and CanEditOfficerNote() then
                        SetGuildRosterSelection( guildEntries[self.index][8] )
                        StaticPopup_Show( button == "LeftButton" and "SET_GUILDPLAYERNOTE" or "SET_GUILDOFFICERNOTE" )
                end
        else
                SetItemRef("player:"..self.unit, "|Hplayer:"..self.unit.."|h["..self.unit.."]|h", "LeftButton")
        end
end

local function Scroll(self, delta)
        if IsControlKeyDown() then
                config.scale = config.scale - delta * .05
                return ShowTablet(tipShown, isGuild)
        end
        slider:SetValue( sliderValue - delta * (IsModifierKeyDown() and 10 or 3) )
end

local buttons = setmetatable( { }, { __index = function( table, key )
        local button = CreateFrame( "Button", nil, f )
        table[key] = button
        button.index = key
        button:SetNormalFontObject(GameFontNormal)
        button:RegisterForClicks"AnyUp"
        button:SetScript( "OnEnter", Menu_OnEnter )
        button:SetScript( "OnLeave", Menu_OnLeave )

        button:EnableMouseWheel(true)
        button:SetScript( "OnMouseWheel", Scroll)

        button.fontName = CreateFS( key, button, "LEFT" )
        if key == 0 then
                motd = button
                motd.fontName:SetJustifyV"TOP"
                motd.fontName:SetPoint( "TOPLEFT", motd, "TOPLEFT" )
                motd:SetPoint( "TOPLEFT", f, "TOPLEFT", GAP, -GAP )
        else
                button:SetHeight( BUTTON_HEIGHT )
                button.icon = button:CreateTexture()
                button.icon:SetWidth( ICON_SIZE ) button.icon:SetHeight( ICON_SIZE )
                button.icon:SetPoint( "LEFT", button, "LEFT" )

                button.fontName:SetPoint( "LEFT", button.icon, "RIGHT", TEXT_OFFSET, 0 )
                button.fontLevel = CreateFS( key, button, "CENTER", button.fontName )
                button.fontZone  = CreateFS( key, button, "CENTER", button.fontLevel )
                button.fontNotes = CreateFS( key, button, "CENTER", button.fontZone )
                button.fontRank  = CreateFS( key, button, "RIGHT",  button.fontNotes )
        end
        return button
end } )

local function SetButtonData( index, name, class, level, zone, notes, rank, inGroup, isGrouped )
        local button = buttons[index]
        button.fontName:SetText( name or "" )
        if class then
                if index > 1 then
                        local color = RAID_CLASS_COLORS[class]
                        button.fontName:SetTextColor( color.r, color.g, color.b )
                        if inGroup then
                                button.icon:SetTexture(isGrouped and "Interface\\Buttons\\UI-CheckBox-Check" or "")
                                button.icon:SetTexCoord(.15,.85,.15,.85)
                        else
                                button.icon:SetTexture"Interface\\Glues\\CharacterCreate\\UI-CharacterCreate-Classes"
                                local offset, left, right, bottom, top = 0.025, unpack( CLASS_BUTTONS[class] )
                                button.icon:SetTexCoord( left+offset, right-offset, bottom+offset, top-offset )
                        end
                        color = GetDifficultyColor(level)
                        button.fontLevel:SetTextColor( color.r, color.g, color.b )
                        button.fontZone:SetTextColor( GetZoneColor(zone or "#") )
                end
                button.fontLevel:SetText( level or "" )
                button.fontZone:SetText( zone or "" )
                button.fontNotes:SetText( notes or "" )
                button.fontRank:SetText( rank or "" )
                return button, button.fontName:GetStringWidth(), button.fontLevel:GetStringWidth(), button.fontZone:GetStringWidth(), button.fontNotes:GetStringWidth(), button.fontRank:GetStringWidth()
        end
        return button, button.fontName:GetStringWidth()
end

local function UpdateScrollButtons(nbEntries)
        local btn = buttons[1]
        for i=2, #buttons do buttons[i]:Hide() end
        for i=2, nbEntries do
                button = buttons[sliderValue+i]
                button:SetPoint("TOPLEFT", btn, "TOPLEFT", 0, BUTTON_HEIGHT*(1-i))
                button:Show()
        end
end

ShowTablet = function( self, _isGuild )
        f:Show()
        isGuild = _isGuild

        f:SetScale(config.scale)
        MAX_ENTRIES = floor( WorldFrame:GetHeight() / ( BUTTON_HEIGHT * f:GetEffectiveScale() ) ) - (config.hideHints and 6 or 12)
        slider:SetHeight(BUTTON_HEIGHT*MAX_ENTRIES)

        local entries = isGuild and guildEntries or friendEntries
        local hasEntries = #entries > 0
        hasSlider = #entries > MAX_ENTRIES

        if not hasEntries then
                entries[1] = new( nil, isGuild and ERR_GUILD_PLAYER_NOT_IN_GUILD or "No friends online.", "" )
        end
        if hasSlider then
                slider:SetMinMaxValues( 0, #entries - MAX_ENTRIES )
                slider:SetValue(sliderValue)
                slider:Show()
        else    slider:Hide() end

        nbEntries = math.min( MAX_ENTRIES, #entries )
        local nameC, levelC, zoneC, notesC, rankC = 0, 0, 0, 0, 0
        local nameW, levelW, zoneW, notesW, rankW
        local hideNotes = isGuild and not config.showGuildNotes
        local extraHeight, button = 0

        local isGrouped = GetNumRaidMembers()>0 and UnitInRaid or GetNumPartyMembers()>0 and UnitInParty or nil
        for i = 1, #entries do
                local entry = entries[i]
                button, nameW, levelW, zoneW, notesW, rankW = SetButtonData( i, entry[2], entry[3], entry[4], entry[5], entry[6], entry[7], isGrouped, isGrouped and entry[1] and isGrouped(entry[1]) )
                button.unit = entry[1]
                button:SetScript( "OnClick", OnGuildmateClick )
                if nameW > nameC then nameC = nameW end
                if levelW and levelW>0 then
                        if levelW > levelC then levelC = levelW end
                        if  zoneW >  zoneC then  zoneC = zoneW  end
                        if notesW > notesC then notesC = notesW end
                        if  rankW >  rankC then  rankC = rankW  end
                        if hideNotes then button.fontNotes:Hide() else button.fontNotes:Show() end
                        button.fontRank:SetPoint( "LEFT", hideNotes and button.fontZone or button.fontNotes, "RIGHT", GAP, 0 )
                end
        end

        if hideNotes then notesC = -GAP end
        local maxWidth = ICON_SIZE + TEXT_OFFSET + nameC + levelC + zoneC + notesC + rankC + GAP * (isGuild and 4 or 3)

        if isGuild and hasEntries then
                SetButtonData( 0, "|cffffffffMOTD:|r  "..GetGuildRosterMOTD() )
                if CanEditMOTD() then motd:SetScript( "OnClick", EditMOTD ) end
                motd.fontName:SetWidth( maxWidth )
                extraHeight = motd.fontName:GetHeight() + BUTTON_HEIGHT
                motd:SetWidth( maxWidth )
                motd:SetHeight( extraHeight )
                motd:Show()
                buttons[1]:SetPoint( "TOPLEFT", motd, "BOTTOMLEFT" )
        else
                buttons[0]:Hide()
                buttons[1]:SetPoint( "TOPLEFT", f, "TOPLEFT", GAP, -GAP )
        end

        UpdateScrollButtons(nbEntries)

        for i=1, #entries do
                button = buttons[i]
                button:SetWidth( maxWidth )
                button.fontName:SetWidth(nameC)
                button.fontLevel:SetWidth(levelC)
                button.fontZone:SetWidth(zoneC)
                button.fontNotes:SetWidth(notesC)
                button.fontRank:SetWidth(rankC)
        end
        colspace[2], colspace[3], colspace[4], colspace[5], colspace[6] = nameC, levelC, zoneC, notesC, rankC

        if hasSlider then slider:SetPoint("TOPRIGHT", buttons[2], "TOPRIGHT", 19 + TEXT_OFFSET, BUTTON_HEIGHT*.5) end
        f:SetWidth( maxWidth + GAP*2 + (hasSlider and 16 + TEXT_OFFSET*2 or 0) )
        f:SetHeight( BUTTON_HEIGHT * nbEntries + extraHeight + GAP*2 )

        block = block or self
        Menu_OnEnter(block)
        local showBelow = select( 2, block:GetCenter() ) > ( UIParent:GetHeight() / 2 )
        f:ClearAllPoints()
        f:SetPoint( showBelow and "TOP" or "BOTTOM", block, showBelow and "BOTTOM" or "TOP" )
        if not (tipShown or config.hideHints) then ShowBlockHints() end
end


function f:FriendsOnEnter()
        if InCombatLockdown() then f:Show() return Menu_OnEnter(f) end
        return ShowTablet( self, false )
end

function f:GuildOnEnter()
        if not dontShow and InCombatLockdown() then f:Show() return Menu_OnEnter(f) end
        for k, v in next, guildEntries do del(v) guildEntries[k]=nil end
        for i=1, GetNumGuildMembers(true) do
                local name, rank, _, level, class, zone, note, offnote, connected, status = GetGuildRosterInfo(i)
                if connected then
                        if #guildEntries == 0 then
                                guildEntries[1] = new( nil, "Name", "", "Lv", "Zone", "Notes", "Rank" )
                        end
                        local notes = note ~= "" and (offnote ~= "" and
                                format( "[%s] - \124cffff9944[%s]\124r", note, offnote ) or
                                format( "[%s]", note )) or (offnote ~= "" and
                                format( "\124cffff9944[%s]\124r", offnote ) or "-")
                        guildEntries[#guildEntries+1] = new( name, status == "" and name or format( "%s %s", status, name ), L[class], level, zone, notes, rank, i )
                end
        end
        f.GuildBlock.text = IsInGuild() and format(config.hideGuildTotal and" %s%d"or" %s%d/%d", config.showGuildName and GetGuildInfo"player" and GetGuildInfo"player"..": " or "", #guildEntries-1, GetNumGuildMembers(true) ) or "No Guild"
        if dontShow then dontShow = false else return ShowTablet(self, true) end
end


local ldb = LibStub("LibDataBroker-1.1")

f.GuildBlock = ldb:NewDataObject( "|cFFFFB366Ara|r Guild", {
        type = "data source",
        text = GUILD,
        icon = "Interface\\AddOns\\Ara_Broker_Guild_Friends\\guild.tga",
        OnEnter = f.GuildOnEnter,
        OnLeave = Menu_OnLeave,
        OnClick = function(self, button)
                if button == "LeftButton" then
                        if IsShiftKeyDown() and IsInGuild() then
                                config.showGuildName = not config.showGuildName
                                f.GuildBlock.text = format( " %s%d/%d", config.showGuildName and GetGuildInfo"player"..": " or "", #guildEntries-1, GetNumGuildMembers(true) )
                        elseif IsControlKeyDown() then
                                config.hideHints = not config.hideHints
                                ShowTablet( block, isGuild )
                        else ToggleFriendsFrame(3) end
                elseif button == "RightButton" and IsShiftKeyDown() then
                        config.hideGuildTotal = not config.hideGuildTotal
                        f:GuildOnEnter()
                elseif not InCombatLockdown() then
                        config.showGuildNotes = not config.showGuildNotes
                        ShowTablet( block, true )
                end
        end,
} )

f.FriendsBlock = ldb:NewDataObject( "|cFFFFB366Ara|r Friends", {
        type = "data source",
        text = FRIENDS,
        icon = "Interface\\AddOns\\Ara_Broker_Guild_Friends\\friends.tga",
        OnEnter = f.FriendsOnEnter,
        OnLeave = Menu_OnLeave,
        OnClick = function(self, btn)
                if IsControlKeyDown() then
                        config.hideHints = not config.hideHints
                        ShowTablet( block, isGuild )
                elseif IsShiftKeyDown() then
                        config.hideTotalFriends = not config.hideTotalFriends
                        f:FRIENDLIST_UPDATE()
                elseif IsModifierKeyDown() or btn~="LeftButton" then
                        StaticPopup_Show"ADD_FRIEND"
                else
                        ToggleFriendsFrame(1)
                end
        end,
} )

local orgGuildRoster = GuildRoster
GuildRoster = function(...)
        t.guildTimer = 0
        return orgGuildRoster(...)
end

local orgShowFriends = ShowFriends
ShowFriends = function(...)
        t.friendTimer = 0
        return orgShowFriends(...)
end

local function OnUpdate( self, elapsed )
        t.guildTimer = t.guildTimer + elapsed
        if t.guildTimer > 15 then
                if IsInGuild() then GuildRoster() else t.guildTimer = 0 end
        end
        t.friendTimer = t.friendTimer + elapsed
        if t.friendTimer > 15 then ShowFriends() end
end

function f:ADDON_LOADED( event, addon )
        if addon ~= "Ara_Broker_Guild_Friends" then return else f:UnregisterEvent(event) end
        AraBrokerGuildFriendsDB = AraBrokerGuildFriendsDB or defaultConfig
        config = AraBrokerGuildFriendsDB
        for k, v in next, defaultConfig do if config[k]==nil then config[k]=v end end
        if config.reloading then config.reloading = nil else SortGuildRoster(config.sortType) if config.sortDESC then SortGuildRoster(config.sortType) end end

        for eng, loc in next, LOCALIZED_CLASS_NAMES_MALE do
                L[loc] = eng
        end
        for eng, loc in next, LOCALIZED_CLASS_NAMES_FEMALE do
                L[loc] = eng
        end

        tip = GameTooltip
        horde = UnitFactionGroup"player" == "Horde"
        if Skinner then
                Skinner:applySkin(self)
        else
                f:SetBackdrop( { bgFile="Interface\\Buttons\\WHITE8X8", edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",
                        tile=true, tileSize=12, edgeSize=12, insets = { left=2, right=2, top=2, bottom=2 } } )
                f:SetBackdropColor( 0, 0, 0, .75 )
        end
        f.red, f.green, f.blue, f.alpha = f:GetBackdropColor()
        f:SetFrameStrata"TOOLTIP"
        f:SetClampedToScreen(true)
        f:EnableMouse(true)

        t:SetScript( "OnUpdate", OnUpdate )
        f:SetScript( "OnEnter", Menu_OnEnter )
        f:SetScript( "OnLeave", Menu_OnLeave )

        f:RegisterEvent"GUILD_ROSTER_UPDATE"
        f:RegisterEvent"PLAYER_GUILD_UPDATE"
        f:RegisterEvent"FRIENDLIST_UPDATE"
        f:RegisterEvent"CHAT_MSG_SYSTEM"

        slider = CreateFrame("Slider", nil, f)
        slider:SetWidth(16)
        slider:SetThumbTexture("Interface\\Buttons\\UI-SliderBar-Button-Horizontal")
        slider:SetBackdrop( {
                bgFile = "Interface\\Buttons\\UI-SliderBar-Background",
                edgeFile = "Interface\\Buttons\\UI-SliderBar-Border",
                edgeSize = 8, tile = true, tileSize = 8,
                insets = {left = 3, right = 3, top = 6, bottom = 6}
        } )
        slider:SetValueStep(1)
        slider:SetScript( "OnLeave", Menu_OnLeave )
        slider:SetScript( "OnValueChanged", function(self, value)
                if hasSlider then
                        sliderValue = value
                        if block then UpdateScrollButtons(MAX_ENTRIES) end
                end
        end )

        ShowFriends()
        if IsInGuild(buttons[0]) then GuildRoster() else t.guildTimer = 0 end
        f:GuildOnEnter()
        f.ADDON_LOADED = nil
end

f:RegisterEvent"ADDON_LOADED"
f:SetScript( "OnEvent", function(self, event, ...) return self[event](self, event, ...) end )
f:SetScript( "OnShow", function(self) f:SetBackdropColor( InCombatLockdown() and f.red+.25 or f.red, f.green, f.blue, f.alpha) end )

Compare with Previous | Blame