Files
coa-altoholic/Altoholic/Frames/Calendar.lua
T
florian.berthold d440c62a73
release / release (push) Successful in 5s
coa.19: revert the window enlargement (restore original 832x447 frame)
The coa.12 enlargement couldn't extend WoW's fixed AuctionFrame art cleanly -> fragmented
background + broken scrollbar. Reverted to the original clean frame (14 rows, intact art,
working scrollbar). All crash fixes, Skills professions cap, login-scan, class icons, and
the char-view profession wrap are preserved (they live in different lines/files).
2026-05-29 20:23:40 +02:00

1022 lines
35 KiB
Lua

local L = LibStub("AceLocale-3.0"):GetLocale("Altoholic")
local WHITE = "|cFFFFFFFF"
local TEAL = "|cFF00FF9A"
local GREEN = "|cFF00FF00"
-- Weekday constants
local CALENDAR_WEEKDAY_NORMALIZED_TEX_LEFT = 0.0;
local CALENDAR_WEEKDAY_NORMALIZED_TEX_TOP = 180 / 256;
local CALENDAR_WEEKDAY_NORMALIZED_TEX_WIDTH = 90 / 256 - 0.001; -- fudge factor to prevent texture seams
local CALENDAR_WEEKDAY_NORMALIZED_TEX_HEIGHT = 28 / 256 - 0.001; -- fudge factor to prevent texture seams
local CALENDAR_MAX_DAYS_PER_MONTH = 42; -- 6 weeks
local CALENDAR_DAYBUTTON_NORMALIZED_TEX_WIDTH = 90 / 256 - 0.001; -- fudge factor to prevent texture seams
local CALENDAR_DAYBUTTON_NORMALIZED_TEX_HEIGHT = 90 / 256 - 0.001; -- fudge factor to prevent texture seams
local CALENDAR_DAYBUTTON_HIGHLIGHT_ALPHA = 0.5;
local DAY_BUTTON = "AltoCalendarDayButton"
local CALENDAR_FIRST_WEEKDAY = 1
-- 1 = Sunday, recreated locally to avoid the problem caused by the calendar addon not being loaded at startup.
-- On an EU client, CALENDAR_FIRST_WEEKDAY = 1 when the game is loaded, but becomes 2 as soon as the calendar is launched.
-- So default it to 1, and add an option to select Monday as 1st day of the week instead. If need be, use a slider.
-- Although the calendar is LoD, avoid it.
local CALENDAR_MONTH_NAMES = { CalendarGetMonthNames() };
local CALENDAR_WEEKDAY_NAMES = { CalendarGetWeekdayNames() };
local CALENDAR_FULLDATE_MONTH_NAMES = {
-- month names show up differently for full date displays in some languages
FULLDATE_MONTH_JANUARY,
FULLDATE_MONTH_FEBRUARY,
FULLDATE_MONTH_MARCH,
FULLDATE_MONTH_APRIL,
FULLDATE_MONTH_MAY,
FULLDATE_MONTH_JUNE,
FULLDATE_MONTH_JULY,
FULLDATE_MONTH_AUGUST,
FULLDATE_MONTH_SEPTEMBER,
FULLDATE_MONTH_OCTOBER,
FULLDATE_MONTH_NOVEMBER,
FULLDATE_MONTH_DECEMBER,
};
local COOLDOWN_LINE = 1
local INSTANCE_LINE = 2
local CALENDAR_LINE = 3
local CONNECTMMO_LINE = 4
local TIMER_LINE = 5
local SHARED_CD_LINE = 6 -- this type is used for shared cooldowns (alchemy, etc..) among others.
Altoholic.Calendar = {}
Altoholic.Calendar.Days = {}
Altoholic.Calendar.Events = {}
local TimeTable = {} -- to pass as an argument to time() see http://lua-users.org/wiki/OsLibraryTutorial for details
local ClockDiff
local lastServerMinute
local function SetClockDiff(elapsed)
-- this function is called every second until the server time changes (track minutes only)
local ServerHour, ServerMinute = GetGameTime()
local continue = true -- keeps the task running
if not lastServerMinute then -- ServerMinute not set ? this is the first pass, save it
lastServerMinute = ServerMinute
else
if lastServerMinute ~= ServerMinute then -- next minute ? do our stuff and stop
local _, ServerMonth, ServerDay, ServerYear = CalendarGetDate()
TimeTable.year = ServerYear
TimeTable.month = ServerMonth
TimeTable.day = ServerDay
TimeTable.hour = ServerHour
TimeTable.min = ServerMinute
TimeTable.sec = 0 -- minute just changed, so second is 0
-- our goal is achieved, we can calculate the difference between server time and local time, in seconds.
-- a positive value means that the server time is ahead of local time.
-- ex: server: 21:05, local 21.02 could lead to something like 180 (or close to it, depending on seconds)
ClockDiff = difftime(time(TimeTable), time())
Altoholic.Calendar.Events:BuildList() -- rebuild the event list to take the known difference into account
-- now that the difference is known, we can check events for warnings, first check should occur right now (hence 0)
Altoholic.Tasks:Add("EventWarning", 0, Altoholic.Calendar.CheckEvents, Altoholic.Calendar)
lastServerMinute = nil
continue = nil
end
end
if continue then
Altoholic.Tasks:Reschedule("SetClockDiff", 1) -- 1 second later
return true
end
end
local function GetClockDiff()
return ClockDiff or 0
end
function Altoholic.Calendar:Init()
-- by default, the week starts on Sunday, adjust CALENDAR_FIRST_WEEKDAY if necessary
if Altoholic.Options:Get("WeekStartsMonday") == 1 then
CALENDAR_FIRST_WEEKDAY = 2
end
local _, thisMonth, _, thisYear = CalendarGetDate();
CalendarSetAbsMonth(thisMonth, thisYear);
-- only register after setting the current month
Altoholic:RegisterEvent("CALENDAR_UPDATE_EVENT_LIST", Altoholic.Calendar.OnUpdate)
local band = bit.band;
-- initialize weekdays
for i = 1, 7 do
local bg = _G["AltoholicFrameCalendarWeekday"..i.."Background"]
local left = (band(i, 1) * CALENDAR_WEEKDAY_NORMALIZED_TEX_WIDTH) + CALENDAR_WEEKDAY_NORMALIZED_TEX_LEFT; -- mod(index, 2) * width
local right = left + CALENDAR_WEEKDAY_NORMALIZED_TEX_WIDTH;
local top = CALENDAR_WEEKDAY_NORMALIZED_TEX_TOP;
local bottom = top + CALENDAR_WEEKDAY_NORMALIZED_TEX_HEIGHT;
bg:SetTexCoord(left, right, top, bottom);
end
-- initialize day buttons
for i = 1, CALENDAR_MAX_DAYS_PER_MONTH do
CreateFrame("Button", DAY_BUTTON..i, AltoholicFrameCalendar, "AltoCalendarDayButtonTemplate");
self.Days:Init(i)
end
self.Events:BuildList()
self.Events:InitialExpiryCheck()
-- determine the difference between local time and server time
Altoholic.Tasks:Add("SetClockDiff", 0, SetClockDiff, Altoholic.Calendar)
end
local function GetWeekdayIndex(index)
-- GetWeekdayIndex takes an index in the range [1, n] and maps it to a weekday starting
-- at CALENDAR_FIRST_WEEKDAY. For example,
-- CALENDAR_FIRST_WEEKDAY = 1 => [SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY]
-- CALENDAR_FIRST_WEEKDAY = 2 => [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
-- CALENDAR_FIRST_WEEKDAY = 6 => [FRIDAY, SATURDAY, SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY]
-- the expanded form for the left input to mod() is:
-- (index - 1) + (CALENDAR_FIRST_WEEKDAY - 1)
-- why the - 1 and then + 1 before return? because lua has 1-based indexes! awesome!
return mod(index - 2 + CALENDAR_FIRST_WEEKDAY, 7) + 1;
end
local function GetFullDate(weekday, month, day, year)
local weekdayName = CALENDAR_WEEKDAY_NAMES[weekday];
local monthName = CALENDAR_FULLDATE_MONTH_NAMES[month];
return weekdayName, monthName, day, year, month;
end
local function GetDay(fullday)
-- full day = a date as YYYY-MM-DD
-- this function is actually different than the one in Blizzard_Calendar.lua, since weekday can't necessarily be determined from a UI button
local refDate = {} -- let's use the 1st of current month as reference date
local refMonthFirstDay
refDate.month, refDate.year, _, refMonthFirstDay = CalendarGetMonth()
refDate.day = 1
local t = {}
local year, month, day = strsplit("-", fullday)
t.year = tonumber(year)
t.month = tonumber(month)
t.day = tonumber(day)
local numDays = floor(difftime(time(t), time(refDate)) / 86400)
local weekday = mod(refMonthFirstDay + numDays, 7)
-- at this point, weekday might be negative or 0, simply add 7 to keep it in the proper range
weekday = (weekday <= 0) and (weekday+7) or weekday
return t.year, t.month, t.day, weekday
end
local function GetEventExpiry(event)
-- returns the number of seconds in which a calendar event expires
assert(type(event) == "table")
local year, month, day = strsplit("-", event.eventDate)
local hour, minute = strsplit(":", event.eventTime)
TimeTable.year = tonumber(year)
TimeTable.month = tonumber(month)
TimeTable.day = tonumber(day)
TimeTable.hour = tonumber(hour)
TimeTable.min = tonumber(minute)
return difftime(time(TimeTable), time() + GetClockDiff()) -- in seconds
end
local TimerThresholds = { 30, 15, 10, 5, 4, 3, 2, 1 }
local CalendarEventTypes = {
[COOLDOWN_LINE] = {
GetReadyNowWarning = function(self, event)
local name = DataStore:GetCraftCooldownInfo(event.source, event.parentID)
return format(L["%s is now ready (%s on %s)"], name, event.char, event.realm)
end,
GetReadySoonWarning = function(self, event, minutes)
local name = DataStore:GetCraftCooldownInfo(event.source, event.parentID)
return format(L["%s will be ready in %d minutes (%s on %s)"], name, minutes, event.char, event.realm)
end,
GetInfo = function(self, event)
local title, expiresIn = DataStore:GetCraftCooldownInfo(event.source, event.parentID)
return title, format("%s %s", COOLDOWN_REMAINING, Altoholic:GetTimeString(expiresIn))
end,
},
[INSTANCE_LINE] = {
GetReadyNowWarning = function(self, event)
local instance = strsplit("|", event.parentID)
return format(L["%s is now unlocked (%s on %s)"], instance, event.char, event.realm)
end,
GetReadySoonWarning = function(self, event, minutes)
local instance = strsplit("|", event.parentID)
return format(L["%s will be unlocked in %d minutes (%s on %s)"], instance, minutes, event.char, event.realm)
end,
GetInfo = function(self, event)
-- title gets the instance name, desc gets the raid id
local instanceName, raidID = strsplit("|", event.parentID)
-- CALENDAR_EVENTNAME_FORMAT_RAID_LOCKOUT = "%s Unlocks"; -- %s = Raid Name
return instanceName, format("%s%s\nID: %s%s", WHITE, format(CALENDAR_EVENTNAME_FORMAT_RAID_LOCKOUT, instanceName), GREEN, raidID)
end,
},
[CALENDAR_LINE] = {
GetReadyNowWarning = function(self, event)
local c = Altoholic:GetCharacterTable(event.char, event.realm)
local _, _, title = strsplit("|", c.Calendar[event.parentID])
return format(CALENDAR_EVENTNAME_FORMAT_START .. " (%s/%s)", title, event.char, event.realm)
end,
GetReadySoonWarning = function(self, event, minutes)
local c = Altoholic:GetCharacterTable(event.char, event.realm)
local _, _, title = strsplit("|", c.Calendar[event.parentID])
return format(L["%s starts in %d minutes (%s on %s)"], title, minutes, event.char, event.realm)
end,
GetInfo = function(self, event)
local c = Altoholic:GetCharacterTable(event.char, event.realm)
local _, _, title, eventType, inviteStatus = strsplit("|", c.Calendar[event.parentID])
inviteStatus = tonumber(inviteStatus)
local desc
if type(inviteStatus) == "number" and (inviteStatus > 1) and (inviteStatus < 8) then
local StatusText = {
CALENDAR_STATUS_INVITED, -- CALENDAR_INVITESTATUS_INVITED = 1
CALENDAR_STATUS_ACCEPTED, -- CALENDAR_INVITESTATUS_ACCEPTED = 2
CALENDAR_STATUS_DECLINED, -- CALENDAR_INVITESTATUS_DECLINED = 3
CALENDAR_STATUS_CONFIRMED, -- CALENDAR_INVITESTATUS_CONFIRMED = 4
CALENDAR_STATUS_OUT, -- CALENDAR_INVITESTATUS_OUT = 5
CALENDAR_STATUS_STANDBY, -- CALENDAR_INVITESTATUS_STANDBY = 6
CALENDAR_STATUS_SIGNEDUP, -- CALENDAR_INVITESTATUS_SIGNEDUP = 7
CALENDAR_STATUS_NOT_SIGNEDUP -- CALENDAR_INVITESTATUS_NOT_SIGNEDUP = 8
}
desc = format("%s: %s", STATUS, WHITE..StatusText[inviteStatus])
else
desc = format("%s", STATUS)
end
return title, desc
end,
},
[CONNECTMMO_LINE] = {
GetReadyNowWarning = function(self, event)
local c = Altoholic:GetCharacterTable(event.char, event.realm)
local _, _, title = strsplit("|", c.ConnectMMO[event.parentID])
return format(CALENDAR_EVENTNAME_FORMAT_START .. " (%s/%s)", title, event.char, event.realm)
end,
GetReadySoonWarning = function(self, event, minutes)
local c = Altoholic:GetCharacterTable(event.char, event.realm)
local _, _, title = strsplit("|", c.ConnectMMO[event.parentID])
return format(L["%s starts in %d minutes (%s on %s)"], title, minutes, event.char, event.realm)
end,
GetInfo = function(self, event)
local c = Altoholic:GetCharacterTable(event.char, event.realm)
local _, _, title, eventType, eventDesc, attendees = strsplit("|", c.ConnectMMO[event.parentID])
local numPlayers, minLvl, maxLvl, privateToFriends, privateToGuild = strsplit(",", eventDesc)
local eventTable = {}
table.insert(eventTable, WHITE .. format(L["Number of players: %s"], GREEN .. numPlayers))
table.insert(eventTable, WHITE .. format(L["Minimum Level: %s"], GREEN .. minLvl))
table.insert(eventTable, WHITE .. format(L["Maximum Level: %s"], GREEN .. maxLvl))
table.insert(eventTable, WHITE .. format(L["Private to friends: %s"], GREEN .. (tonumber(privateToFriends) == 1 and YES or NO)))
table.insert(eventTable, WHITE .. format(L["Private to guild: %s"], GREEN .. (tonumber(privateToGuild) == 1 and YES or NO)))
local attendeesTable = { strsplit(",", attendees) }
if #attendeesTable > 0 then
table.insert(eventTable, "")
table.insert(eventTable, WHITE..L["Attendees: "].."|r")
for _, name in pairs(attendeesTable) do
table.insert(eventTable, " " .. name )
end
table.insert(eventTable, "")
table.insert(eventTable, GREEN .. L["Left-click to invite attendees"])
end
return title, table.concat(eventTable, "\n")
end,
},
[TIMER_LINE] = {
GetReadyNowWarning = function(self, event)
local c = Altoholic:GetCharacterTable(event.char, event.realm)
local item = strsplit("|", c.Timers[event.parentID])
return format(L["%s is now ready (%s on %s)"], item, event.char, event.realm)
end,
GetReadySoonWarning = function(self, event, minutes)
local c = Altoholic:GetCharacterTable(event.char, event.realm)
local item = strsplit("|", c.Timers[event.parentID])
return format(L["%s will be ready in %d minutes (%s on %s)"], item, minutes, event.char, event.realm)
end,
GetInfo = function(self, event)
local c = Altoholic:GetCharacterTable(event.char, event.realm)
local title = strsplit("|", c.Timers[event.parentID])
local expiresIn = GetEventExpiry(event)
return title, format("%s %s", COOLDOWN_REMAINING, Altoholic:GetTimeString(expiresIn))
end,
},
[SHARED_CD_LINE] = {
GetReadyNowWarning = function(self, event)
return format(L["%s is now ready (%s on %s)"], event.source, event.char, event.realm)
end,
GetReadySoonWarning = function(self, event, minutes)
return format(L["%s will be ready in %d minutes (%s on %s)"], event.source, minutes, event.char, event.realm)
end,
GetInfo = function(self, event)
local expiresIn = GetEventExpiry(event)
return event.source, format("%s %s", COOLDOWN_REMAINING, Altoholic:GetTimeString(expiresIn))
end,
},
}
local function ShowExpiryWarning(index, minutes)
local event = Altoholic.Calendar.Events:Get(index)
local CalendarEvent = CalendarEventTypes[event.eventType]
local warning
if minutes == 0 then
warning = CalendarEvent:GetReadyNowWarning(event)
else
warning = CalendarEvent:GetReadySoonWarning(event, minutes)
end
if not warning then return end
-- print instead of dialog box if player is in combat
if Altoholic.Options:Get("WarningDialogBox") == 1 and not UnitAffectingCombat("player") then
AltoMsgBox.ButtonHandler = Altoholic.Calendar.WarningButtonHandler
AltoMsgBox_Text:SetText(format("%s\n%s", WHITE..warning, L["Do you want to open Altoholic's calendar for details ?"]))
AltoMsgBox:Show()
else
Altoholic:Print(warning)
end
end
local function ClearExpiredEvents()
local DS = DataStore
for realm in pairs(DS:GetRealms()) do
for characterName, character in pairs(DS:GetCharacters(realm)) do
-- Profession Cooldowns
local professions = DS:GetProfessions(character)
if professions then
for professionName, profession in pairs(professions) do
DS:ClearExpiredCooldowns(profession)
end
end
local c = Altoholic:GetCharacterTable(characterName, realm)
-- Saved Instances
for k, v in pairs(c.SavedInstance) do
local reset, lastCheck = strsplit("|", v)
reset = tonumber(reset)
lastCheck = tonumber(lastCheck)
if reset - (time() - lastCheck) <= 0 then -- has expired
c.SavedInstance[k] = nil
end
end
-- Other timers (like mysterious egg, etc..)
for k, v in pairs(c.Timers) do
local _, lastCheck, duration = strsplit("|", v)
lastCheck = tonumber(lastCheck)
duration = tonumber(duration)
local expires = duration + lastCheck + GetClockDiff()
if (expires - time()) <= 0 then
c.Timers[k] = nil
end
end
end
end
end
local function IsNumberInString(number, str)
-- ex: with str = "15|10|3" returns true if value is in this string
for v in str:gmatch("(%d+)") do
if tonumber(v) == number then
return true
end
end
end
local WARNING_TYPE_PROFESSION_CD = 1
local WARNING_TYPE_DUNGEON_RESET = 2
local WARNING_TYPE_CALENDAR_EVENT = 3
local WARNING_TYPE_ITEM_TIMER = 4
local EventToWarningType = {
[COOLDOWN_LINE] = WARNING_TYPE_PROFESSION_CD,
[INSTANCE_LINE] = WARNING_TYPE_DUNGEON_RESET,
[CALENDAR_LINE] = WARNING_TYPE_CALENDAR_EVENT,
[CONNECTMMO_LINE] = WARNING_TYPE_CALENDAR_EVENT,
[TIMER_LINE] = WARNING_TYPE_ITEM_TIMER,
[SHARED_CD_LINE] = WARNING_TYPE_PROFESSION_CD,
}
function Altoholic.Calendar:CheckEvents(elapsed)
if Altoholic.Options:Get("DisableWarnings") == 1 then -- warnings disabled ? do nothing
Altoholic.Tasks:Reschedule("EventWarning", 60)
return true
end
-- called every 60 seconds
local hasEventExpired
for k, v in pairs(Altoholic.Calendar.Events.List) do
local numMin = floor(GetEventExpiry(v) / 60)
if numMin < 0 then
hasEventExpired = true -- at least one event has expired
elseif numMin == 0 then
ShowExpiryWarning(k, 0)
hasEventExpired = true -- at least one event has expired
elseif numMin <= 30 then
local warnings = Altoholic.Options:Get("WarningType"..EventToWarningType[v.eventType]) -- Gets something like "15|5|1"
for _, threshold in pairs(TimerThresholds) do
if threshold == numMin then -- if snooze is allowed for this value
if IsNumberInString(threshold, warnings) then
ShowExpiryWarning(k, numMin)
end
break
elseif threshold < numMin then -- save some cpu cycles, exit if threshold too low
break
end
end
end
end
if hasEventExpired then -- if at least one event has expired, rebuild the list & refresh
ClearExpiredEvents()
Altoholic.Calendar.Events:BuildList()
Altoholic.Tabs.Summary:Refresh()
end
-- the task was executed right after the minute changed server side, so reschedule exactly 60 seconds later
Altoholic.Tasks:Reschedule("EventWarning", 60)
return true
end
function Altoholic.Calendar:WarningButtonHandler(button)
AltoMsgBox.ButtonHandler = nil -- prevent any other call to msgbox from coming back here
if not button then return end
Altoholic:ToggleUI()
Altoholic.Tabs.Summary:MenuItem_OnClick(8)
end
function Altoholic.Calendar:SetFirstDayOfWeek(day)
CALENDAR_FIRST_WEEKDAY = day
end
function Altoholic.Calendar:Update()
-- taken from CalendarFrame_Update() in Blizzard_Calendar.lua, adjusted for my needs.
local self = Altoholic.Calendar
self.Events:BuildList() -- force a rebuild when updating the view. In some rare cases, the list was not correctly updated. Temporary workaround 26/04/2010
local presentWeekday, presentMonth, presentDay, presentYear = CalendarGetDate();
local prevMonth, prevYear, prevNumDays = CalendarGetMonth(-1);
local nextMonth, nextYear, nextNumDays = CalendarGetMonth(1);
local month, year, numDays, firstWeekday = CalendarGetMonth();
-- set title
AltoholicFrameCalendar_MonthYear:SetText(CALENDAR_MONTH_NAMES[month] .. " ".. year)
-- initialize weekdays
for i = 1, 7 do
_G["AltoholicFrameCalendarWeekday"..i.."Name"]:SetText(string.sub(CALENDAR_WEEKDAY_NAMES[GetWeekdayIndex(i)], 1, 3));
end
local buttonIndex = 1;
local isDarkened = true
local day;
-- set the previous month's days before the first day of the week
local viewablePrevMonthDays = mod((firstWeekday - CALENDAR_FIRST_WEEKDAY - 1) + 7, 7);
day = prevNumDays - viewablePrevMonthDays;
while ( GetWeekdayIndex(buttonIndex) ~= firstWeekday ) do
self.Days:Update(buttonIndex, day, prevMonth, prevYear, isDarkened)
day = day + 1;
buttonIndex = buttonIndex + 1;
end
-- set the days of this month
day = 1;
isDarkened = false
while ( day <= numDays ) do
self.Days:Update(buttonIndex, day, month, year, isDarkened)
day = day + 1;
buttonIndex = buttonIndex + 1;
end
-- set the first days of the next month
day = 1;
isDarkened = true
while ( buttonIndex <= CALENDAR_MAX_DAYS_PER_MONTH ) do
self.Days:Update(buttonIndex, day, nextMonth, nextYear, isDarkened)
day = day + 1;
buttonIndex = buttonIndex + 1;
end
self.Events:Update()
end
function Altoholic.Calendar.Days:Init(index)
local button = _G[DAY_BUTTON..index]
button:SetID(index)
-- set anchors
button:ClearAllPoints();
if ( index == 1 ) then
button:SetPoint("TOPLEFT", AltoholicFrameCalendar, "TOPLEFT", 285, -1);
elseif ( mod(index, 7) == 1 ) then
button:SetPoint("TOPLEFT", _G[DAY_BUTTON..(index - 7)], "BOTTOMLEFT", 0, 0);
else
button:SetPoint("TOPLEFT", _G[DAY_BUTTON..(index - 1)], "TOPRIGHT", 0, 0);
end
-- set the normal texture to be the background
local tex = button:GetNormalTexture();
tex:SetDrawLayer("BACKGROUND");
local texLeft = random(0,1) * CALENDAR_DAYBUTTON_NORMALIZED_TEX_WIDTH;
local texRight = texLeft + CALENDAR_DAYBUTTON_NORMALIZED_TEX_WIDTH;
local texTop = random(0,1) * CALENDAR_DAYBUTTON_NORMALIZED_TEX_HEIGHT;
local texBottom = texTop + CALENDAR_DAYBUTTON_NORMALIZED_TEX_HEIGHT;
tex:SetTexCoord(texLeft, texRight, texTop, texBottom);
-- adjust the highlight texture layer
tex = button:GetHighlightTexture();
tex:SetAlpha(CALENDAR_DAYBUTTON_HIGHLIGHT_ALPHA);
end
function Altoholic.Calendar.Days:Update(index, day, month, year, isDarkened)
local button = _G[DAY_BUTTON..index]
local buttonName = button:GetName();
button.day = day
button.month = month
button.year = year
-- set date
local dateLabel = _G[buttonName.."Date"];
local tex = button:GetNormalTexture();
dateLabel:SetText(day);
if isDarkened then
tex:SetVertexColor(0.4, 0.4, 0.4)
else
tex:SetVertexColor(1.0, 1.0, 1.0)
end
-- set count
local countLabel = _G[buttonName.."Count"];
local count = Altoholic.Calendar.Events:GetNum(year, month, day)
if count == 0 then
countLabel:Hide()
else
countLabel:SetText(count)
countLabel:Show()
end
end
function Altoholic.Calendar.Days:OnClick(self, button)
local Events = Altoholic.Calendar.Events
local count = Events:GetNum(self.year, self.month, self.day)
if count == 0 then -- no events on that day ? exit
return
end
local index = Events:GetIndex(self.year, self.month, self.day)
if index then
Events:SetOffset(index - 1) -- if the date is the 4th line, offset is 3
Events:Update()
end
end
function Altoholic.Calendar.Days:OnEnter(self)
local Events = Altoholic.Calendar.Events
local count = Events:GetNum(self.year, self.month, self.day)
if count == 0 then -- no events on that day ? exit
return
end
AltoTooltip:SetOwner(self, "ANCHOR_LEFT");
AltoTooltip:ClearLines();
local eventDate = format("%04d-%02d-%02d", self.year, self.month, self.day)
local weekday = GetWeekdayIndex(mod(self:GetID(), 7))
weekday = (weekday == 0) and 7 or weekday
AltoTooltip:AddLine(TEAL..format(FULLDATE, GetFullDate(weekday, self.month, self.day, self.year)));
for k, v in pairs(Events.List) do
if v.eventDate == eventDate then
local char, eventTime, title = Events:GetInfo(k)
AltoTooltip:AddDoubleLine(format("%s %s", WHITE..eventTime, char), title);
end
end
AltoTooltip:Show();
end
local EVENT_DATE = 1
local EVENT_INFO = 2
function Altoholic.Calendar.Events:InitialExpiryCheck()
for index, event in pairs(self.List) do -- browse all events
local expiresIn = GetEventExpiry(event)
if expiresIn < 0 then -- if the event has expired
event.markForDeletion = true -- .. mark it for deletion (no table.remove in this pass, to avoid messing up indexes)
ShowExpiryWarning(index, 0) -- .. and do the appropriate warning
end
end
for i = #self.List, 1, -1 do -- browse the event table backwards
if self.List[i].markForDeletion then -- erase marked events
table.remove(self.List, i)
end
end
ClearExpiredEvents() -- quick fix, it seems that for some reason, egg timers did not get cleared as they should without this call
self.InitialExpiryCheck = nil -- kill self, function won't be called again
end
function Altoholic.Calendar.Events:BuildView()
self.view = self.view or {}
wipe(self.view)
-- the following list of events : 10/05, 10/05, 12/05, 14/05, 14/05
-- turns into this view :
-- "10/05"
-- event 1
-- event 2
-- "12/05"
-- event 1
-- "14/05"
-- event 1
-- event 2
local eventDate = ""
for k, v in pairs(self.List) do
if eventDate ~= v.eventDate then
table.insert(self.view, { linetype = EVENT_DATE, eventDate = v.eventDate })
eventDate = v.eventDate
end
table.insert(self.view, { linetype = EVENT_INFO, parentID = k })
end
end
function Altoholic.Calendar.Events:BuildList()
self.List = self.List or {}
wipe(self.List)
local ClockDiff = GetClockDiff()
local DS = DataStore
for realm in pairs(DS:GetRealms()) do
for characterName, character in pairs(DS:GetCharacters(realm)) do
-- add all timers, even expired ones. Expiries will be handled elsewhere.
-- Profession Cooldowns
local professions = DS:GetProfessions(character)
if professions then
for professionName, profession in pairs(professions) do
local supportsSharedCD
if professionName == GetSpellInfo(2259) or -- alchemy
professionName == GetSpellInfo(3908) or -- tailoring
professionName == GetSpellInfo(2575) then -- mining
supportsSharedCD = true -- current profession supports shared cooldowns
end
if supportsSharedCD then
local sharedCDFound -- is there a shared cd for this profession ?
for i = 1, DS:GetNumActiveCooldowns(profession) do
local name, _, reset, lastCheck = DS:GetCraftCooldownInfo(profession, i)
local expires = reset + lastCheck + ClockDiff
if not sharedCDFound then
sharedCDFound = true
self:Add(SHARED_CD_LINE, date("%Y-%m-%d",expires), date("%H:%M",expires), characterName, realm, nil, professionName)
end
end
else
for i = 1, DS:GetNumActiveCooldowns(profession) do
local name, _, reset, lastCheck = DS:GetCraftCooldownInfo(profession, i)
local expires = reset + lastCheck + ClockDiff
self:Add(COOLDOWN_LINE, date("%Y-%m-%d",expires), date("%H:%M",expires), characterName, realm, i, profession)
end
end
end
end
local c = Altoholic:GetCharacterTable(characterName, realm)
-- Saved Instances
for k, v in pairs(c.SavedInstance) do
local reset, lastCheck = strsplit("|", v)
reset = tonumber(reset)
lastCheck = tonumber(lastCheck)
local expires = reset + lastCheck + ClockDiff
self:Add(INSTANCE_LINE, date("%Y-%m-%d",expires), date("%H:%M",expires), characterName, realm, k)
end
-- Calendar Events
for k, v in pairs(c.Calendar) do
local eventDate, eventTime = strsplit("|", v)
-- TODO: do not add declined invitations
self:Add(CALENDAR_LINE, eventDate, eventTime, characterName, realm, k)
end
-- ConnectMMO events
for k, v in pairs(c.ConnectMMO) do
local eventDate, eventTime = strsplit("|", v)
self:Add(CONNECTMMO_LINE, eventDate, eventTime, characterName, realm, k)
end
-- Other timers (like mysterious egg, etc..)
for k, v in pairs(c.Timers) do
local item, lastCheck, duration = strsplit("|", v)
lastCheck = tonumber(lastCheck)
duration = tonumber(duration)
local expires = duration + lastCheck + ClockDiff
self:Add(TIMER_LINE, date("%Y-%m-%d",expires), date("%H:%M",expires), characterName, realm, k)
end
end
end
-- sort by time
self:Sort()
self:BuildView()
end
local NUM_EVENTLINES = 14
function Altoholic.Calendar.Events:Update()
local self = Altoholic.Calendar.Events
local VisibleLines = NUM_EVENTLINES
local frame = "AltoholicFrameCalendar"
local entry = frame.."Entry"
local offset = FauxScrollFrame_GetOffset( _G[ frame.."ScrollFrame" ] );
for i=1, VisibleLines do
local line = i + offset
if line <= #self.view then
local s = self.view[line]
if s.linetype == EVENT_DATE then
local year, month, day, weekday = GetDay(s.eventDate)
_G[ entry..i.."Date" ]:SetText(format(FULLDATE, GetFullDate(weekday, month, day, year)))
_G[ entry..i.."Date" ]:Show()
_G[ entry..i.."Hour" ]:Hide()
_G[ entry..i.."Character" ]:Hide()
_G[ entry..i.."Title" ]:Hide()
_G[ entry..i.."_Background"]:Show()
elseif s.linetype == EVENT_INFO then
local char, eventTime, title = self:GetInfo(s.parentID)
_G[ entry..i.."Hour" ]:SetText(eventTime)
_G[ entry..i.."Character" ]:SetText(char)
_G[ entry..i.."Title" ]:SetText(title)
_G[ entry..i.."Hour" ]:Show()
_G[ entry..i.."Character" ]:Show()
_G[ entry..i.."Title" ]:Show()
_G[ entry..i.."Date" ]:Hide()
_G[ entry..i.."_Background"]:Hide()
end
_G[ entry..i ]:SetID(line)
_G[ entry..i ]:Show()
else
_G[ entry..i ]:Hide()
end
end
local last = (#self.view < VisibleLines) and VisibleLines or #self.view
FauxScrollFrame_Update( _G[ frame.."ScrollFrame" ], last, VisibleLines, 18);
end
function Altoholic.Calendar.Events:Get(index)
return self.List[index]
end
function Altoholic.Calendar.Events:GetInfo(index)
local event = self.List[index] -- dereference event
if not event then return end
local DS = DataStore
local character = DS:GetCharacter(event.char, event.realm)
local char = DS:GetColoredCharacterName(character)
if event.realm ~= GetRealmName() then -- different realm ?
char = format("%s %s(%s)", char, GREEN, event.realm)
end
local CalendarEvent = CalendarEventTypes[event.eventType]
local title, desc = CalendarEvent:GetInfo(event)
return char, event.eventTime, title, desc
end
function Altoholic.Calendar.Events:Sort()
table.sort(self.List, function(a, b)
if (a.eventDate ~= b.eventDate) then -- sort by date first ..
return a.eventDate < b.eventDate
elseif (a.eventTime ~= b.eventTime) then -- .. then by hour
return a.eventTime < b.eventTime
elseif (a.char ~= b.char) then -- .. then by alt
return a.char < b.char
end
end)
end
function Altoholic.Calendar.Events:Add(eventType, eventDate, eventTime, char, realm, index, externalTable)
table.insert(self.List, {
eventType = eventType,
eventDate = eventDate,
eventTime = eventTime,
char = char,
realm = realm,
parentID = index,
source = externalTable})
end
function Altoholic.Calendar.Events:GetNum(year, month, day)
local eventDate = format("%04d-%02d-%02d", year, month, day)
local count = 0
for k, v in pairs(self.List) do
if v.eventDate == eventDate then
count = count + 1
end
end
return count
end
function Altoholic.Calendar.Events:OnEnter(frame)
local self = Altoholic.Calendar.Events
local s = self.view[frame:GetID()]
if not s or s.linetype == EVENT_DATE then return end
AltoTooltip:SetOwner(frame, "ANCHOR_RIGHT");
AltoTooltip:ClearLines();
-- local eventDate = format("%04d-%02d-%02d", self.year, self.month, self.day)
-- local weekday = GetWeekdayIndex(mod(self:GetID(), 7))
-- AltoTooltip:AddLine(TEAL..format(FULLDATE, GetFullDate(weekday, self.month, self.day, self.year)));
local char, eventTime, title, desc = self:GetInfo(s.parentID)
AltoTooltip:AddDoubleLine(format("%s %s", WHITE..eventTime, char), title);
if desc then
AltoTooltip:AddLine(" ")
AltoTooltip:AddLine(desc)
end
AltoTooltip:Show();
end
function Altoholic.Calendar.Events:OnClick(frame, button)
-- if an event is left-clicked, try to invite attendees. ConnectMMO events only
local self = Altoholic.Calendar.Events
local s = self.view[frame:GetID()]
if not s or s.linetype == EVENT_DATE then return end -- date line ? exit
local e = self.List[s.parentID] -- dereference event
-- not a connectmmo event ? or wrong realm ? exit
if not e or e.eventType ~= CONNECTMMO_LINE or e.realm ~= GetRealmName() then return end
local c = Altoholic:GetCharacterTable(e.char, e.realm)
if not c then return end -- invalid char table ? exit
local _, _, _, _, _, attendees = strsplit("|", c.ConnectMMO[e.parentID])
-- TODO, add support for raid groups
for _, name in pairs({ strsplit(",", attendees) }) do
if name ~= UnitName("player") then
InviteUnit(name)
end
end
end
function Altoholic.Calendar.Events:GetIndex(year, month, day)
local eventDate = format("%04d-%02d-%02d", year, month, day)
for k, v in pairs(self.view) do
if v.linetype == EVENT_DATE and v.eventDate == eventDate then
-- if the date line is found, return its index
return k
end
end
end
function Altoholic.Calendar.Events:SetOffset(offset)
-- if the view has less entries than can be displayed, don't change the offset
if #self.view <= NUM_EVENTLINES then return end
if offset <= 0 then
offset = 0
elseif offset > (#self.view - NUM_EVENTLINES) then
offset = (#self.view - NUM_EVENTLINES)
end
FauxScrollFrame_SetOffset( AltoholicFrameCalendarScrollFrame, offset )
AltoholicFrameCalendarScrollFrameScrollBar:SetValue(offset * 18)
end
function Altoholic.Calendar:Scan()
if not CalendarFrame then
-- The Calendar addon is LoD, and most functions return nil if the calendar is not loaded, so unless the CalendarFrame is valid, exit right away
return
end
Altoholic:UnregisterEvent("CALENDAR_UPDATE_EVENT_LIST") -- prevent CalendarSetAbsMonth from triggering a scan (= avoid infinite loop)
local currentMonth, currentYear = CalendarGetMonth() -- save the current month
local _, thisMonth, thisDay, thisYear = CalendarGetDate();
CalendarSetAbsMonth(thisMonth, thisYear);
local c = Altoholic.ThisCharacter
wipe(c.Calendar)
-- save this month (from today) + 6 following months
for monthOffset = 0, 6 do
local month, year, numDays = CalendarGetMonth(monthOffset)
local startDay = (monthOffset == 0) and thisDay or 1
for day = startDay, numDays do
for i = 1, CalendarGetNumDayEvents(monthOffset, day) do -- number of events that day ..
-- http://www.wowwiki.com/API_CalendarGetDayEvent
local title, hour, minute, calendarType, _, eventType, _, _, inviteStatus = CalendarGetDayEvent(monthOffset, day, i)
if calendarType ~= "HOLIDAY" and calendarType ~= "RAID_LOCKOUT" and
calendarType ~= "RAID_RESET" and inviteStatus ~= CALENDAR_INVITESTATUS_DECLINED then
-- don't save holiday events, they're the same for all chars, and would be redundant..who wants to see 10 fishing contests every sundays ? =)
table.insert(c.Calendar, format("%s|%s|%s|%d|%d",
format("%04d-%02d-%02d", year, month, day), format("%02d:%02d", hour, minute),
title, eventType, inviteStatus ))
end
end
end
end
CalendarSetAbsMonth(currentMonth, currentYear); -- restore current month
Altoholic:RegisterEvent("CALENDAR_UPDATE_EVENT_LIST", Altoholic.Calendar.OnUpdate)
end
function Altoholic.Calendar:OnUpdate()
local self = Altoholic.Calendar
self:Scan()
self.Events:BuildList()
Altoholic.Tabs.Summary:Refresh()
end
local function ToggleWarningThreshold(self)
local id = self.arg1
local warnings = Altoholic.Options:Get("WarningType"..id) -- Gets something like "15|5|1"
local t = {} -- create a temporary table to store checked values
for v in warnings:gmatch("(%d+)") do
v = tonumber(v)
if v ~= self.value then -- add all values except the one that was clicked
table.insert(t, v)
end
end
if not IsNumberInString(self.value, warnings) then -- if number is not yet in the string, save it (we're checking it, otherwise we're unchecking)
table.insert(t, self.value)
end
Altoholic.Options:Set("WarningType"..id, table.concat(t, "|")) -- Sets something like "15|5|10|1"
end
function Altoholic.Calendar:WarningType_Initialize()
local info = UIDropDownMenu_CreateInfo();
local id = self:GetID()
local warnings = Altoholic.Options:Get("WarningType"..id) -- Gets something like "15|5|1"
for _, threshold in pairs(TimerThresholds) do
info.text = format(D_MINUTES, threshold)
info.value = threshold
info.func = ToggleWarningThreshold
info.checked = IsNumberInString(threshold, warnings)
info.arg1 = id -- save the id of the current option
UIDropDownMenu_AddButton(info, 1);
end
end