commit 59c9f1e5ca394af2974c1ee4878ef830b1a3e180 Author: totalllyswede Date: Mon May 11 21:36:56 2026 -0700 Add files via upload diff --git a/Bindings.xml b/Bindings.xml new file mode 100644 index 0000000..e2e37a3 --- /dev/null +++ b/Bindings.xml @@ -0,0 +1,5 @@ + + + ImprovedGuildWindow_Toggle() + + diff --git a/ImprovedGuildWindow_OC.lua b/ImprovedGuildWindow_OC.lua new file mode 100644 index 0000000..0e34657 --- /dev/null +++ b/ImprovedGuildWindow_OC.lua @@ -0,0 +1,4369 @@ +-- Improved Guild Window for WoW 1.12.1 / Turtle WoW +-- Author: Travis + +local IGW = {} +IGW.VERSION = "3.1" + +-- Global function for keybind (must be defined early) +function ImprovedGuildWindow_Toggle() + if IGW and IGW.ToggleWindow then + IGW:ToggleWindow() + end +end + +local frame +local rosterData = {} +local displayedMembers = {} +local sortColumn = "name" +local sortAscending = true +local filterText = "" +local filterRank = -1 +local showOffline = true +local currentTab = "details" + +-- Configuration: Officer rank threshold +-- rankIndex: 0=Guild Master, 1-9=other ranks +-- Members with rankIndex <= this value are considered "officers" +-- Default: 2 (includes Guild Master and top 2 officer ranks) +local OFFICER_RANK_THRESHOLD = 2 + +-- Timezone lookup table +local TIMEZONES = { + PST = "PST (UTC-8)", + PDT = "PDT (UTC-7)", + MST = "MST (UTC-7)", + MDT = "MDT (UTC-6)", + CST = "CST (UTC-6)", + CDT = "CDT (UTC-5)", + EST = "EST (UTC-5)", + EDT = "EDT (UTC-4)", + GMT = "GMT (UTC+0)", + UTC = "UTC (UTC+0)", + CET = "CET (UTC+1)", + EET = "EET (UTC+2)", + MSK = "MSK (UTC+3)", + IST = "IST (UTC+5:30)", + JST = "JST (UTC+9)", + AEST = "AEST (UTC+10)", + NZST = "NZST (UTC+12)" +} + +-- Dungeon details database +local DUNGEON_DETAILS = { + ["Ragefire Chasm"] = { + location = "Orgrimmar", + faction = "Horde", + description = "A volcanic chasm beneath Orgrimmar inhabited by troggs and cultists.", + bosses = "Taragaman the Hungerer, Jergosh the Invoker", + notable = "Cloth, Fire resist gear" + }, + ["The Deadmines"] = { + location = "Westfall", + faction = "Alliance", + description = "An abandoned mine overrun by the Defias Brotherhood.", + bosses = "Edwin VanCleef, Sneed, Mr. Smite", + notable = "Cruel Barb, Smite's Hammer" + }, + ["Wailing Caverns"] = { + location = "Northern Barrens", + faction = "Neutral", + description = "Twisted caverns corrupted by druids of the Fang.", + bosses = "Lady Anacondra, Lord Pythas, Mutanus", + notable = "Armor of the Fang set, Glowing Lizardscale Cloak" + }, + ["Stockades"] = { + location = "Stormwind City", + faction = "Alliance", + description = "The prison of Stormwind, now controlled by rioting prisoners.", + bosses = "Bazil Thredd, Dextren Ward", + notable = "Prison variety loot" + }, + ["Blackfathom Deeps"] = { + location = "Ashenvale", + faction = "Neutral", + description = "Sunken temple ruins inhabited by naga and corrupted creatures.", + bosses = "Gelihast, Lady Sarevess, Aku'mai", + notable = "Blackfathom Mana Oil, Strike of the Hydra" + }, + ["Dragonmaw Retreat"] = { + location = "Wetlands (Turtle WoW)", + faction = "Neutral", + description = "Former stronghold of the Dragonmaw clan.", + bosses = "Various Dragonmaw leaders", + notable = "Custom Turtle WoW dungeon" + }, + ["SM-Graveyard"] = { + location = "Tirisfal Glades", + faction = "Neutral", + description = "Scarlet Monastery Graveyard wing, haunted by the undead.", + bosses = "Interrogator Vishas, Bloodmage Thalnos", + notable = "Scarlet gear, good XP" + }, + ["Gnomeregan"] = { + location = "Dun Morogh", + faction = "Alliance", + description = "The fallen capital of the gnomes, irradiated and overrun by troggs.", + bosses = "Mekgineer Thermaplugg, Crowd Pummeler 9-60", + notable = "Electrocutioner Leg, Triprunner Dungarees" + }, + ["Crescent Grove"] = { + location = "Feralas (Turtle WoW)", + faction = "Neutral", + description = "Ancient druidic grove with moonkin and satyr.", + bosses = "Various nature guardians", + notable = "Custom Turtle WoW dungeon" + }, + ["SM-Library"] = { + location = "Tirisfal Glades", + faction = "Neutral", + description = "Scarlet Monastery Library wing filled with zealots and undead.", + bosses = "Arcanist Doan, Houndmaster Loksey", + notable = "Illusionary Rod, Hypnotic Blade" + }, + ["Razorfen Kraul"] = { + location = "Southern Barrens", + faction = "Neutral", + description = "Twisting bramble-filled tunnels home to quilboar.", + bosses = "Charlga Razorflank, Agathelos", + notable = "Corpsemaker, Razorfen Spaulders" + }, + ["Stormwrought Ruins"] = { + location = "Azshara (Turtle WoW)", + faction = "Neutral", + description = "Ancient naga ruins crackling with arcane energy.", + bosses = "Various naga champions", + notable = "Custom Turtle WoW dungeon" + }, + ["SM-Armory"] = { + location = "Tirisfal Glades", + faction = "Neutral", + description = "Scarlet Monastery Armory, training ground for Scarlet warriors.", + bosses = "Herod the Bully", + notable = "Ravager, Scarlet Leggings" + }, + ["SM-Cathedral"] = { + location = "Tirisfal Glades", + faction = "Neutral", + description = "Scarlet Monastery Cathedral, seat of the Scarlet High Command.", + bosses = "High Inquisitor Whitemane, Scarlet Commander Mograine", + notable = "Whitemane's Chapeau, Mograine's Might" + }, + ["Uldaman"] = { + location = "Badlands", + faction = "Neutral", + description = "Ancient titan vault filled with troggs, dwarves, and stone constructs.", + bosses = "Archaedas, Ironaya, Galgann Firehammer", + notable = "The Rockpounder, Archaedic Stone" + }, + ["Razorfen Downs"] = { + location = "Southern Barrens", + faction = "Neutral", + description = "Death-knight controlled quilboar city serving the Scourge.", + bosses = "Amnennar the Coldbringer, Tuten'kash", + notable = "Shoulder of the Fallen Crusader, Ebon Vise" + }, + ["Gilneas City"] = { + location = "Gilneas (Turtle WoW)", + faction = "Neutral", + description = "Fallen city overrun by worgen and dark forces.", + bosses = "Various worgen alphas", + notable = "Custom Turtle WoW dungeon" + }, + ["Maraudon"] = { + location = "Desolace", + faction = "Neutral", + description = "Massive underground cavern complex with corrupted earth elementals.", + bosses = "Princess Theradras, Celebras, Noxxion", + notable = "Blackstone Ring, Nature Resist gear" + }, + ["Zul'Farrak"] = { + location = "Tanaris", + faction = "Neutral", + description = "Ancient troll city buried in the desert sands.", + bosses = "Chief Ukorz Sandscalp, Gahz'rilla", + notable = "Sul'thraze, Lifeforce Dirk" + }, + ["Sunken Temple"] = { + location = "Swamp of Sorrows", + faction = "Neutral", + description = "Temple of Atal'Hakkar, sunken ruin of troll worship.", + bosses = "Avatar of Hakkar, Shade of Eranikus", + notable = "Drakestone, Eranikus' Fang" + }, + ["Blackrock Depths"] = { + location = "Blackrock Mountain", + faction = "Neutral", + description = "Massive dark iron dwarf city inside Blackrock Mountain.", + bosses = "Emperor Dagran Thaurissan, General Angerforge", + notable = "Hand of Justice, Ironfoe" + }, + ["Hateforge Quarry"] = { + location = "Blackrock Mountain (Turtle WoW)", + faction = "Neutral", + description = "Dark iron mining operation with enslaved workers.", + bosses = "Various dark iron overseers", + notable = "Custom Turtle WoW dungeon" + }, + ["Dire Maul West"] = { + location = "Feralas", + faction = "Neutral", + description = "Western gardens of the fallen night elf city.", + bosses = "Tendris Warpwood, Immol'thar, Prince Tortheldrin", + notable = "Warpwood Binding, Dreadmist Belt" + }, + ["Dire Maul East"] = { + location = "Feralas", + faction = "Neutral", + description = "Eastern section overrun by demons and imps.", + bosses = "Alzzin the Wildshaper, Isalien", + notable = "Felhide Cap, Energized Chestplate" + }, + ["LBRS"] = { + location = "Blackrock Mountain", + faction = "Neutral", + description = "Lower Blackrock Spire, home to the Dark Horde.", + bosses = "General Drakkisath, War Master Voone", + notable = "Truestrike Shoulders, Dal'Rend's set" + }, + ["Dire Maul North"] = { + location = "Feralas", + faction = "Neutral", + description = "Northern wing featuring tribute runs and King Gordok.", + bosses = "King Gordok, Cho'Rush the Observer", + notable = "Tribute buffs, Ogre Tannin" + }, + ["Scholomance"] = { + location = "Western Plaguelands", + faction = "Neutral", + description = "Former school of necromancy, now Scourge stronghold.", + bosses = "Darkmaster Gandling, Ras Frostwhisper", + notable = "Deathbone set, Necromancy books" + }, + ["Stratholme"] = { + location = "Eastern Plaguelands", + faction = "Neutral", + description = "Plagued city divided between Scourge and Scarlet forces.", + bosses = "Baron Rivendare, Ramstein, Balnazzar", + notable = "Deathcharger's Reins, Runeblade of Baron Rivendare" + }, + ["UBRS"] = { + location = "Blackrock Mountain", + faction = "Neutral", + description = "Upper Blackrock Spire, lair of General Drakkisath.", + bosses = "General Drakkisath, Rend Blackhand", + notable = "Tier 0 upgrades, Dalrend's Sacred Charge" + }, + ["Stormwind Vault"] = { + location = "Stormwind City (Turtle WoW)", + faction = "Alliance", + description = "The royal treasury, infiltrated by thieves.", + bosses = "Various elite robbers", + notable = "Custom Turtle WoW dungeon" + }, + ["Karazhan Crypt"] = { + location = "Deadwind Pass (Turtle WoW)", + faction = "Neutral", + description = "Crypts beneath Karazhan filled with spirits and undead.", + bosses = "Various undead guardians", + notable = "Custom Turtle WoW dungeon" + } +} + +-- Dungeon loading screen textures +local DUNGEON_TEXTURES = { + ["Ragefire Chasm"] = "Interface\\Glues\\LoadingScreens\\LoadScreenRFC", + ["The Deadmines"] = "Interface\\Glues\\LoadingScreens\\LoadScreenDeadmines", + ["Wailing Caverns"] = "Interface\\Glues\\LoadingScreens\\LoadScreenWailingCaverns", + ["Stockades"] = "Interface\\Glues\\LoadingScreens\\LoadScreenStockades", + ["Blackfathom Deeps"] = "Interface\\Glues\\LoadingScreens\\LoadScreenBlackfathomDeeps", + ["Gnomeregan"] = "Interface\\Glues\\LoadingScreens\\LoadScreenGnomeregan", + ["Razorfen Kraul"] = "Interface\\Glues\\LoadingScreens\\LoadScreenRazorfenKraul", + ["SM-Graveyard"] = "Interface\\Glues\\LoadingScreens\\LoadScreenScarletMonastery", + ["SM-Library"] = "Interface\\Glues\\LoadingScreens\\LoadScreenScarletMonastery", + ["SM-Armory"] = "Interface\\Glues\\LoadingScreens\\LoadScreenScarletMonastery", + ["SM-Cathedral"] = "Interface\\Glues\\LoadingScreens\\LoadScreenScarletMonastery", + ["Uldaman"] = "Interface\\Glues\\LoadingScreens\\LoadScreenUldaman", + ["Razorfen Downs"] = "Interface\\Glues\\LoadingScreens\\LoadScreenRazorfenDowns", + ["Maraudon"] = "Interface\\Glues\\LoadingScreens\\LoadScreenMaraudon", + ["Zul'Farrak"] = "Interface\\Glues\\LoadingScreens\\LoadScreenZulFarrak", + ["Sunken Temple"] = "Interface\\Glues\\LoadingScreens\\LoadScreenSunkenTemple", + ["Blackrock Depths"] = "Interface\\Glues\\LoadingScreens\\LoadScreenBlackrockDepths", + ["Dire Maul West"] = "Interface\\Glues\\LoadingScreens\\LoadScreenDireMaul", + ["Dire Maul East"] = "Interface\\Glues\\LoadingScreens\\LoadScreenDireMaul", + ["Dire Maul North"] = "Interface\\Glues\\LoadingScreens\\LoadScreenDireMaul", + ["LBRS"] = "Interface\\Glues\\LoadingScreens\\LoadScreenBlackrockSpire", + ["UBRS"] = "Interface\\Glues\\LoadingScreens\\LoadScreenBlackrockSpire", + ["Scholomance"] = "Interface\\Glues\\LoadingScreens\\LoadScreenScholomance", + ["Stratholme"] = "Interface\\Glues\\LoadingScreens\\LoadScreenStratholme" +} + +-- Dungeon full display names +local DUNGEON_FULL_NAMES = { + ["SM-Graveyard"] = "Scarlet Monastery - Graveyard", + ["SM-Library"] = "Scarlet Monastery - Library", + ["SM-Armory"] = "Scarlet Monastery - Armory", + ["SM-Cathedral"] = "Scarlet Monastery - Cathedral", + ["LBRS"] = "Lower Blackrock Spire", + ["UBRS"] = "Upper Blackrock Spire" +} + +-- Default loading screen for dungeons without custom backgrounds +local DEFAULT_LOADING_SCREEN = "Interface\\Glues\\LoadingScreens\\LoadScreenKalimdor" + +-- Timezone to region mapping +local TIMEZONE_REGIONS = { + -- Americas + PST = "Americas", + PDT = "Americas", + MST = "Americas", + MDT = "Americas", + CST = "Americas", + CDT = "Americas", + EST = "Americas", + EDT = "Americas", + -- Oceania + AEST = "Oceania", + NZST = "Oceania", + -- Europe + GMT = "Europe", + UTC = "Europe", + CET = "Europe", + EET = "Europe", + -- Asia + MSK = "Asia", + IST = "Asia", + JST = "Asia" +} + + + + +-- Fixed background opacity (v1.2) +local IGW_BG_OPACITY = 0.95 + +-- Layout constants +local FILTER_ROW_TOP_Y = -50 -- where the first filter row starts +local FILTER_ROW_HEIGHT = 30 -- height of a single filter row +local FILTER_ROW_GAP = 5 -- spacing between filter rows +local FILTER_ROWS = 2 -- number of filter rows (second row reserved for future options) +local FILTER_EXTRA_SPACING = 0 -- extra space under filter rows (reserved for future options) +local HEADER_BASE_OFFSET_Y = 10 -- gap between filters and column headers + +-- Row heights +local ROW_HEIGHT_DEFAULT = 20 +local ROW_HEIGHT_DETAILS = 25 -- +5px only on Member Details tab + +-- Visible rows +local VISIBLE_ROWS_GUILD = 13 -- Guild Members tab (show 1 fewer row) +local VISIBLE_ROWS_DETAILS = 11 -- Member Details tab + + + +local function IGW_GetHeaderTopY() + return FILTER_ROW_TOP_Y + - (FILTER_ROW_HEIGHT * FILTER_ROWS) + - (FILTER_ROW_GAP * (FILTER_ROWS - 1)) + - FILTER_EXTRA_SPACING + - HEADER_BASE_OFFSET_Y +end + +-- Race detection from officer note (first character(s) followed by dash) +local function IGW_GetRace(officerNote) + if not officerNote or officerNote == "" then return "" end + + -- Check for 2-character codes first (Ta-, He-, Go-, Hu-, NE-, Dw-, Un-) + local firstTwo = string.sub(officerNote, 1, 2) + local thirdChar = string.sub(officerNote, 3, 3) + + if thirdChar == "-" then + if firstTwo == "Ta" then return "Tauren" + elseif firstTwo == "He" or firstTwo == "HE" then return "High Elf" + elseif firstTwo == "Go" then return "Goblin" + elseif firstTwo == "Hu" or firstTwo == "HU" then return "Human" + elseif firstTwo == "NE" then return "Night Elf" + elseif firstTwo == "Dw" or firstTwo == "DW" then return "Dwarf" + elseif firstTwo == "Un" or firstTwo == "UN" then return "Undead" + elseif firstTwo == "Gn" or firstTwo == "GN" then return "Gnome" + elseif firstTwo == "Or" or firstTwo == "OR" then return "Orc" + elseif firstTwo == "Tr" or firstTwo == "TR" then return "Troll" + end + end + + -- Check for single char + dash (backwards compatibility) + local firstChar = string.sub(officerNote, 1, 1) + local secondChar = string.sub(officerNote, 2, 2) + + if secondChar ~= "-" then + return "" -- No dash found, return empty + end + + if firstChar == "N" then return "Night Elf" + elseif firstChar == "D" then return "Dwarf" + elseif firstChar == "H" then return "Human" + elseif firstChar == "T" then return "Troll" + elseif firstChar == "O" then return "Orc" + elseif firstChar == "G" then return "Gnome" + elseif firstChar == "U" then return "Undead" + else return "" + end +end + +-- Faction based on race +local function IGW_GetFaction(race) + if race == "" then return "" end + + if race == "Night Elf" or race == "Dwarf" or race == "Human" or race == "Gnome" or race == "High Elf" then + return "Alliance" + elseif race == "Troll" or race == "Tauren" or race == "Orc" or race == "Undead" or race == "Goblin" then + return "Horde" + else + return "" + end +end + +-- Keybinding strings (shown in ESC > Key Bindings) +BINDING_HEADER_IMPROVEDGUILDWINDOW = "Improved Guild Window" +BINDING_NAME_IMPROVEDGUILDWINDOW_TOGGLE = "Toggle Window" + + +-- ESC-close support (WoW closes frames listed in UISpecialFrames before opening the game menu) +local function IGW_AddToSpecialFrames(frameName, toFront) + if not frameName or frameName == "" then return end + if not UISpecialFrames then UISpecialFrames = {} end + + -- remove duplicates + for i = table.getn(UISpecialFrames), 1, -1 do + if UISpecialFrames[i] == frameName then + table.remove(UISpecialFrames, i) + end + end + + if toFront then + table.insert(UISpecialFrames, 1, frameName) + else + table.insert(UISpecialFrames, frameName) + end +end + +-- Class color lookup +local CLASS_COLORS = { + ["Warrior"] = {r=0.78, g=0.61, b=0.43}, + ["Paladin"] = {r=0.96, g=0.55, b=0.73}, + ["Hunter"] = {r=0.67, g=0.83, b=0.45}, + ["Rogue"] = {r=1.0, g=0.96, b=0.41}, + ["Priest"] = {r=1.0, g=1.0, b=1.0}, + ["Shaman"] = {r=0.0, g=0.44, b=0.87}, + ["Mage"] = {r=0.41, g=0.8, b=0.94}, + ["Warlock"] = {r=0.58, g=0.51, b=0.79}, + ["Druid"] = {r=1.0, g=0.49, b=0.04} +} + +-- Initialize saved variables +function IGW:OnLoad() + -- Note: At this point, SavedVariables are NOT yet loaded + -- We'll initialize them in OnPlayerLogin when they're available + self:RegisterEvents() + + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00Improved Guild Window loaded!|r") + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00Set keybind in ESC > Key Bindings > Improved Guild Window|r") + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00Or use /igw to open|r") +end + +-- Initialize SavedVariables and create frame (called after PLAYER_LOGIN when SavedVariables are loaded) +function IGW:InitializeSavedVariables() + if not ImprovedGuildWindowDB then + ImprovedGuildWindowDB = { + position = {}, + width = 650, + height = 500, + sortColumn = "name", + sortAscending = true, + showOffline = true, + opacity = 1.0, + bgColor = {r = 0.15, g = 0.15, b = 0.15} -- Default dark grey + } + end + + -- Ensure bgColor exists (for existing saved variables) + if not ImprovedGuildWindowDB.bgColor then + ImprovedGuildWindowDB.bgColor = {r = 0.15, g = 0.15, b = 0.15} + end + + -- Load saved opacity or use default + IGW_BG_OPACITY = ImprovedGuildWindowDB.opacity or 0.95 + + sortColumn = ImprovedGuildWindowDB.sortColumn or "name" + sortAscending = ImprovedGuildWindowDB.sortAscending + if sortAscending == nil then sortAscending = true end + + -- If rememberSorting is off, reset to defaults + if ImprovedGuildWindowDB.rememberSorting == false then + sortColumn = "name" + sortAscending = true + end + + showOffline = ImprovedGuildWindowDB.showOffline + if showOffline == nil then showOffline = true end + + -- NOW create the main frame with SavedVariables loaded + self:CreateMainFrame() +end + +-- Create the main window frame +function IGW:CreateMainFrame() + -- Ensure DB exists (should have been initialized by now) + if not ImprovedGuildWindowDB then + DEFAULT_CHAT_FRAME:AddMessage("|cFFFF0000ERROR: ImprovedGuildWindowDB not initialized!|r") + return + end + + -- Don't create if already exists + if frame then + return + end + + -- Main frame + frame = CreateFrame("Frame", "ImprovedGuildWindowFrame", UIParent) + IGW_AddToSpecialFrames("ImprovedGuildWindowFrame", false) + frame:SetWidth(ImprovedGuildWindowDB.width or 650) + frame:SetHeight(ImprovedGuildWindowDB.height or 500) + frame:SetFrameStrata("HIGH") + frame:SetFrameLevel(20) + frame:SetMovable(true) + frame:EnableMouse(true) + frame:SetClampedToScreen(true) + frame:Hide() + + -- Background (border only, solid texture added separately) + frame:SetBackdrop({ + edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", + edgeSize = 32, + insets = { left = 11, right = 12, top = 12, bottom = 11 } + }) + frame:SetBackdropBorderColor(1, 1, 1, 1) + + -- Create solid background texture (inside the border) + local bgTexture = frame:CreateTexture(nil, "BACKGROUND") + bgTexture:SetPoint("TOPLEFT", frame, "TOPLEFT", 11, -12) + bgTexture:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, 11) + -- Use saved color or default to dark grey + local bgColor = {r = 0.15, g = 0.15, b = 0.15} + if ImprovedGuildWindowDB and ImprovedGuildWindowDB.bgColor then + bgColor = ImprovedGuildWindowDB.bgColor + end + bgTexture:SetTexture(bgColor.r, bgColor.g, bgColor.b, IGW_BG_OPACITY) + frame.bgTexture = bgTexture + + -- Title bar for dragging + local titleBar = CreateFrame("Frame", nil, frame) + titleBar:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -12) + titleBar:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -12, -12) + titleBar:SetHeight(30) + titleBar:SetFrameLevel(frame:GetFrameLevel() + 1) + titleBar:EnableMouse(true) + titleBar:RegisterForDrag("LeftButton") + titleBar:SetScript("OnDragStart", function() + frame:StartMoving() + end) + titleBar:SetScript("OnDragStop", function() + frame:StopMovingOrSizing() + IGW:SavePosition() + end) + + -- Title text + local title = titleBar:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + title:SetPoint("CENTER", titleBar, "CENTER", 0, 0) + title:SetText(GetGuildInfo("player") or "Guild Window") + frame.titleText = title + + -- Close button (with 5px padding) + local closeBtn = CreateFrame("Button", nil, frame, "UIPanelCloseButton") + closeBtn:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -10, -10) + closeBtn:SetFrameLevel(frame:GetFrameLevel() + 2) + closeBtn:SetScript("OnClick", function() + -- Save window states if remember is enabled + if ImprovedGuildWindowDB and ImprovedGuildWindowDB.rememberWindows then + ImprovedGuildWindowDB.windowStates = { + detailsOpen = IGW.detailsFrame and IGW.detailsFrame:IsVisible() or false, + infoOpen = IGW.infoFrame and IGW.infoFrame:IsVisible() or false + } + end + frame:Hide() + -- Close details window when main window closes + if IGW.detailsFrame then + IGW.detailsFrame:Hide() + end + if IGW.infoFrame then + IGW.infoFrame:Hide() + -- Un-highlight Guild Info button + frame.tab4:SetBackdropColor(0.2, 0.2, 0.2, 1) + frame.tab4Text:SetTextColor(0.7, 0.7, 0.7) + end + -- Close options window + if IGW.optionsFrame then + IGW.optionsFrame:Hide() + end + -- Close dungeon details dialog + if IGW.dungeonDialog then + IGW.dungeonDialog:Hide() + end + end) + + -- Options button (standard button, anchored to left of close button) + local optionsBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") + optionsBtn:SetPoint("RIGHT", closeBtn, "LEFT", -2, 0) -- 2px gap from close button + optionsBtn:SetWidth(65) + optionsBtn:SetHeight(20) + optionsBtn:SetText("Options") + optionsBtn:SetFrameLevel(frame:GetFrameLevel() + 2) + + optionsBtn:SetScript("OnClick", function() + IGW:ToggleOptionsWindow() + end) + -- Filter section + self:CreateFilterSection() + + -- Column headers + self:CreateColumnHeaders() + + -- Scroll frame for roster + self:CreateRosterScrollFrame() + + -- Tabs + self:CreateTabs() + + -- Member count - position above tabs, below roster + local memberCount = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + memberCount:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 20, 45) + memberCount:SetText("0 Online | 0 Total") + frame.memberCount = memberCount + + -- Restore saved position or set default + if ImprovedGuildWindowDB.position and ImprovedGuildWindowDB.position.point then + frame:SetPoint( + ImprovedGuildWindowDB.position.point, + UIParent, + ImprovedGuildWindowDB.position.point, + ImprovedGuildWindowDB.position.x or 0, + ImprovedGuildWindowDB.position.y or 0 + ) + else + frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + end + + -- Initialize with Guild Members tab (details) + self:UpdateColumnHeaders("details") +end + +-- Create filter section +function IGW:CreateFilterSection() + local filterFrame = CreateFrame("Frame", nil, frame) +filterFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 20, FILTER_ROW_TOP_Y) +filterFrame:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -20, FILTER_ROW_TOP_Y) +filterFrame:SetHeight(FILTER_ROW_HEIGHT) + +-- Second filter row frame (Advanced Search row) +local filterFrame2 = CreateFrame("Frame", nil, frame) +filterFrame2:SetPoint("TOPLEFT", filterFrame, "BOTTOMLEFT", 0, -FILTER_ROW_GAP) +filterFrame2:SetPoint("TOPRIGHT", filterFrame, "BOTTOMRIGHT", 0, -FILTER_ROW_GAP) +filterFrame2:SetHeight(FILTER_ROW_HEIGHT) +-- Frame always visible, search boxes toggle +frame.filterFrame2 = filterFrame2 + +-- Advanced Search toggle button (on filterFrame2, left side) +local advSearchBtn = CreateFrame("Button", nil, filterFrame2, "UIPanelButtonTemplate") +advSearchBtn:SetPoint("LEFT", filterFrame2, "LEFT", 0, 0) +advSearchBtn:SetWidth(120) +advSearchBtn:SetHeight(22) +advSearchBtn:SetText("Advanced Search") +advSearchBtn:SetScript("OnClick", function() + if frame.advSearchNote:IsVisible() then + frame.advSearchNote:Hide() + frame.advSearchOfficerNote:Hide() + frame.advSearchNoteLabel:Hide() + frame.advSearchOfficerNoteLabel:Hide() + else + frame.advSearchNote:Show() + frame.advSearchOfficerNote:Show() + frame.advSearchNoteLabel:Show() + frame.advSearchOfficerNoteLabel:Show() + end +end) +frame.advSearchBtn = advSearchBtn + +-- Advanced Search filters (name, note, officer note) +local filterWidth = 100 +local filterGap = 15 + +-- Helper function to create search box +local function CreateAdvancedSearchBox(parent, label, xOffset, filterKey) + local labelText = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + labelText:SetPoint("LEFT", parent, "LEFT", xOffset, 0) + labelText:SetText(label .. ":") + labelText:Hide() -- Hidden by default + + local editBox = CreateFrame("EditBox", nil, parent) + editBox:SetPoint("LEFT", labelText, "RIGHT", 5, 0) + editBox:SetWidth(filterWidth) + editBox:SetHeight(22) + editBox:SetFontObject(GameFontNormal) + editBox:SetAutoFocus(false) + editBox:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + tileSize = 16, + edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 } + }) + editBox:SetBackdropColor(0, 0, 0, 0.8) + editBox:SetTextInsets(6, 6, 0, 0) -- Add left/right padding for text + editBox:SetScript("OnTextChanged", function() + local text = this:GetText() + if type(text) == "string" then + frame.advancedFilters[filterKey] = string.lower(text) + else + frame.advancedFilters[filterKey] = "" + end + IGW:UpdateRosterDisplay() + end) + editBox:SetScript("OnEscapePressed", function() + this:ClearFocus() + end) + editBox:Hide() -- Hidden by default + + return editBox, labelText +end + +-- Initialize advanced filters table +frame.advancedFilters = { + note = "", + officernote = "", + region = "" +} + +-- Create search boxes (removed name field) +local xPos = 130 -- Start after button + +local noteBox, noteLabel = CreateAdvancedSearchBox(filterFrame2, "Note", xPos, "note") +frame.advSearchNote = noteBox +frame.advSearchNoteLabel = noteLabel + +xPos = xPos + 35 + filterWidth + filterGap + +local officerNoteBox, officerNoteLabel = CreateAdvancedSearchBox(filterFrame2, "Officer Note", xPos, "officernote") +frame.advSearchOfficerNote = officerNoteBox +frame.advSearchOfficerNoteLabel = officerNoteLabel + +xPos = xPos + 80 + filterWidth + filterGap + +-- (Region dropdown moved to main filter bar) + + + -- Evenly space 4 filter elements across the filter bar + local filterBarWidth = 590 -- Match main window width + + -- Search label and box (element 1) + local searchLabel = filterFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + searchLabel:SetPoint("LEFT", filterFrame, "LEFT", 0, 0) + searchLabel:SetText("Search:") + + -- Search editbox + local searchBox = CreateFrame("EditBox", nil, filterFrame) + searchBox:SetPoint("LEFT", searchLabel, "RIGHT", 5, 0) + searchBox:SetWidth(120) + searchBox:SetHeight(22) + searchBox:SetFontObject(GameFontNormal) + searchBox:SetAutoFocus(false) + searchBox:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + tileSize = 16, + edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 } + }) + searchBox:SetBackdropColor(0, 0, 0, 0.8) + searchBox:SetTextInsets(6, 6, 0, 0) -- Add left/right padding for text + searchBox:SetScript("OnTextChanged", function() + local text = this:GetText() + if type(text) == "string" then + filterText = string.lower(text) + else + filterText = "" + end + IGW:UpdateRosterDisplay() + end) + searchBox:SetScript("OnEscapePressed", function() + this:ClearFocus() + end) + frame.searchBox = searchBox + + -- Rank filter label (element 2 - positioned at 1/4 mark + 30px) + local rankLabel = filterFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + rankLabel:SetPoint("LEFT", filterFrame, "LEFT", filterBarWidth * 0.25 + 30, 0) + rankLabel:SetText("Rank:") + + -- Rank dropdown + local rankDropdown = CreateFrame("Button", "IGW_RankDropdown", filterFrame, "UIDropDownMenuTemplate") + rankDropdown:SetPoint("LEFT", rankLabel, "RIGHT", -15, -2) + UIDropDownMenu_SetWidth(100, rankDropdown) + UIDropDownMenu_SetText("All Ranks", rankDropdown) + + UIDropDownMenu_Initialize(rankDropdown, function() + local info = {} + info.text = "All Ranks" + info.value = -1 + info.func = function() + filterRank = -1 + UIDropDownMenu_SetText("All Ranks", rankDropdown) + IGW:UpdateRosterDisplay() + end + UIDropDownMenu_AddButton(info) + + -- Add guild ranks + -- GuildControlGetRankName uses 1-based (1=GM, 2=next, etc) + -- GetGuildRosterInfo returns 0-based rankIndex (0=GM, 1=next, etc) + -- So we need to store value as i-1 to match GetGuildRosterInfo + for i = 1, GuildControlGetNumRanks() do + local rankName = GuildControlGetRankName(i) + local rankValue = i - 1 -- Convert to 0-based for matching + info = {} + info.text = rankName + info.value = rankValue + info.func = function() + filterRank = rankValue + UIDropDownMenu_SetText(rankName, rankDropdown) + IGW:UpdateRosterDisplay() + end + UIDropDownMenu_AddButton(info) + end + end) + + -- Region dropdown (element 3 - positioned at 1/2 mark + 40px) + local regionLabel = filterFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + regionLabel:SetPoint("LEFT", filterFrame, "LEFT", filterBarWidth * 0.5 + 40, 2) + regionLabel:SetText("Region:") + + local regionDropdown = CreateFrame("Frame", "IGW_RegionDropdown", filterFrame, "UIDropDownMenuTemplate") + regionDropdown:SetPoint("LEFT", regionLabel, "RIGHT", -15, -2) + UIDropDownMenu_SetWidth(90, regionDropdown) + UIDropDownMenu_SetText("All", regionDropdown) + frame.regionDropdown = regionDropdown + + UIDropDownMenu_Initialize(regionDropdown, function() + local info = {} + + -- All option + info.text = "All" + info.value = "" + info.func = function() + frame.advancedFilters.region = "" + UIDropDownMenu_SetText("All", regionDropdown) + IGW:UpdateRosterDisplay() + end + UIDropDownMenu_AddButton(info) + + -- Americas + info = {} + info.text = "Americas" + info.value = "Americas" + info.func = function() + frame.advancedFilters.region = "Americas" + UIDropDownMenu_SetText("Americas", regionDropdown) + IGW:UpdateRosterDisplay() + end + UIDropDownMenu_AddButton(info) + + -- Oceania + info = {} + info.text = "Oceania" + info.value = "Oceania" + info.func = function() + frame.advancedFilters.region = "Oceania" + UIDropDownMenu_SetText("Oceania", regionDropdown) + IGW:UpdateRosterDisplay() + end + UIDropDownMenu_AddButton(info) + + -- Europe + info = {} + info.text = "Europe" + info.value = "Europe" + info.func = function() + frame.advancedFilters.region = "Europe" + UIDropDownMenu_SetText("Europe", regionDropdown) + IGW:UpdateRosterDisplay() + end + UIDropDownMenu_AddButton(info) + + -- Asia + info = {} + info.text = "Asia" + info.value = "Asia" + info.func = function() + frame.advancedFilters.region = "Asia" + UIDropDownMenu_SetText("Asia", regionDropdown) + IGW:UpdateRosterDisplay() + end + UIDropDownMenu_AddButton(info) + end) + + -- Refresh button (on advanced search row, aligned right) + local refreshBtn = CreateFrame("Button", nil, filterFrame2, "UIPanelButtonTemplate") + refreshBtn:SetPoint("RIGHT", filterFrame2, "RIGHT", 0, 0) + refreshBtn:SetWidth(100) + refreshBtn:SetHeight(22) + refreshBtn:SetText("Clear / Refresh") + refreshBtn:SetScript("OnClick", function() + -- Clear legacy search box + if frame.searchBox then + frame.searchBox:SetText("") + end + filterText = "" + + -- Clear advanced search boxes + if frame.advSearchNote then + frame.advSearchNote:SetText("") + end + if frame.advSearchOfficerNote then + frame.advSearchOfficerNote:SetText("") + end + if frame.regionDropdown then + frame.advancedFilters.region = "" + UIDropDownMenu_SetText("All", frame.regionDropdown) + end + if frame.advancedFilters then + frame.advancedFilters.name = "" + frame.advancedFilters.note = "" + frame.advancedFilters.officernote = "" + end + + -- Reset rank filter to "All Ranks" + filterRank = -1 + if rankDropdown then + UIDropDownMenu_SetText("All Ranks", rankDropdown) + end + + -- Reset sorting to default + sortColumn = "rank" + sortAscending = false + + -- Update header arrows + if frame.headerButtons then + for _, btn in ipairs(frame.headerButtons) do + if btn.column == sortColumn then + btn.arrow:SetText(sortAscending and "^" or "v") + else + btn.arrow:SetText("") + end + end + end + + -- Refresh guild roster + GuildRoster() + IGW:UpdateRosterDisplay() + end) + + -- Show Offline checkbox (element 4 - positioned at 3/4 mark + 65px, aligned with search text) + local offlineCheck = CreateFrame("CheckButton", "IGW_OfflineCheck", filterFrame, "UICheckButtonTemplate") + offlineCheck:SetPoint("LEFT", filterFrame, "LEFT", filterBarWidth * 0.75 + 65, 0) + offlineCheck:SetWidth(24) + offlineCheck:SetHeight(24) + offlineCheck:SetChecked(showOffline) + + local offlineLabel = offlineCheck:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + offlineLabel:SetPoint("LEFT", offlineCheck, "RIGHT", 0, 0) + offlineLabel:SetText("Show Offline") + + offlineCheck:SetScript("OnClick", function() + showOffline = this:GetChecked() == 1 + if ImprovedGuildWindowDB then + ImprovedGuildWindowDB.showOffline = showOffline + end + IGW:UpdateRosterDisplay() + end) + + frame.offlineCheck = offlineCheck +end + +-- Create column headers +function IGW:CreateColumnHeaders() + local headerFrame = CreateFrame("Frame", nil, frame) + local headerTopY = IGW_GetHeaderTopY() + headerFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 20, headerTopY) + headerFrame:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -40, headerTopY) +local headerTopY = IGW_GetHeaderTopY() +headerFrame:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -40, -90) + headerFrame:SetHeight(20) + + frame.headerFrame = headerFrame + frame.headerButtons = {} + + -- Initial headers will be set by UpdateColumnHeaders +end + +-- Create roster scroll frame +function IGW:CreateRosterScrollFrame() + -- Create scroll frame with FauxScrollFrame + local scrollFrame = CreateFrame("ScrollFrame", "IGW_RosterScroll", frame, "FauxScrollFrameTemplate") + local headerTopY = IGW_GetHeaderTopY() +-- Scroll starts below the header row (20px header + ~5px breathing room) +scrollFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 20, headerTopY - 25) + scrollFrame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -40, 50) + scrollFrame:SetFrameLevel(frame:GetFrameLevel() + 1) + + -- Set up scroll behavior + scrollFrame:SetScript("OnVerticalScroll", function() + FauxScrollFrame_OnVerticalScroll(15, function() IGW:UpdateRosterDisplay() end) + end) + + -- Create row frames as children of main frame + local rows = {} + for i = 1, 15 do + local row = CreateFrame("Button", nil, frame) + row:SetWidth(590) + row:SetHeight(ROW_HEIGHT_DEFAULT) + row:SetFrameLevel(frame:GetFrameLevel() + 2) + row:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") + + -- Position relative to main frame, accounting for scroll frame position + row:SetPoint("TOPLEFT", frame, "TOPLEFT", 25, (headerTopY - 30) - (i-1) * ROW_HEIGHT_DEFAULT) + + -- Name + local name = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + name:SetPoint("LEFT", row, "LEFT", 5, 0) + name:SetWidth(105) + name:SetJustifyH("LEFT") + row.name = name + + -- Level + local level = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + level:SetPoint("LEFT", row, "LEFT", 115, 0) + level:SetWidth(35) + level:SetJustifyH("CENTER") + row.level = level + + -- Class (reuse for rank in roster view) + local class = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + class:SetPoint("LEFT", row, "LEFT", 155, 0) + class:SetWidth(85) + class:SetJustifyH("LEFT") + row.class = class + + -- Race + local race = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + race:SetPoint("LEFT", row, "LEFT", 245, 0) + race:SetWidth(75) + race:SetJustifyH("LEFT") + row.race = race + + -- Faction + local faction = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + faction:SetPoint("LEFT", row, "LEFT", 325, 0) + faction:SetWidth(50) + faction:SetJustifyH("LEFT") + row.faction = faction + + -- Rank (reuse for note in roster view) - shifted right + local rank = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + rank:SetPoint("LEFT", row, "LEFT", 380, 0) + rank:SetWidth(85) + rank:SetJustifyH("LEFT") + row.rank = rank + + -- Note (reuse for officer note in roster view) + local note = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + note:SetPoint("LEFT", row, "LEFT", 355, 0) + note:SetWidth(105) + note:SetJustifyH("LEFT") + row.note = note + + -- Officer Note (reuse for last online in roster view) + local officerNote = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + officerNote:SetPoint("LEFT", row, "LEFT", 465, 0) + officerNote:SetWidth(75) + officerNote:SetJustifyH("LEFT") + row.officerNote = officerNote + + -- Click to show member details + row:SetScript("OnClick", function() + if row.memberIndex then + IGW:ShowMemberDetails(row.memberIndex) + end + end) + + table.insert(rows, row) + end + + frame.rosterRows = rows + frame.rosterScroll = scrollFrame +end + +-- Create tabs at bottom of window +function IGW:CreateTabs() + local tabHeight = 25 + + -- All 4 tabs evenly distributed across bottom + local tabWidth = 120 + local tabGap = 10 + + -- Calculate positions for 4 evenly spaced tabs + local windowWidth = 620 -- Main window width + local totalTabWidth = (tabWidth * 4) + local totalGapSpace = windowWidth - totalTabWidth + local gapSize = totalGapSpace / 5 -- 5 gaps: left, between tabs (3), right + + -- Tab 4: Guild Info (far left, evenly spaced) + local tab4 = CreateFrame("Button", nil, frame) + tab4:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", gapSize, 15) + tab4:SetWidth(tabWidth) + tab4:SetHeight(tabHeight) + tab4:SetFrameLevel(frame:GetFrameLevel() + 1) + + tab4:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + tileSize = 16, + edgeSize = 16, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + tab4:SetBackdropColor(0.2, 0.2, 0.2, 1) + tab4:SetBackdropBorderColor(0.5, 0.5, 0.5, 1) + + local tab4Text = tab4:CreateFontString(nil, "OVERLAY", "GameFontNormal") + tab4Text:SetPoint("CENTER", tab4, "CENTER", 0, 0) + tab4Text:SetText("|cFFFF0000<|r Guild Info") -- Red left arrow before text + tab4Text:SetTextColor(0.7, 0.7, 0.7) + + tab4:SetScript("OnClick", function() + IGW:ToggleGuildInfoWindow() + end) + + -- Tab 1: Guild Members + local tab1 = CreateFrame("Button", nil, frame) + tab1:SetPoint("LEFT", tab4, "RIGHT", gapSize, 0) + tab1:SetWidth(tabWidth) + tab1:SetHeight(tabHeight) + tab1:SetFrameLevel(frame:GetFrameLevel() + 1) + + -- Background for tab + tab1:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + tileSize = 16, + edgeSize = 16, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + tab1:SetBackdropColor(0.2, 0.2, 0.2, 1) + tab1:SetBackdropBorderColor(0.5, 0.5, 0.5, 1) + + local tab1Text = tab1:CreateFontString(nil, "OVERLAY", "GameFontNormal") + tab1Text:SetPoint("CENTER", tab1, "CENTER", 0, 0) + tab1Text:SetText("Guild Members") + + tab1:SetScript("OnClick", function() + IGW:SwitchTab("details") + end) + + -- Tab 2: Notes & Rank + local tab2 = CreateFrame("Button", nil, frame) + tab2:SetPoint("LEFT", tab1, "RIGHT", gapSize, 0) + tab2:SetWidth(tabWidth) + tab2:SetHeight(tabHeight) + tab2:SetFrameLevel(frame:GetFrameLevel() + 1) + + -- Background for tab + tab2:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + tileSize = 16, + edgeSize = 16, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + tab2:SetBackdropColor(0.2, 0.2, 0.2, 1) + tab2:SetBackdropBorderColor(0.5, 0.5, 0.5, 1) + + local tab2Text = tab2:CreateFontString(nil, "OVERLAY", "GameFontNormal") + tab2Text:SetPoint("CENTER", tab2, "CENTER", 0, 0) + tab2Text:SetText("Notes & Rank") + + tab2:SetScript("OnClick", function() + IGW:SwitchTab("roster") + end) + + -- Tab 3: Detailed View + local tab3 = CreateFrame("Button", nil, frame) + tab3:SetPoint("LEFT", tab2, "RIGHT", gapSize, 0) + tab3:SetWidth(tabWidth) + tab3:SetHeight(tabHeight) + tab3:SetFrameLevel(frame:GetFrameLevel() + 1) + + -- Background for tab + tab3:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + tileSize = 16, + edgeSize = 16, + insets = { left = 3, right = 3, top = 3, bottom = 3 } + }) + tab3:SetBackdropColor(0.2, 0.2, 0.2, 1) + tab3:SetBackdropBorderColor(0.5, 0.5, 0.5, 1) + + local tab3Text = tab3:CreateFontString(nil, "OVERLAY", "GameFontNormal") + tab3Text:SetPoint("CENTER", tab3, "CENTER", 0, 0) + tab3Text:SetText("Detailed View") + + tab3:SetScript("OnClick", function() + IGW:SwitchTab("detailed") + end) + + frame.tab1 = tab1 + frame.tab2 = tab2 + frame.tab3 = tab3 + frame.tab1Text = tab1Text + frame.tab2Text = tab2Text + frame.tab3Text = tab3Text + frame.tab4 = tab4 + frame.tab4Text = tab4Text +end + +-- Switch between tabs +function IGW:SwitchTab(tabName) + currentTab = tabName + + if tabName == "details" then + -- Activate Guild Members tab (tab1) + frame.tab1:SetBackdropColor(0.5, 0.5, 0.5, 1) + frame.tab2:SetBackdropColor(0.2, 0.2, 0.2, 1) + frame.tab3:SetBackdropColor(0.2, 0.2, 0.2, 1) + frame.tab1Text:SetTextColor(1, 1, 1) + frame.tab2Text:SetTextColor(0.7, 0.7, 0.7) + frame.tab3Text:SetTextColor(0.7, 0.7, 0.7) + + -- Set default sorting (only if not remembering) + if not ImprovedGuildWindowDB or ImprovedGuildWindowDB.rememberSorting == false then + sortColumn = "rank" + sortAscending = false -- Descending + end + + -- Hide offline by default + showOffline = false + if frame.offlineCheck then + frame.offlineCheck:SetChecked(false) + end + + -- Show details columns + IGW:UpdateColumnHeaders("details") + elseif tabName == "roster" then + -- Activate Notes & Rank tab (tab2) + frame.tab1:SetBackdropColor(0.2, 0.2, 0.2, 1) + frame.tab2:SetBackdropColor(0.5, 0.5, 0.5, 1) + frame.tab3:SetBackdropColor(0.2, 0.2, 0.2, 1) + frame.tab1Text:SetTextColor(0.7, 0.7, 0.7) + frame.tab2Text:SetTextColor(1, 1, 1) + frame.tab3Text:SetTextColor(0.7, 0.7, 0.7) + + -- Set default sorting (only if not remembering) + if not ImprovedGuildWindowDB or ImprovedGuildWindowDB.rememberSorting == false then + sortColumn = "rank" + sortAscending = false -- Descending + end + + -- Show offline on this tab (use saved preference) + showOffline = (ImprovedGuildWindowDB and ImprovedGuildWindowDB.showOffline ~= false) + if frame.offlineCheck then + frame.offlineCheck:SetChecked(showOffline) + end + + -- Show roster columns + IGW:UpdateColumnHeaders("roster") + elseif tabName == "detailed" then + -- Activate Detailed View tab (tab3) + frame.tab1:SetBackdropColor(0.2, 0.2, 0.2, 1) + frame.tab2:SetBackdropColor(0.2, 0.2, 0.2, 1) + frame.tab3:SetBackdropColor(0.5, 0.5, 0.5, 1) + frame.tab1Text:SetTextColor(0.7, 0.7, 0.7) + frame.tab2Text:SetTextColor(0.7, 0.7, 0.7) + frame.tab3Text:SetTextColor(1, 1, 1) + + -- Set default sorting (only if not remembering) + if not ImprovedGuildWindowDB or ImprovedGuildWindowDB.rememberSorting == false then + sortColumn = "name" + sortAscending = true + end + + -- Show all members including offline + showOffline = true + if frame.offlineCheck then + frame.offlineCheck:SetChecked(true) + end + + -- Show detailed view columns + IGW:UpdateColumnHeaders("detailed") + end + + -- Update header arrows to show current sort state + if frame.headerButtons then + for _, btn in ipairs(frame.headerButtons) do + if btn.column == sortColumn then + btn.arrow:SetText(sortAscending and "^" or "v") + else + btn.arrow:SetText("") + end + end + end + + IGW:UpdateRosterDisplay() +end + +-- Update column headers based on tab +function IGW:UpdateColumnHeaders(tabName) + -- Hide all existing header buttons + if frame.headerButtons then + for _, btn in ipairs(frame.headerButtons) do + btn:Hide() + end + end + + frame.headerButtons = {} + + local headers + if tabName == "roster" then +headers = { + {text = "Name", width = 95, column = "name"}, + {text = "Level", width = 55, column = "level"}, + {text = "Rank", width = 80, column = "rank"}, + {text = "Note", width = 140, column = "note"}, -- narrowed + {text = "Officer Note", width = 140, column = "officernote"}, -- narrowed + {text = "Last Online", width = 80, column = "lastonline"} +} + elseif tabName == "detailed" then + headers = { + {text = "Name", width = 140, column = "name"}, + {text = "Level", width = 60, column = "level"}, + {text = "Rank", width = 120, column = "rank"}, + {text = "Date Joined", width = 110, column = "datejoined"}, + {text = "Time Zone", width = 190, column = "timezone"} + } + else + headers = { + {text = "Name", width = 110, column = "name"}, + {text = "Level", width = 60, column = "level"}, + {text = "Class", width = 75, column = "class"}, + {text = "Race", width = 75, column = "race"}, + {text = "A/H", width = 50, column = "faction"}, + {text = "Location", width = 145, column = "zone"}, + {text = "Rank", width = 85, column = "rank"} + } + end + + local headerFrame = frame.headerFrame + local xPos = 0 + for _, header in ipairs(headers) do + local btn = CreateFrame("Button", nil, headerFrame) + btn:SetPoint("LEFT", headerFrame, "LEFT", xPos, 0) + btn:SetWidth(header.width) + btn:SetHeight(20) + + local text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + text:SetPoint("LEFT", btn, "LEFT", 5, 0) + text:SetPoint("RIGHT", btn, "RIGHT", -15, 0) + text:SetJustifyH("LEFT") + text:SetText(header.text) + + -- Sort arrow + local arrow = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + arrow:SetPoint("RIGHT", btn, "RIGHT", -5, 0) + arrow:SetJustifyH("RIGHT") + arrow:SetText("") + btn.arrow = arrow + btn.column = header.column + + btn:SetScript("OnClick", function() + IGW:SortRoster(this.column) + end) + + -- Highlight on hover + btn:SetScript("OnEnter", function() + text:SetTextColor(1, 1, 0) + end) + btn:SetScript("OnLeave", function() + text:SetTextColor(1, 1, 1) + end) + + if header.column == sortColumn then + arrow:SetText(sortAscending and "^" or "v") + end + + btn:Show() + xPos = xPos + header.width + + table.insert(frame.headerButtons, btn) + end + + -- Keep row columns aligned with header columns + self:ApplyRowLayout(tabName, headers) +end + +-- Apply header-aligned column positions and widths to roster rows (keeps headers + data perfectly lined up) +function IGW:ApplyRowLayout(tabName, headers) + if not frame or not frame.rosterRows or not headers then return end + + -- Build x positions for each header column + local xPos = 0 + local colX = {} + local colW = {} + for _, h in ipairs(headers) do + colX[h.column] = xPos + colW[h.column] = h.width + xPos = xPos + h.width + end + + for _, row in ipairs(frame.rosterRows) do + -- Ensure everything is left-justified for a clean grid + row.name:SetJustifyH("LEFT") + row.level:SetJustifyH("LEFT") + row.class:SetJustifyH("LEFT") + row.race:SetJustifyH("LEFT") + row.faction:SetJustifyH("LEFT") + row.rank:SetJustifyH("LEFT") + row.note:SetJustifyH("LEFT") + row.officerNote:SetJustifyH("LEFT") + + if tabName == "roster" then + -- Member Details tab uses: name, level, (rank -> row.class), (note -> row.rank), + -- (officer note -> row.note), (last online -> row.officerNote) + row.name:ClearAllPoints() + row.name:SetPoint("LEFT", row, "LEFT", colX["name"] + 5, 0) + row.name:SetWidth(colW["name"] - 10) + + row.level:ClearAllPoints() + row.level:SetPoint("LEFT", row, "LEFT", colX["level"] + 5, 0) + row.level:SetWidth(colW["level"] - 10) + + row.class:ClearAllPoints() + row.class:SetPoint("LEFT", row, "LEFT", colX["rank"] + 5, 0) + row.class:SetWidth(colW["rank"] - 10) + + row.rank:ClearAllPoints() + row.rank:SetPoint("LEFT", row, "LEFT", colX["note"] + 5, 0) + row.rank:SetWidth(colW["note"] - 10) + + row.note:ClearAllPoints() + row.note:SetPoint("LEFT", row, "LEFT", colX["officernote"] + 5, 0) + row.note:SetWidth(colW["officernote"] - 10) + + row.officerNote:ClearAllPoints() + row.officerNote:SetPoint("LEFT", row, "LEFT", colX["lastonline"] + 5, 0) + row.officerNote:SetWidth(colW["lastonline"] - 10) + elseif tabName == "detailed" then + -- Detailed View tab uses: name, level, (rank -> row.class), (datejoined -> row.rank), + -- (timezone -> row.note) + row.name:ClearAllPoints() + row.name:SetPoint("LEFT", row, "LEFT", colX["name"] + 5, 0) + row.name:SetWidth(colW["name"] - 10) + + row.level:ClearAllPoints() + row.level:SetPoint("LEFT", row, "LEFT", colX["level"] + 5, 0) + row.level:SetWidth(colW["level"] - 10) + + row.class:ClearAllPoints() + row.class:SetPoint("LEFT", row, "LEFT", colX["rank"] + 5, 0) + row.class:SetWidth(colW["rank"] - 10) + + row.rank:ClearAllPoints() + row.rank:SetPoint("LEFT", row, "LEFT", colX["datejoined"] + 5, 0) + row.rank:SetWidth(colW["datejoined"] - 10) + + row.note:ClearAllPoints() + row.note:SetPoint("LEFT", row, "LEFT", colX["timezone"] + 5, 0) + row.note:SetWidth(colW["timezone"] - 10) + + -- Not used in this view + row.officerNote:SetText("") + row.race:SetText("") + row.faction:SetText("") + else + -- Guild Members tab uses: name, level, class, race, faction, (location -> row.rank), (rank -> row.note) + row.name:ClearAllPoints() + row.name:SetPoint("LEFT", row, "LEFT", colX["name"] + 5, 0) + row.name:SetWidth(colW["name"] - 10) + + row.level:ClearAllPoints() + row.level:SetPoint("LEFT", row, "LEFT", colX["level"] + 5, 0) + row.level:SetWidth(colW["level"] - 10) + + row.class:ClearAllPoints() + row.class:SetPoint("LEFT", row, "LEFT", colX["class"] + 5, 0) + row.class:SetWidth(colW["class"] - 10) + + row.race:ClearAllPoints() + row.race:SetPoint("LEFT", row, "LEFT", colX["race"] + 5, 0) + row.race:SetWidth(colW["race"] - 10) + + row.faction:ClearAllPoints() + row.faction:SetPoint("LEFT", row, "LEFT", colX["faction"] + 5, 0) + row.faction:SetWidth(colW["faction"] - 10) + + row.rank:ClearAllPoints() + row.rank:SetPoint("LEFT", row, "LEFT", colX["zone"] + 5, 0) + row.rank:SetWidth(colW["zone"] - 10) + + row.note:ClearAllPoints() + row.note:SetPoint("LEFT", row, "LEFT", colX["rank"] + 5, 0) + row.note:SetWidth(colW["rank"] - 10) + + -- Not used in this view + row.officerNote:SetText("") + end + end +end + +-- Show officer note edit dialog +function IGW:ShowOfficerNoteEdit(index, memberName) + -- Use default WoW dialog + SetGuildRosterSelection(index) + StaticPopup_Show("SET_GUILDOFFICERNOTE") +end + +-- Show public note edit dialog +function IGW:ShowPublicNoteEdit(index, memberName) + -- Use default WoW dialog + SetGuildRosterSelection(index) + StaticPopup_Show("SET_GUILDPLAYERNOTE") +end + +-- Show member details window +function IGW:ShowMemberDetails(index) + local member = rosterData[index] + if not member then return end + + -- Create details frame if it doesn't exist + if not IGW.detailsFrame then + local df = CreateFrame("Frame", "IGW_DetailsFrame", UIParent) + IGW_AddToSpecialFrames("IGW_DetailsFrame", true) + df:SetWidth(250) + df:SetHeight(frame:GetHeight()) + df:SetFrameStrata("HIGH") + df:SetFrameLevel(30) + -- Only allow moving if setting is enabled + df:SetMovable((ImprovedGuildWindowDB and ImprovedGuildWindowDB.allowMoveSideWindows) or false) + df:EnableMouse(true) + df:SetClampedToScreen(true) + df:Hide() + + -- Background (border only, solid texture added separately) + df:SetBackdrop({ + edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", + edgeSize = 32, + insets = { left = 11, right = 12, top = 12, bottom = 11 } + }) + df:SetBackdropBorderColor(1, 1, 1, 1) + + -- Create solid background texture (inside the border) + local dfBgTexture = df:CreateTexture(nil, "BACKGROUND") + dfBgTexture:SetPoint("TOPLEFT", df, "TOPLEFT", 11, -12) + dfBgTexture:SetPoint("BOTTOMRIGHT", df, "BOTTOMRIGHT", -12, 11) + -- Use saved color or default to dark grey + local bgColor = {r = 0.15, g = 0.15, b = 0.15} + if ImprovedGuildWindowDB and ImprovedGuildWindowDB.bgColor then + bgColor = ImprovedGuildWindowDB.bgColor + end + dfBgTexture:SetTexture(bgColor.r, bgColor.g, bgColor.b, IGW_BG_OPACITY) + df.bgTexture = dfBgTexture + + -- Title bar + local titleBar = CreateFrame("Frame", nil, df) + titleBar:SetPoint("TOPLEFT", df, "TOPLEFT", 12, -12) + titleBar:SetPoint("TOPRIGHT", df, "TOPRIGHT", -12, -12) + titleBar:SetHeight(30) + titleBar:SetFrameLevel(df:GetFrameLevel() + 1) + titleBar:EnableMouse(true) + titleBar:RegisterForDrag("LeftButton") + titleBar:SetScript("OnDragStart", function() + if df:IsMovable() then + df:StartMoving() + end + end) + titleBar:SetScript("OnDragStop", function() + if df:IsMovable() then + df:StopMovingOrSizing() + end + end) + + -- Title text + local title = titleBar:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + title:SetPoint("CENTER", titleBar, "CENTER", 0, 0) + title:SetText("Member Details") + df.title = title + + -- Close button + local closeBtn = CreateFrame("Button", nil, df, "UIPanelCloseButton") + closeBtn:SetPoint("TOPRIGHT", df, "TOPRIGHT", -10, -10) + closeBtn:SetFrameLevel(df:GetFrameLevel() + 2) + closeBtn:SetScript("OnClick", function() + df:Hide() + end) + + -- Content area + local yOffset = -50 + local leftX = 20 + + -- Name + local nameLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal") + nameLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset) + nameLabel:SetText("Name:") + + local nameValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + nameValue:SetPoint("LEFT", nameLabel, "RIGHT", 10, 0) + nameValue:SetText("") + df.nameValue = nameValue + + yOffset = yOffset - 20 + + -- Level & Class + local levelLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal") + levelLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset) + levelLabel:SetText("Level:") + + local levelValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + levelValue:SetPoint("LEFT", levelLabel, "RIGHT", 10, 0) + levelValue:SetText("") + df.levelValue = levelValue + + local classValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + classValue:SetPoint("LEFT", levelValue, "RIGHT", 10, 0) + classValue:SetText("") + df.classValue = classValue + + yOffset = yOffset - 20 + + -- Rank + local rankLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal") + rankLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset) + rankLabel:SetText("Rank:") + + local rankValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + rankValue:SetPoint("LEFT", rankLabel, "RIGHT", 10, 0) + rankValue:SetText("") + df.rankValue = rankValue + + yOffset = yOffset - 20 + + -- Zone + local zoneLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal") + zoneLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset) + zoneLabel:SetText("Zone:") + + local zoneValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + zoneValue:SetPoint("LEFT", zoneLabel, "RIGHT", 10, 0) + zoneValue:SetWidth(150) + zoneValue:SetJustifyH("LEFT") + zoneValue:SetText("") + df.zoneValue = zoneValue + + yOffset = yOffset - 20 + + -- Online status + local onlineLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal") + onlineLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset) + onlineLabel:SetText("Status:") + + local onlineValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + onlineValue:SetPoint("LEFT", onlineLabel, "RIGHT", 10, 0) + onlineValue:SetText("") + df.onlineValue = onlineValue + + yOffset = yOffset - 20 + + -- Date Joined + local dateJoinedLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal") + dateJoinedLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset) + dateJoinedLabel:SetText("Date Joined:") + + local dateJoinedValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + dateJoinedValue:SetPoint("LEFT", dateJoinedLabel, "RIGHT", 10, 0) + dateJoinedValue:SetText("") + df.dateJoinedValue = dateJoinedValue + + yOffset = yOffset - 20 + + -- Time Zone + local timeZoneLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal") + timeZoneLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset) + timeZoneLabel:SetText("Time Zone:") + + local timeZoneValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + timeZoneValue:SetPoint("LEFT", timeZoneLabel, "RIGHT", 10, 0) + timeZoneValue:SetText("") + df.timeZoneValue = timeZoneValue + + -- Faction icon (top right, above divider) + local factionIcon = df:CreateTexture(nil, "ARTWORK") + factionIcon:SetWidth(48) + factionIcon:SetHeight(48) + factionIcon:SetPoint("TOPRIGHT", df, "TOPRIGHT", -12, -40) + factionIcon:Hide() -- Hidden by default + df.factionIcon = factionIcon + + yOffset = yOffset - 30 + + -- Divider 1 (after status info) + local div1 = df:CreateTexture(nil, "ARTWORK") + div1:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider") + div1:SetPoint("TOP", df, "TOP", 36, yOffset) + div1:SetWidth(300) + div1:SetHeight(16) + df.div1 = div1 + + yOffset = yOffset - 16 + +-- Public Note +local noteLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal") +noteLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset) +noteLabel:SetText("Public Note:") +df.noteLabel = noteLabel + +-- Make heading clickable +local noteLabelBtn = CreateFrame("Button", nil, df) +noteLabelBtn:SetPoint("TOPLEFT", noteLabel, "TOPLEFT", -2, 2) +noteLabelBtn:SetPoint("BOTTOMRIGHT", noteLabel, "BOTTOMRIGHT", 2, -2) +noteLabelBtn:SetFrameLevel(df:GetFrameLevel() + 1) +noteLabelBtn:SetScript("OnClick", function() + if df.memberIndex and (not CanEditPublicNote or CanEditPublicNote()) then + IGW:ShowPublicNoteEdit(df.memberIndex, df.memberName) + end +end) + +yOffset = yOffset - 18 + +local noteValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") +noteValue:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset) +noteValue:SetWidth(210) +noteValue:SetJustifyH("LEFT") +noteValue:SetText("") +df.noteValue = noteValue + +local noteValueBtn = CreateFrame("Button", nil, df) +noteValueBtn:SetPoint("TOPLEFT", df, "TOPLEFT", leftX - 2, yOffset + 2) +noteValueBtn:SetWidth(214) +noteValueBtn:SetHeight(40) +noteValueBtn:SetFrameLevel(df:GetFrameLevel() + 1) +noteValueBtn:SetScript("OnClick", function() + if df.memberIndex and (not CanEditPublicNote or CanEditPublicNote()) then + IGW:ShowPublicNoteEdit(df.memberIndex, df.memberName) + end +end) + +yOffset = yOffset - 48 + +-- Officer Note (clickable label) +local officerNoteLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal") +officerNoteLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset) +officerNoteLabel:SetText("Officer Note:") +df.officerNoteLabel = officerNoteLabel + +-- Make label clickable +local officerNoteLabelBtn = CreateFrame("Button", nil, df) +officerNoteLabelBtn:SetPoint("TOPLEFT", officerNoteLabel, "TOPLEFT", -2, 2) +officerNoteLabelBtn:SetPoint("BOTTOMRIGHT", officerNoteLabel, "BOTTOMRIGHT", 2, -2) +officerNoteLabelBtn:SetFrameLevel(df:GetFrameLevel() + 1) +officerNoteLabelBtn:SetScript("OnClick", function() + if df.memberIndex and CanEditOfficerNote() then + IGW:ShowOfficerNoteEdit(df.memberIndex, df.memberName) + end +end) + +yOffset = yOffset - 18 + +-- Officer Note Value (clickable) +local officerNoteValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") +officerNoteValue:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset) +officerNoteValue:SetWidth(210) +officerNoteValue:SetJustifyH("LEFT") +officerNoteValue:SetText("") +df.officerNoteValue = officerNoteValue + +local officerNoteValueBtn = CreateFrame("Button", nil, df) +officerNoteValueBtn:SetPoint("TOPLEFT", df, "TOPLEFT", leftX - 2, yOffset + 2) +officerNoteValueBtn:SetWidth(214) +officerNoteValueBtn:SetHeight(40) +officerNoteValueBtn:SetFrameLevel(df:GetFrameLevel() + 1) +officerNoteValueBtn:SetScript("OnClick", function() + if df.memberIndex and CanEditOfficerNote() then + IGW:ShowOfficerNoteEdit(df.memberIndex, df.memberName) + end +end) + +yOffset = yOffset - 48 + + -- Divider 2 (after officer notes) + local div2 = df:CreateTexture(nil, "ARTWORK") + div2:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider") + div2:SetPoint("TOP", df, "TOP", 36, yOffset) + div2:SetWidth(300) + div2:SetHeight(16) + df.div2 = div2 + + yOffset = yOffset - 16 + + -- Whisper + Invite buttons (even spacing) +local buttonYGap = 8 +local buttonGap = 10 +local whisperWidth = 80 +local inviteWidth = 120 +local buttonHeight = 22 + +local whisperBtn = CreateFrame("Button", nil, df, "UIPanelButtonTemplate") +whisperBtn:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset) +whisperBtn:SetWidth(whisperWidth) +whisperBtn:SetHeight(buttonHeight) +whisperBtn:SetFrameLevel(df:GetFrameLevel() + 1) +whisperBtn:SetText("Whisper") +whisperBtn:SetScript("OnClick", function() + if df.memberName then + ChatFrame_OpenChat("/w " .. df.memberName .. " ", DEFAULT_CHAT_FRAME) + end +end) +df.whisperBtn = whisperBtn + +local inviteBtn = CreateFrame("Button", nil, df, "UIPanelButtonTemplate") +inviteBtn:SetPoint("LEFT", whisperBtn, "RIGHT", buttonGap, 0) +inviteBtn:SetWidth(inviteWidth) +inviteBtn:SetHeight(buttonHeight) +inviteBtn:SetFrameLevel(df:GetFrameLevel() + 1) +inviteBtn:SetText("Invite to Group") +inviteBtn:SetScript("OnClick", function() + if df.memberName then + InviteByName(df.memberName) + end +end) +df.inviteBtn = inviteBtn + +yOffset = yOffset - (buttonHeight + buttonYGap) + + -- Divider 3 (after buttons) + local div3 = df:CreateTexture(nil, "ARTWORK") + div3:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider") + div3:SetPoint("TOP", df, "TOP", 36, yOffset) + div3:SetWidth(300) + div3:SetHeight(16) + df.div3 = div3 + + yOffset = yOffset - 16 + + -- Alts section + local altsLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal") + altsLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset) + altsLabel:SetText("Other Characters:") + df.altsLabel = altsLabel + + yOffset = yOffset - 18 + + local altsValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + altsValue:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset) + altsValue:SetWidth(210) + altsValue:SetJustifyH("LEFT") + altsValue:SetText("—") + df.altsValue = altsValue + + IGW.detailsFrame = df + end + + -- Populate with member data + local df = IGW.detailsFrame + df.memberIndex = index + df.memberName = member.name + + -- Set values + df.nameValue:SetText(member.name or "Unknown") + + -- Get race from officer note + local race = IGW_GetRace(member.officernote or "") + + -- Get class color + local color = CLASS_COLORS[member.class] or {r=1, g=1, b=1} + local classColorCode = string.format("|cFF%02x%02x%02x", color.r * 255, color.g * 255, color.b * 255) + + -- Build level display: "Level Race Class" with class colored + local levelText = tostring(member.level or "?") + if race ~= "" then + levelText = levelText .. " " .. race + end + levelText = levelText .. " " .. classColorCode .. (member.class or "") .. "|r" + + df.levelValue:SetText(levelText) + + -- Clear the separate class field (now included in levelValue) + df.classValue:SetText("") + + df.rankValue:SetText(member.rank or "") + df.zoneValue:SetText(member.zone or "Unknown") + + -- Online status + if member.online then + df.onlineValue:SetTextColor(0, 1, 0) + df.onlineValue:SetText("Online") + else + df.onlineValue:SetTextColor(0.5, 0.5, 0.5) + df.onlineValue:SetText("Offline") + end + + -- Date Joined and Time Zone (extract from officer note) + -- Format: RaceCode-MMDDYYTTT or just MMDDYYTTT (if no race code) or just TT (timezone only) + -- Example: O-030125PST or 030125PST or O-030125 (no timezone) or O-PST (just timezone) + local dateJoined = "—" + local timeZone = "—" + local officerNote = member.officernote or "" + + -- First try to find 6 digits + 3 letters (date with timezone) + local _, _, dateStr, tzStr = string.find(officerNote, "(%d%d%d%d%d%d)(%a%a%a)") + + -- If no timezone found with date, try to find just 6 digits (date only) + if not dateStr then + _, _, dateStr = string.find(officerNote, "(%d%d%d%d%d%d)") + end + + -- If still no timezone found, try to find just 3 letters (timezone only) + if not tzStr then + -- Look for 3 consecutive letters that match a known timezone + for tz, _ in pairs(TIMEZONES) do + if string.find(officerNote, tz) then + tzStr = tz + break + end + end + end + + if dateStr then + -- Parse MMDDYY + local month = string.sub(dateStr, 1, 2) + local day = string.sub(dateStr, 3, 4) + local year = string.sub(dateStr, 5, 6) + + -- Format as MM/DD/YY + dateJoined = month .. "/" .. day .. "/" .. year + end + + -- Look up timezone full name if we have one + if tzStr then + timeZone = TIMEZONES[tzStr] or tzStr + end + + df.dateJoinedValue:SetText(dateJoined) + df.timeZoneValue:SetText(timeZone) + + -- Faction icon + local faction = IGW_GetFaction(race) + if faction == "Alliance" then + df.factionIcon:SetTexture("Interface\\TargetingFrame\\UI-PVP-Alliance") + df.factionIcon:Show() + elseif faction == "Horde" then + df.factionIcon:SetTexture("Interface\\TargetingFrame\\UI-PVP-Horde") + df.factionIcon:Show() + else + df.factionIcon:Hide() -- No faction data + end + + df.noteValue:SetText(member.note or "") + + -- Officer note (only if can view) + if CanViewOfficerNote() then + df.officerNoteLabel:Show() + df.officerNoteValue:Show() + df.officerNoteValue:SetText(member.officernote or "") + else + -- Hide officer note section when user can't view + df.officerNoteLabel:Hide() + df.officerNoteValue:Hide() + end + + -- Find alts: bidirectional detection + -- 1. Find characters with "alt of [current player]" + -- 2. If current player is an alt, find the main and all other alts + local alts = {} + local searchName = string.lower(member.name or "") + local mainCharacter = nil + + -- Helper function to check for exact name match with word boundaries + local function hasExactNameMatch(note, name) + if string.find(note, "^" .. name .. "[%s%p]") or -- Start of string + string.find(note, "[%s%p]" .. name .. "$") or -- End of string + string.find(note, "[%s%p]" .. name .. "[%s%p]") or -- Middle + note == name then -- Exact match only + return true + end + return false + end + + -- Check if current character is an alt (has "alt of X" in their note) + if member.note then + local currentNote = string.lower(member.note) + if string.find(currentNote, "alt") then + -- Extract main character name from note + for _, guildMember in ipairs(rosterData) do + if guildMember.name ~= member.name then + local testName = string.lower(guildMember.name) + if hasExactNameMatch(currentNote, testName) then + mainCharacter = guildMember.name + break + end + end + end + end + end + + -- If we found a main character, search for all alts of that main + if mainCharacter then + local mainNameLower = string.lower(mainCharacter) + + -- Add the main character to the list + table.insert(alts, mainCharacter) + + -- Find all other alts of this main + for _, guildMember in ipairs(rosterData) do + if guildMember.name ~= member.name and guildMember.note then + local note = string.lower(guildMember.note) + if string.find(note, "alt") and hasExactNameMatch(note, mainNameLower) then + table.insert(alts, guildMember.name) + end + end + end + else + -- Current character is the main, find their alts + for _, guildMember in ipairs(rosterData) do + if guildMember.name ~= member.name and guildMember.note then + local note = string.lower(guildMember.note) + if string.find(note, "alt") and hasExactNameMatch(note, searchName) then + table.insert(alts, guildMember.name) + end + end + end + end + + -- Display alts + if table.getn(alts) > 0 then + df.altsLabel:Show() + df.altsValue:Show() + df.div3:Show() + df.altsValue:SetText(table.concat(alts, ", ")) + else + -- Hide alts section when no alts found + df.altsLabel:Hide() + df.altsValue:Hide() + df.div3:Hide() + end + -- Position to the right of main window with 5px gap, same top alignment + df:ClearAllPoints() + df:SetPoint("TOPLEFT", frame, "TOPRIGHT", 5, 0) + df:SetHeight(frame:GetHeight()) + + df:Show() +end + +-- Update opacity of windows +function IGW:UpdateOpacity() + -- Background opacity is fixed in v1.2 + local opacity = IGW_BG_OPACITY + + if frame then + frame:SetBackdropColor(0, 0, 0, opacity) + end + + if IGW.detailsFrame then + IGW.detailsFrame:SetBackdropColor(0, 0, 0, opacity) + end +end + +-- Save window position + +function IGW:SavePosition() + if not ImprovedGuildWindowDB then return end + local point, _, _, x, y = frame:GetPoint() + ImprovedGuildWindowDB.position = { + point = point, + x = x, + y = y + } +end + +-- Register events +function IGW:RegisterEvents() + local eventFrame = CreateFrame("Frame") + eventFrame:RegisterEvent("PLAYER_LOGIN") + eventFrame:RegisterEvent("PLAYER_GUILD_UPDATE") + eventFrame:RegisterEvent("GUILD_ROSTER_UPDATE") + + eventFrame:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + IGW:OnPlayerLogin() + elseif event == "PLAYER_GUILD_UPDATE" or event == "GUILD_ROSTER_UPDATE" then + IGW:UpdateGuildData() + end + end) +end + +-- Handle player login +function IGW:OnPlayerLogin() + -- Initialize SavedVariables and create main frame now that SavedVariables are loaded + self:InitializeSavedVariables() + + if IsInGuild() then + GuildRoster() + end +end + +-- Update guild data +function IGW:UpdateGuildData() + rosterData = {} + local numMembers = GetNumGuildMembers(true) + + -- Update window title with guild name + if frame and frame.titleText then + local guildName = GetGuildInfo("player") + if guildName then + frame.titleText:SetText(guildName) + end + end + + for i = 1, numMembers do + local name, rank, rankIndex, level, class, zone, note, officernote, online, status, classFileName, achievementPoints, achievementRank, isMobile, canSoR, repStanding, guid = GetGuildRosterInfo(i) + + -- Get last online time (years, months, days, hours) + local yearsOffline, monthsOffline, daysOffline, hoursOffline = GetGuildRosterLastOnline(i) + + table.insert(rosterData, { + name = name, + rank = rank, + rankIndex = rankIndex, + level = level, + class = class, + zone = zone, + note = note, + officernote = officernote, + online = online, + status = status, + yearsOffline = yearsOffline, + monthsOffline = monthsOffline, + daysOffline = daysOffline, + hoursOffline = hoursOffline + }) + end + + self:UpdateRosterDisplay() + if IGW.infoFrame and IGW.infoFrame:IsVisible() then + IGW:UpdateGuildInfoWindow() + end +end + +-- Sort roster +function IGW:SortRoster(column) + if sortColumn == column then + sortAscending = not sortAscending + else + sortColumn = column + -- Level defaults to descending (high to low), others ascending + sortAscending = (column ~= "level") + end + + if ImprovedGuildWindowDB and ImprovedGuildWindowDB.rememberSorting ~= false then + ImprovedGuildWindowDB.sortColumn = sortColumn + ImprovedGuildWindowDB.sortAscending = sortAscending + end + + -- Update header arrows + if frame.headerButtons then + for _, btn in ipairs(frame.headerButtons) do + if btn.column == sortColumn then + btn.arrow:SetText(sortAscending and "^" or "v") + else + btn.arrow:SetText("") + end + end + end + + self:UpdateRosterDisplay() +end + +-- Filter and display roster +function IGW:UpdateRosterDisplay() + if not frame or not frame:IsVisible() then return end + if not frame.rosterRows or not frame.rosterScroll then return end + + displayedMembers = {} + + -- Filter members + for i, member in ipairs(rosterData) do + local show = true + + -- Text filter (legacy search box) + if filterText ~= "" then + local searchIn = string.lower(member.name or "") .. + string.lower(member.class or "") .. + string.lower(member.rank or "") .. + string.lower(member.note or "") .. + string.lower(member.officernote or "") + if not string.find(searchIn, filterText, 1, true) then + show = false + end + end + + -- Advanced filters (individual column searches) + if show and frame.advancedFilters then + if show and frame.advancedFilters.note ~= "" then + local noteSearch = string.lower(member.note or "") + if not string.find(noteSearch, frame.advancedFilters.note, 1, true) then + show = false + end + end + + if show and frame.advancedFilters.officernote ~= "" then + local officerNoteSearch = string.lower(member.officernote or "") + if not string.find(officerNoteSearch, frame.advancedFilters.officernote, 1, true) then + show = false + end + end + + -- Region filter (based on timezone) + if show and frame.advancedFilters.region ~= "" then + local officerNote = member.officernote or "" + local _, _, tzStr = string.find(officerNote, "%d%d%d%d%d%d(%a%a%a)") + local memberRegion = tzStr and TIMEZONE_REGIONS[tzStr] or nil + if memberRegion ~= frame.advancedFilters.region then + show = false + end + end + end + + -- Offline filter + if show and not showOffline then + if not member.online then + show = false + end + end + + -- Rank filter + if show and filterRank >= 0 then + if member.rankIndex ~= filterRank then + show = false + end + end + + if show then + table.insert(displayedMembers, { + index = i, + data = member + }) + end + end + + -- Sort members + table.sort(displayedMembers, function(a, b) + local aVal, bVal + + if sortColumn == "name" then + aVal = a.data.name or "" + bVal = b.data.name or "" + elseif sortColumn == "level" then + aVal = a.data.level or 0 + bVal = b.data.level or 0 + elseif sortColumn == "class" then + aVal = a.data.class or "" + bVal = b.data.class or "" + elseif sortColumn == "rank" then + aVal = a.data.rankIndex or 0 + bVal = b.data.rankIndex or 0 + elseif sortColumn == "note" then + aVal = a.data.note or "" + bVal = b.data.note or "" + elseif sortColumn == "officernote" then + aVal = a.data.officernote or "" + bVal = b.data.officernote or "" + elseif sortColumn == "lastonline" or sortColumn == "online" then + -- Sort by time offline (online first, then by how long offline) + if a.data.online and b.data.online then + aVal = 0 + bVal = 0 + elseif a.data.online then + aVal = 0 -- Online = 0 (most recent) + bVal = 999999 -- Offline = large number + elseif b.data.online then + aVal = 999999 + bVal = 0 + else + -- Both offline, calculate time in hours + local aHours = (a.data.yearsOffline or 0) * 8760 + + (a.data.monthsOffline or 0) * 730 + + (a.data.daysOffline or 0) * 24 + + (a.data.hoursOffline or 0) + local bHours = (b.data.yearsOffline or 0) * 8760 + + (b.data.monthsOffline or 0) * 730 + + (b.data.daysOffline or 0) * 24 + + (b.data.hoursOffline or 0) + aVal = aHours + bVal = bHours + end + elseif sortColumn == "zone" then + aVal = a.data.zone or "" + bVal = b.data.zone or "" + elseif sortColumn == "race" then + local aRace = IGW_GetRace(a.data.officernote or "") + local bRace = IGW_GetRace(b.data.officernote or "") + aVal = aRace + bVal = bRace + elseif sortColumn == "faction" then + local aRace = IGW_GetRace(a.data.officernote or "") + local bRace = IGW_GetRace(b.data.officernote or "") + local aFaction = IGW_GetFaction(aRace) + local bFaction = IGW_GetFaction(bRace) + aVal = aFaction + bVal = bFaction + elseif sortColumn == "datejoined" then + -- Extract date directly from officer note (no main fallback) + local _, _, aDateStr = string.find(a.data.officernote or "", "(%d%d%d%d%d%d)") + local _, _, bDateStr = string.find(b.data.officernote or "", "(%d%d%d%d%d%d)") + + -- Convert MMDDYY to YYMMDD for proper sorting (newest first when descending) + if aDateStr then + local aMonth = string.sub(aDateStr, 1, 2) + local aDay = string.sub(aDateStr, 3, 4) + local aYear = string.sub(aDateStr, 5, 6) + aVal = aYear .. aMonth .. aDay + else + aVal = "999999" -- No date = sort to end + end + if bDateStr then + local bMonth = string.sub(bDateStr, 1, 2) + local bDay = string.sub(bDateStr, 3, 4) + local bYear = string.sub(bDateStr, 5, 6) + bVal = bYear .. bMonth .. bDay + else + bVal = "999999" -- No date = sort to end + end + elseif sortColumn == "timezone" then + -- Extract timezone from officer note (can be with or without date) + local _, _, aTzStr = string.find(a.data.officernote or "", "%d%d%d%d%d%d(%a%a%a)") + local _, _, bTzStr = string.find(b.data.officernote or "", "%d%d%d%d%d%d(%a%a%a)") + + -- If not found with date, try standalone timezone + if not aTzStr then + local aOfficerNote = a.data.officernote or "" + for tz, _ in pairs(TIMEZONES) do + if string.find(aOfficerNote, tz) then + aTzStr = tz + break + end + end + end + if not bTzStr then + local bOfficerNote = b.data.officernote or "" + for tz, _ in pairs(TIMEZONES) do + if string.find(bOfficerNote, tz) then + bTzStr = tz + break + end + end + end + + aVal = aTzStr or "zzz" -- No timezone = sort to end + bVal = bTzStr or "zzz" + end + + if sortAscending then + return aVal < bVal + else + return aVal > bVal + end + end) + + -- Update scroll frame + local numDisplayed = table.getn(displayedMembers) + + -- Update FauxScrollFrame + local rowHeight = (currentTab == "roster" or currentTab == "detailed") and ROW_HEIGHT_DETAILS or ROW_HEIGHT_DEFAULT + local maxRows = (currentTab == "roster" or currentTab == "detailed") and VISIBLE_ROWS_DETAILS or VISIBLE_ROWS_GUILD + + -- Ensure we don't have more scroll range than needed + -- If we have fewer items than visible rows, no scrolling needed + local scrollItems = numDisplayed + if numDisplayed <= maxRows then + scrollItems = maxRows -- Set to maxRows to disable scrolling + end + + FauxScrollFrame_Update(frame.rosterScroll, scrollItems, maxRows, rowHeight) + local offset = FauxScrollFrame_GetOffset(frame.rosterScroll) + + -- Safety clamp: ensure offset doesn't exceed valid range + local maxOffset = math.max(0, numDisplayed - maxRows) + if offset > maxOffset then + offset = maxOffset + end + + + local headerTopY = IGW_GetHeaderTopY() + -- Display rows with offset + for i = 1, 14 do + local row = frame.rosterRows[i] + + + + local maxRows = (currentTab == "roster" or currentTab == "detailed") and VISIBLE_ROWS_DETAILS or VISIBLE_ROWS_GUILD + if i > maxRows then + row:Hide() + end + -- Only show fewer rows on Notes & Rank or Detailed View tabs + if (currentTab == "roster" or currentTab == "detailed") and i > VISIBLE_ROWS_DETAILS then + row:Hide() + end + row:SetHeight(rowHeight) + row:ClearAllPoints() + row:SetPoint("TOPLEFT", frame, "TOPLEFT", 25, (headerTopY - 30) - (i-1) * rowHeight) + local dataIndex = offset + i + if dataIndex <= numDisplayed and not ((currentTab == "roster" or currentTab == "detailed") and i > VISIBLE_ROWS_DETAILS) then + local member = displayedMembers[dataIndex].data + local index = displayedMembers[dataIndex].index + + -- Debug first few rows + + if currentTab == "roster" then + -- Roster view columns: Name, Level, Rank, Note, Officer Note, Last Online + + -- Hide Race and Faction (not used in this view) + row.race:SetText("") + row.faction:SetText("") + + -- Name (without offline tag) + row.name:SetText(member.name or "") + if member.online then + row.name:SetTextColor(1, 1, 1) + else + row.name:SetTextColor(0.5, 0.5, 0.5) + end + + -- Level + row.level:SetText(member.level) + row.level:SetTextColor(1, 1, 1) + + -- Rank (using class field) + row.class:SetText(member.rank or "") + row.class:SetTextColor(1, 1, 1) + + -- Note (using rank field) + row.rank:SetText(member.note or "") + row.rank:SetTextColor(1, 1, 1) + + -- Officer Note (using note field) + if CanViewOfficerNote() then + row.note:SetText(member.officernote or "") + else + row.note:SetText("") + end + row.note:SetTextColor(1, 1, 1) + + -- Last Online (using officerNote field) + if member.online then + row.officerNote:SetText("Online") + row.officerNote:SetTextColor(0, 1, 0) + else + -- Format last online time + local lastOnlineText = "" + local isOldOffline = false + + if member.yearsOffline and member.yearsOffline > 0 then + lastOnlineText = member.yearsOffline .. "y ago" + isOldOffline = true + elseif member.monthsOffline and member.monthsOffline >= 2 then + lastOnlineText = member.monthsOffline .. "m ago" + isOldOffline = true + elseif member.monthsOffline and member.monthsOffline > 0 then + lastOnlineText = member.monthsOffline .. "m ago" + elseif member.daysOffline and member.daysOffline > 0 then + lastOnlineText = member.daysOffline .. "d ago" + elseif member.hoursOffline and member.hoursOffline > 0 then + lastOnlineText = member.hoursOffline .. "h ago" + else + lastOnlineText = "Recently" + end + + row.officerNote:SetText(lastOnlineText) + if isOldOffline then + row.officerNote:SetTextColor(1, 0, 0) -- Red for 2+ months + else + row.officerNote:SetTextColor(0.5, 0.5, 0.5) -- Gray + end + end + elseif currentTab == "detailed" then + -- Detailed View columns: Name, Level, Rank, Date Joined, Time Zone + + -- Name (without offline tag) + row.name:SetText(member.name or "") + if member.online then + row.name:SetTextColor(1, 1, 1) + else + row.name:SetTextColor(0.5, 0.5, 0.5) + end + + -- Level + row.level:SetText(member.level) + row.level:SetTextColor(1, 1, 1) + + -- Rank (using class field) + row.class:SetText(member.rank or "") + row.class:SetTextColor(1, 1, 1) + + -- Extract Date Joined and Time Zone from officer note (timezone can be standalone) + local dateJoined = "—" + local timeZone = "—" + local officerNote = member.officernote or "" + local _, _, dateStr, tzStr = string.find(officerNote, "(%d%d%d%d%d%d)(%a%a%a)") + if not dateStr then + _, _, dateStr = string.find(officerNote, "(%d%d%d%d%d%d)") + end + + -- If still no timezone found, try to find just 3 letters (timezone only) + if not tzStr then + for tz, _ in pairs(TIMEZONES) do + if string.find(officerNote, tz) then + tzStr = tz + break + end + end + end + + if dateStr then + local month = string.sub(dateStr, 1, 2) + local day = string.sub(dateStr, 3, 4) + local year = string.sub(dateStr, 5, 6) + dateJoined = month .. "/" .. day .. "/" .. year + end + + if tzStr then + timeZone = TIMEZONES[tzStr] or tzStr + end + + -- Date Joined (using rank field) + row.rank:SetText(dateJoined) + row.rank:SetTextColor(1, 1, 1) + + -- Time Zone (using note field) + row.note:SetText(timeZone) + row.note:SetTextColor(1, 1, 1) + + -- Hide race and faction fields + row.race:SetText("") + row.faction:SetText("") + row.officerNote:SetText("") + else + -- Guild Members view columns: Name, Level, Class, Location, Rank + -- Name + local nameText = member.name + row.name:SetText(nameText) + if member.online then + row.name:SetTextColor(1, 1, 1) + else + row.name:SetTextColor(0.5, 0.5, 0.5) + end + + -- Level + row.level:SetText(member.level) + row.level:SetTextColor(1, 1, 1) + + -- Class with color + local color = CLASS_COLORS[member.class] or {r=1, g=1, b=1} + row.class:SetTextColor(color.r, color.g, color.b) + row.class:SetText(member.class or "") + + -- Race + local race = IGW_GetRace(member.officernote or "") + row.race:SetText(race) + row.race:SetTextColor(1, 1, 1) + + -- Faction + local faction = IGW_GetFaction(race) + if faction == "Alliance" then + row.faction:SetTextColor(0.2, 0.5, 1) -- Blue + row.faction:SetText("A") + elseif faction == "Horde" then + row.faction:SetTextColor(1, 0.2, 0.2) -- Red + row.faction:SetText("H") + else + row.faction:SetText("") -- Blank for unknown + end + + -- Location (use rank field for display) + if member.online then + row.rank:SetText(member.zone or "Unknown") + row.rank:SetTextColor(1, 1, 1) + else + row.rank:SetText("Offline") + row.rank:SetTextColor(0.5, 0.5, 0.5) + end + + -- Rank (use note field for display) + row.note:SetText(member.rank or "") + row.note:SetTextColor(1, 1, 1) + + -- Hide officer note field (not used in this view) + row.officerNote:SetText("") + end + + row.memberIndex = index + row.memberName = member.name + row:Show() + else + row:Hide() + end + end + + -- Update member count with online and total + local onlineCount = 0 + local totalMembers = table.getn(rosterData) + + for _, m in ipairs(rosterData) do + if m.online then + onlineCount = onlineCount + 1 + end + end + + if frame.memberCount then + frame.memberCount:SetText(string.format("%d Online | %d Total", onlineCount, totalMembers)) + end +end + +-- Toggle window visibility +function IGW:ToggleWindow() + if frame:IsVisible() then + frame:Hide() + -- Save window states if remember is enabled + if ImprovedGuildWindowDB and ImprovedGuildWindowDB.rememberWindows then + ImprovedGuildWindowDB.windowStates = { + detailsOpen = IGW.detailsFrame and IGW.detailsFrame:IsVisible() or false, + infoOpen = IGW.infoFrame and IGW.infoFrame:IsVisible() or false + } + end + -- Close details window when main window closes + if IGW.detailsFrame then + IGW.detailsFrame:Hide() + end + -- Close guild info window when main window closes + if IGW.infoFrame then + IGW.infoFrame:Hide() + end + -- Close dungeon details dialog when main window closes + if IGW.dungeonDialog then + IGW.dungeonDialog:Hide() + end + else + if IsInGuild() then + GuildRoster() + -- Use saved default tab or fall back to "details" + local defaultTab = "details" + if ImprovedGuildWindowDB and ImprovedGuildWindowDB.defaultTab then + defaultTab = ImprovedGuildWindowDB.defaultTab + end + self:SwitchTab(defaultTab) + frame:Show() + self:UpdateRosterDisplay() + + -- Restore remembered windows if enabled + if ImprovedGuildWindowDB and ImprovedGuildWindowDB.rememberWindows and ImprovedGuildWindowDB.windowStates then + if ImprovedGuildWindowDB.windowStates.detailsOpen and IGW.detailsFrame then + IGW.detailsFrame:Show() + end + if ImprovedGuildWindowDB.windowStates.infoOpen and IGW.infoFrame then + IGW:ToggleGuildInfoWindow() + end + end + else + DEFAULT_CHAT_FRAME:AddMessage("|cFFFF0000You are not in a guild.|r") + end + end +end + +-- Update Guild Info window contents +function IGW:UpdateGuildInfoWindow() + if not IGW.infoFrame then return end + local gf = IGW.infoFrame + + -- Guild name + local guildName = GetGuildInfo("player") or "Guild" + if gf.guildNameText then + gf.guildNameText:SetText(guildName) + end + + -- Total members (use rosterData if populated, otherwise query API) + local total = 0 + -- Count guild members (excluding alts) and alts separately + local totalMembers = 0 + local totalAlts = 0 + if rosterData and table.getn(rosterData) > 0 then + for _, member in ipairs(rosterData) do + local rank = string.lower(member.rank or "") + if string.find(rank, "alt") then + totalAlts = totalAlts + 1 + else + totalMembers = totalMembers + 1 + end + end + else + totalMembers = GetNumGuildMembers(true) or 0 + end + if gf.totalMembersText then + gf.totalMembersText:SetText("Guild Members: " .. totalMembers) + end + if gf.totalAltsText then + gf.totalAltsText:SetText("Guild Member Alts: " .. totalAlts) + end + + -- MOTD + local motd = "" + if GetGuildRosterMOTD then + motd = GetGuildRosterMOTD() or "" + end + if motd == "" then motd = "—" end + if gf.motdValue then + gf.motdValue:SetText(motd) + end + + -- Guild Information (public guild info text) + local infoText = "" + if GetGuildInfoText then + infoText = GetGuildInfoText() or "" + end + if infoText == "" then infoText = "—" end + if gf.infoValue then + gf.infoValue:SetText(infoText) + end + + -- Class distribution (from rosterData) - Bar Graph + -- Clear existing bars + if gf.classBars then + for _, bar in ipairs(gf.classBars) do + bar.frame:Hide() + end + gf.classBars = {} + end + + local counts = {} + local maxCount = 0 + for _, m in ipairs(rosterData or {}) do + if m and m.class then + counts[m.class] = (counts[m.class] or 0) + 1 + if counts[m.class] > maxCount then + maxCount = counts[m.class] + end + end + end + + local entries = {} + for cls, c in pairs(counts) do + table.insert(entries, {cls=cls, c=c}) + end + table.sort(entries, function(a,b) + if a.c == b.c then + return a.cls < b.cls + end + return a.c > b.c + end) + + -- Create bars + local barHeight = 12 + local barGap = 2 + local maxBarWidth = 180 + local yOffset = 0 + + for i, e in ipairs(entries) do + if i > 10 then break end -- Limit to top 10 classes + + local barFrame = CreateFrame("Frame", nil, gf.classBarsContainer) + barFrame:SetPoint("TOPLEFT", gf.classBarsContainer, "TOPLEFT", 0, yOffset) + barFrame:SetHeight(barHeight) + + -- Calculate bar width based on count + local barWidth = maxCount > 0 and (e.c / maxCount) * maxBarWidth or 0 + barFrame:SetWidth(barWidth) + + -- Bar background with class color + local color = CLASS_COLORS[e.cls] or {r=0.5, g=0.5, b=0.5} + local barBg = barFrame:CreateTexture(nil, "BACKGROUND") + barBg:SetAllPoints(barFrame) + barBg:SetTexture("Interface\\TargetingFrame\\UI-StatusBar") + barBg:SetVertexColor(color.r, color.g, color.b, 0.8) + + -- Label (class name and count) + local label = barFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + label:SetPoint("LEFT", barFrame, "LEFT", 3, 0) + label:SetText(string.format("%s: %d", e.cls, e.c)) + label:SetTextColor(1, 1, 1) + + table.insert(gf.classBars, {frame=barFrame, bg=barBg, label=label}) + + yOffset = yOffset - (barHeight + barGap) + end + + -- Level 60 Class distribution - Bar Graph + -- Clear existing bars + if gf.class60Bars then + for _, bar in ipairs(gf.class60Bars) do + bar.frame:Hide() + end + gf.class60Bars = {} + end + + local counts60 = {} + local maxCount60 = 0 + for _, m in ipairs(rosterData or {}) do + if m and m.class and m.level == 60 then + counts60[m.class] = (counts60[m.class] or 0) + 1 + if counts60[m.class] > maxCount60 then + maxCount60 = counts60[m.class] + end + end + end + + local entries60 = {} + for cls, c in pairs(counts60) do + table.insert(entries60, {cls=cls, c=c}) + end + table.sort(entries60, function(a,b) + if a.c == b.c then + return a.cls < b.cls + end + return a.c > b.c + end) + + -- Create level 60 bars + local barHeight60 = 12 + local barGap60 = 2 + local maxBarWidth60 = 180 + local yOffset60 = 0 + + for i, e in ipairs(entries60) do + if i > 10 then break end -- Limit to top 10 classes + + local barFrame = CreateFrame("Frame", nil, gf.class60BarsContainer) + barFrame:SetPoint("TOPLEFT", gf.class60BarsContainer, "TOPLEFT", 0, yOffset60) + barFrame:SetHeight(barHeight60) + + -- Calculate bar width based on count + local barWidth = maxCount60 > 0 and (e.c / maxCount60) * maxBarWidth60 or 0 + barFrame:SetWidth(barWidth) + + -- Bar background with class color + local color = CLASS_COLORS[e.cls] or {r=0.5, g=0.5, b=0.5} + local barBg = barFrame:CreateTexture(nil, "BACKGROUND") + barBg:SetAllPoints(barFrame) + barBg:SetTexture("Interface\\TargetingFrame\\UI-StatusBar") + barBg:SetVertexColor(color.r, color.g, color.b, 0.8) + + -- Label (class name and count) + local label = barFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + label:SetPoint("LEFT", barFrame, "LEFT", 3, 0) + label:SetText(string.format("%s: %d", e.cls, e.c)) + label:SetTextColor(1, 1, 1) + + table.insert(gf.class60Bars, {frame=barFrame, bg=barBg, label=label}) + + yOffset60 = yOffset60 - (barHeight60 + barGap60) + end + + -- Officers Online (uses OFFICER_RANK_THRESHOLD configuration) + local onlineOfficers = {} + for _, m in ipairs(rosterData or {}) do + if m and m.online and m.rankIndex ~= nil then + -- rankIndex: 0=Guild Master, 1-9=other ranks + if m.rankIndex <= OFFICER_RANK_THRESHOLD then + table.insert(onlineOfficers, m.name) + end + end + end + + local officersText = "" + if table.getn(onlineOfficers) > 0 then + table.sort(onlineOfficers) + officersText = table.concat(onlineOfficers, "\n") + else + officersText = "No officers online" + end + + if gf.officersValue then + gf.officersValue:SetText(officersText) + end +end + +-- Update Page 3 - Crafters +function IGW:UpdateCraftersPage() + if not IGW.infoFrame then return end + local gf = IGW.infoFrame + local content = gf.craftersContent + if not content then return end + + -- Clear existing content + local children = {content:GetChildren()} + for _, child in ipairs(children) do + child:Hide() + child:SetParent(nil) + end + + -- Parse crafters from public notes (300+ skill level only) + local crafters = {} + for _, m in ipairs(rosterData or {}) do + if m and m.name and m.note then + local noteUpper = string.upper(m.note) + + -- Profession search patterns with abbreviations (crafting professions only) + local professions = { + {name="Alchemy", patterns={"ALCHEMY", "ALCH", "ALC"}}, + {name="Blacksmithing", patterns={"BLACKSMITHING", "BLACKSMITH", "SMITH", "BS", "B%.S%."}}, + {name="Enchanting", patterns={"ENCHANTING", "ENCHANT", "ENCH", "ENC"}}, + {name="Engineering", patterns={"ENGINEERING", "ENGINEER", "ENG", "ENGI"}}, + {name="Jewelcrafting", patterns={"JEWELCRAFTING", "JEWEL", "JC", "J%.C%."}}, + {name="Leatherworking", patterns={"LEATHERWORKING", "LEATHER", "LW", "L%.W%."}}, + {name="Tailoring", patterns={"TAILORING", "TAILOR", "TAIL", "TLR"}} + } + + for _, prof in ipairs(professions) do + for _, pattern in ipairs(prof.patterns) do + local skill = nil + -- Try pattern: ProfessionName 300 or ProfessionName: 300 + local _, _, skill1 = string.find(noteUpper, pattern .. "%s*:?%s*(%d+)") + -- Try pattern: 300 ProfessionName + local _, _, skill2 = string.find(noteUpper, "(%d+)%s*" .. pattern) + + skill = skill1 or skill2 + + if skill then + local skillNum = tonumber(skill) + -- Only include online players with 300+ skill + if skillNum and skillNum >= 300 and m.online then + -- Check if we already added this profession for this player + local alreadyAdded = false + for _, existing in ipairs(crafters) do + if existing.name == m.name and existing.prof == prof.name then + alreadyAdded = true + break + end + end + + if not alreadyAdded then + table.insert(crafters, { + name = m.name, + prof = prof.name, + skill = skillNum, + online = true + }) + end + break -- Found this profession, skip other patterns for it + end + end + end + end + end + end + + -- Group by profession + local professionGroups = {} + for _, crafter in ipairs(crafters) do + if not professionGroups[crafter.prof] then + professionGroups[crafter.prof] = {} + end + table.insert(professionGroups[crafter.prof], crafter) + end + + -- Sort each profession group alphabetically by name + for prof, group in pairs(professionGroups) do + table.sort(group, function(a, b) + return a.name < b.name + end) + end + + -- Sort professions alphabetically + local sortedProfs = {} + for prof, _ in pairs(professionGroups) do + table.insert(sortedProfs, prof) + end + table.sort(sortedProfs) + + -- Define all 7 crafting professions in order + local allProfessions = { + "Alchemy", "Blacksmithing", "Enchanting", + "Engineering", "Jewelcrafting", "Leatherworking", + "Tailoring" + } + + -- Display all professions with fixed spacing + local yOffset = 0 + local sectionHeight = 50 -- Height per profession section (title + 2 rows + gap) + + for _, prof in ipairs(allProfessions) do + local group = professionGroups[prof] or {} -- Use empty table if no crafters + + -- Profession subtitle (centered) + local subtitle = content:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + subtitle:SetPoint("TOP", content, "TOP", 0, yOffset) + subtitle:SetWidth(210) + subtitle:SetJustifyH("CENTER") + subtitle:SetTextColor(1, 1, 1) -- White color + subtitle:SetText(prof) + yOffset = yOffset - 18 + + -- Display up to 6 crafters in 2 lines (3 per line, clickable) + for line = 1, 2 do + local startIdx = (line - 1) * 3 + 1 + local lineMembers = {} + + for i = startIdx, math.min(startIdx + 2, table.getn(group)) do + table.insert(lineMembers, group[i]) + end + + if table.getn(lineMembers) > 0 then + -- Create container for this line + local lineContainer = CreateFrame("Frame", nil, content) + lineContainer:SetPoint("TOP", content, "TOP", 0, yOffset) + lineContainer:SetWidth(210) + lineContainer:SetHeight(14) + + -- Calculate actual text widths using a temporary FontString + local tempText = lineContainer:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + local totalWidth = 0 + local buttonWidths = {} + + for _, member in ipairs(lineMembers) do + tempText:SetText(member.name) + local nameWidth = tempText:GetStringWidth() + 1 -- Reduced from +2 to +1 + table.insert(buttonWidths, nameWidth) + totalWidth = totalWidth + nameWidth + end + tempText:Hide() -- Hide temporary text + + -- Add comma widths (", " spacing) + if table.getn(lineMembers) > 1 then + tempText:SetText(", ") + local commaWidth = tempText:GetStringWidth() + totalWidth = totalWidth + ((table.getn(lineMembers) - 1) * commaWidth) + end + + -- Starting position (centered) + local xOffset = -(totalWidth / 2) + + -- Create clickable buttons for each name + for idx, member in ipairs(lineMembers) do + -- Capture member name in local variable to avoid closure issues + local memberName = member.name + + local nameBtn = CreateFrame("Button", nil, lineContainer) + nameBtn:SetPoint("LEFT", lineContainer, "LEFT", xOffset + 105, 0) + nameBtn:SetWidth(buttonWidths[idx]) + nameBtn:SetHeight(14) + + local nameText = nameBtn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + nameText:SetAllPoints(nameBtn) + nameText:SetJustifyH("LEFT") + nameText:SetText(string.format("|cFF00FF00%s|r", memberName)) + + nameBtn:SetScript("OnClick", function() + ChatFrameEditBox:SetText("/w " .. memberName .. " ") + ChatFrameEditBox:Show() + ChatFrameEditBox:SetFocus() + end) + + nameBtn:SetScript("OnEnter", function() + nameText:SetText(string.format("|cFFFFFF00%s|r", memberName)) -- Yellow on hover + end) + + nameBtn:SetScript("OnLeave", function() + nameText:SetText(string.format("|cFF00FF00%s|r", memberName)) -- Green normally + end) + + xOffset = xOffset + buttonWidths[idx] + + -- Add comma after name (except last one) + if idx < table.getn(lineMembers) then + local comma = lineContainer:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + comma:SetPoint("LEFT", lineContainer, "LEFT", xOffset + 105, 0) + comma:SetText("|cFF00FF00, |r") + tempText:SetText(", ") + xOffset = xOffset + tempText:GetStringWidth() + end + end + end + + yOffset = yOffset - 14 + end + + -- Gap after each profession + yOffset = yOffset - 6 + end +end + +-- Show dungeon details dialog +function IGW:ShowDungeonDetailsDialog(dungeon) + if not dungeon then return end + + -- Create dialog if it doesn't exist + if not IGW.dungeonDialog then + local dialog = CreateFrame("Frame", "IGW_DungeonDetailsDialog", UIParent) + dialog:SetWidth(350) + dialog:SetHeight(220) + dialog:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + dialog:SetFrameStrata("DIALOG") + dialog:SetBackdrop({ + bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", + edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", + tile = true, + tileSize = 32, + edgeSize = 32, + insets = { left = 11, right = 12, top = 12, bottom = 11 } + }) + dialog:SetBackdropColor(0, 0, 0, 1) + dialog:EnableMouse(true) + dialog:SetMovable(true) + dialog:RegisterForDrag("LeftButton") + dialog:SetScript("OnDragStart", function() this:StartMoving() end) + dialog:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + -- Title + local title = dialog:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + title:SetPoint("TOP", dialog, "TOP", 0, -20) + dialog.title = title + + -- Level range + local levelRange = dialog:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + levelRange:SetPoint("TOP", title, "BOTTOM", 0, -10) + dialog.levelRange = levelRange + + -- Location + local location = dialog:CreateFontString(nil, "OVERLAY", "GameFontNormal") + location:SetPoint("TOP", levelRange, "BOTTOM", 0, -8) + dialog.location = location + + -- Faction + local faction = dialog:CreateFontString(nil, "OVERLAY", "GameFontNormal") + faction:SetPoint("TOP", location, "BOTTOM", 0, -5) + dialog.faction = faction + + -- Divider + local divider = dialog:CreateTexture(nil, "ARTWORK") + divider:SetHeight(1) + divider:SetWidth(310) + divider:SetPoint("TOP", faction, "BOTTOM", 0, -10) + divider:SetTexture(1, 1, 1, 0.2) + + -- Description + local description = dialog:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + description:SetPoint("TOP", divider, "BOTTOM", 0, -10) + description:SetWidth(310) + description:SetJustifyH("LEFT") + description:SetSpacing(3) + dialog.description = description + + -- Close button + local closeBtn = CreateFrame("Button", nil, dialog, "UIPanelCloseButton") + closeBtn:SetPoint("TOPRIGHT", dialog, "TOPRIGHT", -5, -5) + + dialog:Hide() + IGW.dungeonDialog = dialog + end + + local dialog = IGW.dungeonDialog + + -- Update dialog content + dialog.title:SetText(dungeon.name) + dialog.levelRange:SetText(string.format("Level %d-%d", dungeon.minLevel, dungeon.maxLevel)) + dialog.location:SetText("Location: " .. (dungeon.location or "Unknown")) + + -- Faction coloring + local factionText = "Faction: " + if dungeon.faction == "Alliance" then + dialog.faction:SetText(factionText .. "|cFF0080FFAlliance|r") + elseif dungeon.faction == "Horde" then + dialog.faction:SetText(factionText .. "|cFFFF0000Horde|r") + else + dialog.faction:SetText(factionText .. "Both") + end + + dialog.description:SetText(dungeon.description or "No description available.") + + dialog:Show() +end + +-- Update Page 4 - Suggested Dungeons +function IGW:UpdateDungeonsPage() + local gf = IGW.infoFrame + if not gf or not gf.dungeonsContent then return end + + local content = gf.dungeonsContent + + -- Clear existing dungeon entries + local children = {content:GetChildren()} + for _, child in ipairs(children) do + child:Hide() + end + + -- Get online members' levels + local onlineLevels = {} + for _, m in ipairs(rosterData or {}) do + if m and m.online and m.level then + table.insert(onlineLevels, m.level) + end + end + + if table.getn(onlineLevels) == 0 then + local noData = content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + noData:SetPoint("TOP", content, "TOP", 0, -20) + noData:SetWidth(210) + noData:SetJustifyH("CENTER") + noData:SetText("No members online") + noData:SetTextColor(0.7, 0.7, 0.7) + return + end + + -- Calculate average level of online members + local totalLevel = 0 + for _, lvl in ipairs(onlineLevels) do + totalLevel = totalLevel + lvl + end + local avgLevel = totalLevel / table.getn(onlineLevels) + + -- Dungeon database with level ranges (Turtle WoW) + local dungeons = { + {name = "Ragefire Chasm", minLevel = 13, maxLevel = 18, location = "Orgrimmar", faction = "Horde", description = "A lava-filled cavern beneath Orgrimmar, home to Trogg invaders and fire cultists."}, + {name = "The Deadmines", minLevel = 17, maxLevel = 24, location = "Westfall", faction = "Alliance", description = "A network of tunnels beneath Westfall controlled by the Defias Brotherhood."}, + {name = "Wailing Caverns", minLevel = 17, maxLevel = 24, location = "The Barrens", faction = "Both", description = "A corrupted underground oasis in the Barrens with deviate creatures."}, + {name = "Stockades", minLevel = 22, maxLevel = 30, location = "Stormwind", faction = "Alliance", description = "A high-security prison in Stormwind where dangerous criminals are held."}, + {name = "Blackfathom Deeps", minLevel = 22, maxLevel = 31, location = "Ashenvale", faction = "Both", description = "A former temple now overrun by the Twilight's Hammer cult."}, + {name = "Dragonmaw Retreat", minLevel = 25, maxLevel = 34, location = "Wetlands", faction = "Both", description = "A custom Turtle WoW dungeon featuring Dragonmaw orcs in the Wetlands."}, + {name = "SM-Graveyard", minLevel = 27, maxLevel = 36, location = "Tirisfal Glades", faction = "Both", description = "Scarlet Monastery: Graveyard - The burial grounds corrupted by the undead."}, + {name = "Gnomeregan", minLevel = 29, maxLevel = 38, location = "Dun Morogh", faction = "Both", description = "The irradiated gnomish city, now overrun by troggs and malfunctioning machines."}, + {name = "Crescent Grove", minLevel = 32, maxLevel = 38, location = "Feralas", faction = "Both", description = "A custom Turtle WoW dungeon in Feralas with ancient night elf ruins."}, + {name = "SM-Library", minLevel = 32, maxLevel = 39, location = "Tirisfal Glades", faction = "Both", description = "Scarlet Monastery: Library - Repository of Scarlet Crusade knowledge."}, + {name = "Razorfen Kraul", minLevel = 32, maxLevel = 42, location = "The Barrens", faction = "Both", description = "An ancient quilboar stronghold with death speaker cultists."}, + {name = "Stormwrought Ruins", minLevel = 35, maxLevel = 41, location = "Azshara", faction = "Both", description = "A custom Turtle WoW dungeon featuring elementals and ancient magic."}, + {name = "SM-Armory", minLevel = 40, maxLevel = 45, location = "Tirisfal Glades", faction = "Both", description = "Scarlet Monastery: Armory - Training grounds of the Scarlet Crusade."}, + {name = "SM-Cathedral", minLevel = 40, maxLevel = 45, location = "Tirisfal Glades", faction = "Both", description = "Scarlet Monastery: Cathedral - The heart of Scarlet Crusade power."}, + {name = "Uldaman", minLevel = 40, maxLevel = 51, location = "Badlands", faction = "Both", description = "An ancient titan vault filled with troggs and earthen constructs."}, + {name = "Razorfen Downs", minLevel = 42, maxLevel = 44, location = "The Barrens", faction = "Both", description = "A quilboar burial ground corrupted by the Scourge."}, + {name = "Gilneas City", minLevel = 43, maxLevel = 49, location = "Gilneas", faction = "Both", description = "A custom Turtle WoW dungeon in the cursed ruins of Gilneas."}, + {name = "Maraudon", minLevel = 45, maxLevel = 55, location = "Desolace", faction = "Both", description = "A vast cavern complex beneath Desolace with centaur and elemental forces."}, + {name = "Zul'Farrak", minLevel = 46, maxLevel = 56, location = "Tanaris", faction = "Both", description = "An ancient troll city in Tanaris filled with sandfury trolls."}, + {name = "Sunken Temple", minLevel = 50, maxLevel = 60, location = "Swamp of Sorrows", faction = "Both", description = "Temple of Atal'Hakkar - An ancient troll temple dedicated to the blood god."}, + {name = "Blackrock Depths", minLevel = 52, maxLevel = 60, location = "Blackrock Mountain", faction = "Both", description = "A massive Dark Iron dwarf city within Blackrock Mountain."}, + {name = "Hateforge Quarry", minLevel = 52, maxLevel = 60, location = "Searing Gorge", faction = "Both", description = "A custom Turtle WoW dungeon with Dark Iron operations."}, + {name = "Dire Maul West", minLevel = 55, maxLevel = 60, location = "Feralas", faction = "Both", description = "Ancient elven ruins overrun by ogres - Western wing."}, + {name = "Dire Maul East", minLevel = 55, maxLevel = 60, location = "Feralas", faction = "Both", description = "Ancient elven ruins overrun by ogres - Eastern wing."}, + {name = "LBRS", minLevel = 55, maxLevel = 60, location = "Blackrock Mountain", faction = "Both", description = "Lower Blackrock Spire - Orc stronghold within Blackrock Mountain."}, + {name = "Dire Maul North", minLevel = 58, maxLevel = 60, location = "Feralas", faction = "Both", description = "Ancient elven ruins - Northern wing with the Tribute Run."}, + {name = "Scholomance", minLevel = 58, maxLevel = 60, location = "Western Plaguelands", faction = "Both", description = "A school of necromancy run by the Scourge."}, + {name = "Stratholme", minLevel = 58, maxLevel = 60, location = "Eastern Plaguelands", faction = "Both", description = "A city destroyed by the Scourge with Living and Undead sides."}, + {name = "UBRS", minLevel = 55, maxLevel = 60, location = "Blackrock Mountain", faction = "Both", description = "Upper Blackrock Spire - Stronghold of the Blackrock orcs."}, + {name = "Stormwind Vault", minLevel = 60, maxLevel = 60, location = "Stormwind", faction = "Alliance", description = "A custom Turtle WoW dungeon beneath Stormwind."}, + {name = "Karazhan Crypt", minLevel = 60, maxLevel = 60, location = "Deadwind Pass", faction = "Both", description = "A custom Turtle WoW dungeon in the crypts beneath Karazhan."} + } + + -- Score each dungeon based on how well it fits the online levels + for _, dungeon in ipairs(dungeons) do + local score = 0 + for _, lvl in ipairs(onlineLevels) do + if lvl >= dungeon.minLevel and lvl <= dungeon.maxLevel then + score = score + 1 + end + end + dungeon.score = score + dungeon.coverage = table.getn(onlineLevels) > 0 and (score / table.getn(onlineLevels)) or 0 + end + + -- Sort by minimum level ascending + table.sort(dungeons, function(a, b) + return a.minLevel < b.minLevel + end) + + -- Display up to 22 dungeons with at least 4 players + local yOffset = -10 + local displayed = 0 + + -- Get current player's level + local playerLevel = UnitLevel("player") + + for i, dungeon in ipairs(dungeons) do + if displayed >= 22 then break end + if dungeon.score >= 4 then -- Only show dungeons with at least 4 matching members + -- Create clickable button + local dungeonBtn = CreateFrame("Button", nil, content) + dungeonBtn:SetPoint("TOP", content, "TOP", 0, yOffset) + dungeonBtn:SetWidth(210) + dungeonBtn:SetHeight(16) + + -- Create text for the button + local btnText = dungeonBtn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + btnText:SetPoint("LEFT", dungeonBtn, "LEFT", 0, 0) + btnText:SetWidth(210) + btnText:SetJustifyH("LEFT") + dungeonBtn:SetFontString(btnText) + + -- Color based on player level vs dungeon range + local colorCode = "|cFFFFFFFF" -- White (default) + if playerLevel >= dungeon.minLevel and playerLevel <= dungeon.maxLevel then + colorCode = "|cFF00FF00" -- Green (perfect fit) + elseif playerLevel >= dungeon.minLevel - 3 and playerLevel <= dungeon.maxLevel + 3 then + colorCode = "|cFFFFFF00" -- Yellow (close fit) + elseif playerLevel < dungeon.minLevel then + colorCode = "|cFFFF4040" -- Red (too low) + else + colorCode = "|cFF808080" -- Gray (too high) + end + + -- Format player count text + local playerText = dungeon.score == 1 and "1 Player" or string.format("%d Players", dungeon.score) + + btnText:SetText(string.format("%s%s|r (%d-%d) - |cFF888888%s|r", + colorCode, dungeon.name, dungeon.minLevel, dungeon.maxLevel, playerText)) + + -- Highlight on hover + local highlight = dungeonBtn:CreateTexture(nil, "BACKGROUND") + highlight:SetAllPoints() + highlight:SetTexture(1, 1, 1, 0.1) + dungeonBtn:SetHighlightTexture(highlight) + + -- Store dungeon info directly on the button + dungeonBtn.dungeonName = dungeon.name + dungeonBtn.minLevel = dungeon.minLevel + dungeonBtn.maxLevel = dungeon.maxLevel + dungeonBtn.score = dungeon.score + + -- Click handler - show dungeon details dialog + dungeonBtn:SetScript("OnClick", function() + IGW:ShowDungeonDetails(this.dungeonName, this.minLevel, this.maxLevel, this.score) + end) + + yOffset = yOffset - 16 + displayed = displayed + 1 + end + end + + if displayed == 0 then + local noSuggestions = content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + noSuggestions:SetPoint("TOP", content, "TOP", 0, -20) + noSuggestions:SetWidth(210) + noSuggestions:SetJustifyH("CENTER") + noSuggestions:SetText("No suitable dungeons found") + noSuggestions:SetTextColor(0.7, 0.7, 0.7) + end +end + + + +-- Show dungeon details dialog +function IGW:ShowDungeonDetails(dungeonName, minLevel, maxLevel, score) + if not dungeonName or not DUNGEON_DETAILS[dungeonName] then return end + + local details = DUNGEON_DETAILS[dungeonName] + + -- Create or reuse dialog + if not IGW.dungeonDialog then + local dialog = CreateFrame("Frame", "IGW_DungeonDialog", UIParent) + dialog:SetWidth(350) + dialog:SetHeight(220) + dialog:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + dialog:SetFrameStrata("DIALOG") + dialog:SetBackdrop({ + bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", + edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", + tile = true, + tileSize = 32, + edgeSize = 32, + insets = { left = 11, right = 12, top = 12, bottom = 11 } + }) + dialog:SetBackdropColor(0, 0, 0, 1) + dialog:EnableMouse(true) + dialog:SetMovable(true) + dialog:RegisterForDrag("LeftButton") + dialog:SetScript("OnDragStart", function() this:StartMoving() end) + dialog:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + -- Background loading screen texture (scaled UP by 75% = 175% total) + local bgTexture = dialog:CreateTexture(nil, "BACKGROUND") + bgTexture:SetPoint("TOPLEFT", dialog, "TOPLEFT", 11, -12) + bgTexture:SetPoint("BOTTOMRIGHT", dialog, "BOTTOMRIGHT", -12, 11) + -- Crop the texture to show center portion at 175% zoom + -- To show center at 1.75x zoom, we need to show the middle 57.1% of the texture (1/1.75 = 0.571) + local cropAmount = (1 - (1 / 1.75)) / 2 -- 0.214 on each side + bgTexture:SetTexCoord(cropAmount, 1 - cropAmount, cropAmount, 1 - cropAmount) + dialog.bgTexture = bgTexture + + -- Dark overlay for readability (35% opacity) + local overlay = dialog:CreateTexture(nil, "BORDER") + overlay:SetPoint("TOPLEFT", dialog, "TOPLEFT", 11, -12) + overlay:SetPoint("BOTTOMRIGHT", dialog, "BOTTOMRIGHT", -12, 11) + overlay:SetTexture(0, 0, 0, 0.35) + + -- Title + local title = dialog:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + title:SetPoint("TOP", dialog, "TOP", 0, -20) + title:SetWidth(310) + dialog.title = title + + -- Location + local location = dialog:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + location:SetPoint("TOP", title, "BOTTOM", 0, -8) + location:SetWidth(310) + location:SetTextColor(0.8, 0.8, 0.8) + dialog.location = location + + -- Level range + local levelRange = dialog:CreateFontString(nil, "OVERLAY", "GameFontNormal") + levelRange:SetPoint("TOP", location, "BOTTOM", 0, -10) + levelRange:SetWidth(310) + dialog.levelRange = levelRange + + -- Player count + local playerCount = dialog:CreateFontString(nil, "OVERLAY", "GameFontNormal") + playerCount:SetPoint("TOP", levelRange, "BOTTOM", 0, -5) + playerCount:SetWidth(310) + playerCount:SetTextColor(0.5, 1.0, 0.5) + dialog.playerCount = playerCount + + -- Description + local desc = dialog:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + desc:SetPoint("TOP", playerCount, "BOTTOM", 0, -15) + desc:SetWidth(310) + desc:SetJustifyH("CENTER") + desc:SetTextColor(1, 1, 1) + dialog.desc = desc + + -- Bosses label + local bossesLabel = dialog:CreateFontString(nil, "OVERLAY", "GameFontNormal") + bossesLabel:SetPoint("TOP", desc, "BOTTOM", 0, -15) + bossesLabel:SetWidth(310) + bossesLabel:SetText("Bosses:") + bossesLabel:SetTextColor(1, 0.82, 0) + + -- Bosses + local bosses = dialog:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + bosses:SetPoint("TOP", bossesLabel, "BOTTOM", 0, -5) + bosses:SetWidth(310) + bosses:SetJustifyH("CENTER") + bosses:SetTextColor(0.9, 0.9, 0.9) + dialog.bosses = bosses + + -- Close button + local closeBtn = CreateFrame("Button", nil, dialog, "UIPanelCloseButton") + closeBtn:SetPoint("TOPRIGHT", dialog, "TOPRIGHT", -5, -5) + + -- Make ESC key close the dialog + table.insert(UISpecialFrames, "IGW_DungeonDialog") + + dialog:Hide() + IGW.dungeonDialog = dialog + end + + local dialog = IGW.dungeonDialog + + -- Update loading screen background + -- First clear any existing texture, then set new one + local texture = DUNGEON_TEXTURES[dungeonName] or DEFAULT_LOADING_SCREEN + dialog.bgTexture:SetTexture(texture) + + -- Get full display name if available + local displayName = DUNGEON_FULL_NAMES[dungeonName] or dungeonName + + -- Update dialog content + dialog.title:SetText(displayName) + dialog.location:SetText(details.location .. " (" .. details.faction .. ")") + dialog.levelRange:SetText("Levels: " .. minLevel .. "-" .. maxLevel) + + local playerText = score == 1 and "1 player online" or score .. " players online" + dialog.playerCount:SetText(playerText) + + dialog.desc:SetText(details.description) + dialog.bosses:SetText(details.bosses) + + dialog:Show() +end + +-- Toggle Guild Info window (left-side companion window; empty for now) +function IGW:ToggleGuildInfoWindow() + if not frame then return end + + -- Create if needed + if not IGW.infoFrame then + local gf = CreateFrame("Frame", "IGW_GuildInfoFrame", UIParent) + IGW_AddToSpecialFrames("IGW_GuildInfoFrame", true) + gf:SetWidth(250) + gf:SetHeight(frame:GetHeight()) + gf:SetFrameStrata("HIGH") + gf:SetFrameLevel(30) + -- Only allow moving if setting is enabled + gf:SetMovable((ImprovedGuildWindowDB and ImprovedGuildWindowDB.allowMoveSideWindows) or false) + gf:EnableMouse(true) + gf:SetClampedToScreen(true) + gf:Hide() + + -- Background (border only, solid texture added separately) + gf:SetBackdrop({ + edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", + edgeSize = 32, + insets = { left = 11, right = 12, top = 12, bottom = 11 } + }) + gf:SetBackdropBorderColor(1, 1, 1, 1) + + -- Create solid background texture (inside the border) + local gfBgTexture = gf:CreateTexture(nil, "BACKGROUND") + gfBgTexture:SetPoint("TOPLEFT", gf, "TOPLEFT", 11, -12) + gfBgTexture:SetPoint("BOTTOMRIGHT", gf, "BOTTOMRIGHT", -12, 11) + -- Use saved color or default to dark grey + local bgColor = {r = 0.15, g = 0.15, b = 0.15} + if ImprovedGuildWindowDB and ImprovedGuildWindowDB.bgColor then + bgColor = ImprovedGuildWindowDB.bgColor + end + gfBgTexture:SetTexture(bgColor.r, bgColor.g, bgColor.b, IGW_BG_OPACITY) + gf.bgTexture = gfBgTexture + +-- Title bar + local titleBar = CreateFrame("Frame", nil, gf) + titleBar:SetPoint("TOPLEFT", gf, "TOPLEFT", 12, -12) + titleBar:SetPoint("TOPRIGHT", gf, "TOPRIGHT", -12, -12) + titleBar:SetHeight(30) + titleBar:SetFrameLevel(gf:GetFrameLevel() + 1) + titleBar:EnableMouse(true) + titleBar:RegisterForDrag("LeftButton") + titleBar:SetScript("OnDragStart", function() + if gf:IsMovable() then + gf:StartMoving() + end + end) + titleBar:SetScript("OnDragStop", function() + if gf:IsMovable() then + gf:StopMovingOrSizing() + end + end) + + local title = titleBar:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + title:SetPoint("CENTER", titleBar, "CENTER", 0, 0) + title:SetText("Guild Info") + gf.title = title + + local closeBtn = CreateFrame("Button", nil, gf, "UIPanelCloseButton") + closeBtn:SetPoint("TOPRIGHT", gf, "TOPRIGHT", -10, -10) + closeBtn:SetFrameLevel(gf:GetFrameLevel() + 2) + closeBtn:SetScript("OnClick", function() gf:Hide() end) + + +-- Content area +local content = CreateFrame("Frame", nil, gf) +content:SetPoint("TOPLEFT", gf, "TOPLEFT", 20, -50) +content:SetPoint("BOTTOMRIGHT", gf, "BOTTOMRIGHT", -20, 20) +gf.content = content + +-- Guild name (large, centered) +local guildNameText = content:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") +guildNameText:SetPoint("TOP", content, "TOP", 0, 0) +guildNameText:SetJustifyH("CENTER") +guildNameText:SetText("Guild") +gf.guildNameText = guildNameText + +-- Guild Members (excluding alts) +local totalMembersText = content:CreateFontString(nil, "OVERLAY", "GameFontNormal") +totalMembersText:SetPoint("TOP", guildNameText, "BOTTOM", 0, -10) +totalMembersText:SetJustifyH("CENTER") +totalMembersText:SetText("Guild Members: 0") +gf.totalMembersText = totalMembersText + +-- Guild Member Alts (members with "alt" in rank) +local totalAltsText = content:CreateFontString(nil, "OVERLAY", "GameFontNormal") +totalAltsText:SetPoint("TOP", totalMembersText, "BOTTOM", 0, -5) +totalAltsText:SetJustifyH("CENTER") +totalAltsText:SetText("Guild Member Alts: 0") +gf.totalAltsText = totalAltsText + +-- Divider 1 (after member counts) +local div1 = content:CreateTexture(nil, "ARTWORK") +div1:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider") +div1:SetPoint("TOP", totalAltsText, "BOTTOM", 36, -12) +div1:SetWidth(300) +div1:SetHeight(16) + +-- MOTD +local motdLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") +motdLabel:SetPoint("TOP", div1, "BOTTOM", -36, -10) +motdLabel:SetJustifyH("CENTER") +motdLabel:SetText("Message of the Day") +gf.motdLabel = motdLabel + +local motdValue = content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") +motdValue:SetPoint("TOP", motdLabel, "BOTTOM", 0, -6) +motdValue:SetWidth(210) +motdValue:SetJustifyH("CENTER") +motdValue:SetText("—") +gf.motdValue = motdValue + +-- Divider 2 (after MOTD) +local div2 = content:CreateTexture(nil, "ARTWORK") +div2:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider") +div2:SetPoint("TOP", motdValue, "BOTTOM", 36, -12) +div2:SetWidth(300) +div2:SetHeight(16) + +-- Guild Information +local infoLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") +infoLabel:SetPoint("TOP", div2, "BOTTOM", -36, -10) +infoLabel:SetJustifyH("CENTER") +infoLabel:SetText("Guild Information") +gf.infoLabel = infoLabel + +local infoValue = content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") +infoValue:SetPoint("TOP", infoLabel, "BOTTOM", 0, -6) +infoValue:SetWidth(210) +infoValue:SetJustifyH("CENTER") +infoValue:SetText("—") +gf.infoValue = infoValue + +-- Page 2 content frame (hidden by default) +local content2 = CreateFrame("Frame", nil, gf) +content2:SetPoint("TOPLEFT", gf, "TOPLEFT", 20, -50) +content2:SetPoint("BOTTOMRIGHT", gf, "BOTTOMRIGHT", -20, 50) +content2:Hide() +gf.content2 = content2 + +-- Page 2 - Class Distribution +local classLabel = content2:CreateFontString(nil, "OVERLAY", "GameFontNormal") +classLabel:SetPoint("TOP", content2, "TOP", 0, 0) +classLabel:SetJustifyH("CENTER") +classLabel:SetText("Class Distribution") +classLabel:SetTextColor(1, 1, 1) +gf.classLabel = classLabel + +-- Container for class bars (reduced height) +local classBarsContainer = CreateFrame("Frame", nil, content2) +classBarsContainer:SetPoint("TOP", classLabel, "BOTTOM", 0, -4) +classBarsContainer:SetWidth(210) +classBarsContainer:SetHeight(120) +gf.classBarsContainer = classBarsContainer + +-- Store bars for updating +gf.classBars = {} + +-- Divider before Level 60 Class Distribution +local page2Div1 = content2:CreateTexture(nil, "ARTWORK") +page2Div1:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider") +page2Div1:SetPoint("TOP", classBarsContainer, "BOTTOM", 36, -8) +page2Div1:SetWidth(300) +page2Div1:SetHeight(16) + +-- Level 60 Class Distribution +local class60Label = content2:CreateFontString(nil, "OVERLAY", "GameFontNormal") +class60Label:SetPoint("TOP", page2Div1, "BOTTOM", -36, -4) +class60Label:SetJustifyH("CENTER") +class60Label:SetText("Level 60 Class Distribution") +class60Label:SetTextColor(1, 1, 1) +gf.class60Label = class60Label + +-- Container for level 60 class bars +local class60BarsContainer = CreateFrame("Frame", nil, content2) +class60BarsContainer:SetPoint("TOP", class60Label, "BOTTOM", 0, -4) +class60BarsContainer:SetWidth(210) +class60BarsContainer:SetHeight(120) +gf.class60BarsContainer = class60BarsContainer + +-- Store bars for updating +gf.class60Bars = {} + +-- Divider before Officers +local page2Div2 = content2:CreateTexture(nil, "ARTWORK") +page2Div2:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider") +page2Div2:SetPoint("TOP", class60BarsContainer, "BOTTOM", 36, -8) +page2Div2:SetWidth(300) +page2Div2:SetHeight(16) + +-- Officers Online +local officersLabel = content2:CreateFontString(nil, "OVERLAY", "GameFontNormal") +officersLabel:SetPoint("TOP", page2Div2, "BOTTOM", -36, -4) +officersLabel:SetJustifyH("CENTER") +officersLabel:SetText("Officers Online") +officersLabel:SetTextColor(1, 1, 1) +gf.officersLabel = officersLabel + +local officersValue = content2:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") +officersValue:SetPoint("TOP", officersLabel, "BOTTOM", 0, -4) +officersValue:SetWidth(210) +officersValue:SetJustifyH("CENTER") +officersValue:SetText("—") +gf.officersValue = officersValue + + +-- Page 3 content frame (hidden by default) +local content3 = CreateFrame("Frame", nil, gf) +content3:SetPoint("TOPLEFT", gf, "TOPLEFT", 20, -50) +content3:SetPoint("BOTTOMRIGHT", gf, "BOTTOMRIGHT", -20, 50) +content3:Hide() +gf.content3 = content3 + +-- Page 3 - Max Level Crafters Online +local craftersTitle = content3:CreateFontString(nil, "OVERLAY", "GameFontNormal") +craftersTitle:SetPoint("TOP", content3, "TOP", 0, 0) +craftersTitle:SetJustifyH("CENTER") +craftersTitle:SetText("Max Level Crafters Online") +gf.craftersTitle = craftersTitle + +-- Instruction text (green) +local instructionText = content3:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") +instructionText:SetPoint("TOP", craftersTitle, "BOTTOM", 0, -6) +instructionText:SetJustifyH("CENTER") +instructionText:SetTextColor(0, 1, 0) -- Green +instructionText:SetText("Click Player to Whisper") + +-- Fixed content area for crafters (not scrollable, centered) +local craftersContent = CreateFrame("Frame", nil, content3) +craftersContent:SetPoint("TOP", instructionText, "BOTTOM", 0, -6) +craftersContent:SetWidth(210) +craftersContent:SetPoint("BOTTOM", content3, "BOTTOM", 0, 50) +gf.craftersContent = craftersContent + +-- Page 4 content frame (hidden by default) +local content4 = CreateFrame("Frame", nil, gf) +content4:SetPoint("TOPLEFT", gf, "TOPLEFT", 20, -50) +content4:SetPoint("BOTTOMRIGHT", gf, "BOTTOMRIGHT", -20, 50) +content4:Hide() +gf.content4 = content4 + +-- Page 4 - Suggested Dungeons +local dungeonsTitle = content4:CreateFontString(nil, "OVERLAY", "GameFontNormal") +dungeonsTitle:SetPoint("TOP", content4, "TOP", 0, 0) +dungeonsTitle:SetJustifyH("CENTER") +dungeonsTitle:SetText("Suggested Dungeons") +dungeonsTitle:SetTextColor(1, 1, 1) +gf.dungeonsTitle = dungeonsTitle + +local dungeonsInstruction = content4:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") +dungeonsInstruction:SetPoint("TOP", dungeonsTitle, "BOTTOM", 0, -4) +dungeonsInstruction:SetWidth(210) +dungeonsInstruction:SetJustifyH("CENTER") +dungeonsInstruction:SetText("Based on online guild members' levels") +dungeonsInstruction:SetTextColor(0.8, 0.8, 0.8) +gf.dungeonsInstruction = dungeonsInstruction + +-- Container for dungeons (no scroll, shows all matching) +local dungeonsContent = CreateFrame("Frame", nil, content4) +dungeonsContent:SetPoint("TOPLEFT", dungeonsInstruction, "BOTTOMLEFT", 0, -8) +dungeonsContent:SetPoint("TOPRIGHT", dungeonsInstruction, "BOTTOMRIGHT", 0, -8) +dungeonsContent:SetPoint("BOTTOM", content4, "BOTTOM", 0, 50) +gf.dungeonsContent = dungeonsContent + +-- Pagination buttons at bottom (match tab button styling) +local buttonHeight = 25 -- Match tab button height +local prevBtn = CreateFrame("Button", nil, gf, "UIPanelButtonTemplate") +prevBtn:SetPoint("BOTTOMLEFT", gf, "BOTTOMLEFT", 15, 15) +prevBtn:SetWidth(30) +prevBtn:SetHeight(buttonHeight) +prevBtn:SetText("<") +prevBtn:SetScript("OnClick", function() + if gf.currentPage == 2 then + gf.currentPage = 1 + gf.content:Show() + gf.content2:Hide() + gf.content3:Hide() + gf.content4:Hide() + gf.pageIndicator:SetText("Page 1 / 4") + elseif gf.currentPage == 3 then + gf.currentPage = 2 + gf.content:Hide() + gf.content2:Show() + gf.content3:Hide() + gf.content4:Hide() + gf.pageIndicator:SetText("Page 2 / 4") + elseif gf.currentPage == 4 then + gf.currentPage = 3 + gf.content:Hide() + gf.content2:Hide() + gf.content3:Show() + gf.content4:Hide() + IGW:UpdateCraftersPage() + gf.pageIndicator:SetText("Page 3 / 4") + end +end) +gf.prevBtn = prevBtn + +local nextBtn = CreateFrame("Button", nil, gf, "UIPanelButtonTemplate") +nextBtn:SetPoint("BOTTOMRIGHT", gf, "BOTTOMRIGHT", -15, 15) +nextBtn:SetWidth(30) +nextBtn:SetHeight(buttonHeight) +nextBtn:SetText(">") +nextBtn:SetScript("OnClick", function() + if gf.currentPage == 1 then + gf.currentPage = 2 + gf.content:Hide() + gf.content2:Show() + gf.content3:Hide() + gf.content4:Hide() + gf.pageIndicator:SetText("Page 2 / 4") + elseif gf.currentPage == 2 then + gf.currentPage = 3 + gf.content:Hide() + gf.content2:Hide() + gf.content3:Show() + gf.content4:Hide() + IGW:UpdateCraftersPage() + gf.pageIndicator:SetText("Page 3 / 4") + elseif gf.currentPage == 3 then + gf.currentPage = 4 + gf.content:Hide() + gf.content2:Hide() + gf.content3:Hide() + gf.content4:Show() + IGW:UpdateDungeonsPage() + gf.pageIndicator:SetText("Page 4 / 4") + end +end) +gf.nextBtn = nextBtn + +-- Page indicator (vertically centered with buttons) +local pageIndicator = gf:CreateFontString(nil, "OVERLAY", "GameFontNormal") +pageIndicator:SetPoint("BOTTOM", gf, "BOTTOM", 0, 15 + (buttonHeight / 2)) +pageIndicator:SetText("Page 1 / 4") +gf.pageIndicator = pageIndicator + +-- Initialize to page 1 +gf.currentPage = 1 + + IGW.infoFrame = gf + end + + local gf = IGW.infoFrame + if gf:IsVisible() then + gf:Hide() + -- Un-highlight tab4 button + frame.tab4:SetBackdropColor(0.2, 0.2, 0.2, 1) + frame.tab4Text:SetTextColor(0.7, 0.7, 0.7) + else + gf:ClearAllPoints() + gf:SetPoint("TOPRIGHT", frame, "TOPLEFT", -5, 0) + gf:SetHeight(frame:GetHeight()) + -- Highlight tab4 button + frame.tab4:SetBackdropColor(0.5, 0.5, 0.5, 1) + frame.tab4Text:SetTextColor(1, 1, 1) + -- Update texture with saved color and opacity + if gf.bgTexture then + local bgColor = {r = 0.15, g = 0.15, b = 0.15} + if ImprovedGuildWindowDB and ImprovedGuildWindowDB.bgColor then + bgColor = ImprovedGuildWindowDB.bgColor + end + gf.bgTexture:SetTexture(bgColor.r, bgColor.g, bgColor.b, IGW_BG_OPACITY) + end + IGW:UpdateGuildInfoWindow() + gf:Show() + end +end + +-- Toggle Options window +function IGW:ToggleOptionsWindow() + if not frame then return end + + -- Create if needed + if not IGW.optionsFrame then + local of = CreateFrame("Frame", "IGW_OptionsFrame", UIParent) + of:SetWidth(650) -- Same width as main window + of:SetHeight(500) -- Same height as main window + of:SetFrameStrata("DIALOG") + of:SetFrameLevel(50) + of:SetMovable(true) + of:EnableMouse(true) + of:SetClampedToScreen(true) + of:Hide() + + -- Add to special frames for ESC key support + IGW_AddToSpecialFrames("IGW_OptionsFrame", true) + + -- Border first + of:SetBackdrop({ + edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", + edgeSize = 32, + insets = { left = 11, right = 12, top = 12, bottom = 11 } + }) + of:SetBackdropBorderColor(1, 1, 1, 1) + + -- Create solid background texture (inside the border) + local bgTexture = of:CreateTexture(nil, "BACKGROUND") + bgTexture:SetPoint("TOPLEFT", of, "TOPLEFT", 11, -12) + bgTexture:SetPoint("BOTTOMRIGHT", of, "BOTTOMRIGHT", -12, 11) + -- Use saved color or default to dark grey, always 90% opaque for options + local bgColor = {r = 0.15, g = 0.15, b = 0.15} + if ImprovedGuildWindowDB and ImprovedGuildWindowDB.bgColor then + bgColor = ImprovedGuildWindowDB.bgColor + end + bgTexture:SetTexture(bgColor.r, bgColor.g, bgColor.b, 0.9) + of.bgTexture = bgTexture + + -- Title bar + local titleBar = CreateFrame("Frame", nil, of) + titleBar:SetPoint("TOPLEFT", of, "TOPLEFT", 12, -12) + titleBar:SetPoint("TOPRIGHT", of, "TOPRIGHT", -12, -12) + titleBar:SetHeight(30) + titleBar:SetFrameLevel(of:GetFrameLevel() + 1) + titleBar:EnableMouse(true) + titleBar:RegisterForDrag("LeftButton") + titleBar:SetScript("OnDragStart", function() of:StartMoving() end) + titleBar:SetScript("OnDragStop", function() of:StopMovingOrSizing() end) + + local title = titleBar:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + title:SetPoint("CENTER", titleBar, "CENTER", 0, 0) + title:SetText("Improved Guild Window - Options") + + local closeBtn = CreateFrame("Button", nil, of, "UIPanelCloseButton") + closeBtn:SetPoint("TOPRIGHT", of, "TOPRIGHT", -10, -10) + closeBtn:SetFrameLevel(of:GetFrameLevel() + 2) + closeBtn:SetScript("OnClick", function() of:Hide() end) + + -- Content area + local content = CreateFrame("Frame", nil, of) + content:SetPoint("TOPLEFT", of, "TOPLEFT", 20, -50) + content:SetPoint("BOTTOMRIGHT", of, "BOTTOMRIGHT", -20, 20) + of.content = content + + local yOffset = 0 + + -- Visual Settings Section + local visualHeader = content:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + visualHeader:SetPoint("TOPLEFT", content, "TOPLEFT", 0, yOffset) + visualHeader:SetText("Visual Settings") + yOffset = yOffset - 25 + + -- Opacity Slider + local opacityLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal") + opacityLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 10, yOffset) + opacityLabel:SetText("Window Opacity:") + + local opacitySlider = CreateFrame("Slider", "IGW_OpacitySlider", content, "OptionsSliderTemplate") + opacitySlider:SetPoint("TOPLEFT", opacityLabel, "BOTTOMLEFT", 0, -10) + opacitySlider:SetWidth(300) + opacitySlider:SetHeight(15) + opacitySlider:SetMinMaxValues(0.3, 1.0) + opacitySlider:SetValueStep(0.05) + opacitySlider:SetValue(IGW_BG_OPACITY) + getglobal("IGW_OpacitySliderLow"):SetText("30%") + getglobal("IGW_OpacitySliderHigh"):SetText("100%") + getglobal("IGW_OpacitySliderText"):SetText(string.format("%.0f%%", IGW_BG_OPACITY * 100)) + + opacitySlider:SetScript("OnValueChanged", function() + local value = this:GetValue() + getglobal("IGW_OpacitySliderText"):SetText(string.format("%.0f%%", value * 100)) + -- Will be applied on save + end) + of.opacitySlider = opacitySlider + + -- Color options label (to the right of slider, moved up) + local colorLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal") + colorLabel:SetPoint("LEFT", opacitySlider, "RIGHT", 40, 30) + colorLabel:SetText("Background Color:") + + -- Define 8 color options + local colorOptions = { + {r = 0.15, g = 0.15, b = 0.15, name = "Dark Grey"}, -- Default + {r = 0.05, g = 0.05, b = 0.05, name = "Black"}, + {r = 0.1, g = 0.1, b = 0.2, name = "Dark Blue"}, + {r = 0.2, g = 0.15, b = 0.1, name = "Dark Brown"}, + {r = 0.15, g = 0.2, b = 0.15, name = "Dark Green"}, + {r = 0.2, g = 0.1, b = 0.15, name = "Dark Purple"}, + {r = 0.2, g = 0.15, b = 0.15, name = "Dark Red"}, + {r = 0.1, g = 0.15, b = 0.15, name = "Dark Cyan"} + } + + -- Current selected color (default to first option) + of.selectedColor = 1 + + -- Create clickable color boxes + local colorBoxes = {} + for i, color in ipairs(colorOptions) do + local box = CreateFrame("Button", nil, content) + box:SetPoint("LEFT", colorLabel, "BOTTOMLEFT", (i-1) * 32, -30) + box:SetWidth(30) + box:SetHeight(30) + + -- Store color info on box + box.colorName = color.name + box.colorIndex = i + + -- Border first + box:SetBackdrop({ + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 } + }) + box:SetBackdropBorderColor(0.5, 0.5, 0.5, 1) + + -- Background color (inset to stay within border) + local bgTex = box:CreateTexture(nil, "BACKGROUND") + bgTex:SetPoint("TOPLEFT", box, "TOPLEFT", 4, -4) + bgTex:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -4, 4) + bgTex:SetTexture(color.r, color.g, color.b, 1) + box.bgTex = bgTex + + -- Selection indicator (white border when selected) + box.selected = false + + box:SetScript("OnClick", function() + -- Deselect all + for j, b in ipairs(colorBoxes) do + b:SetBackdropBorderColor(0.5, 0.5, 0.5, 1) + b.selected = false + end + -- Select this one + this:SetBackdropBorderColor(1, 1, 1, 1) + this.selected = true + of.selectedColor = this.colorIndex + end) + + box:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:SetText(this.colorName) + GameTooltip:Show() + end) + + box:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + + colorBoxes[i] = box + end + + -- Preselect the saved color or default to first + local savedColor = {r = 0.15, g = 0.15, b = 0.15} + if ImprovedGuildWindowDB and ImprovedGuildWindowDB.bgColor then + savedColor = ImprovedGuildWindowDB.bgColor + end + local selectedIndex = 1 + for i, color in ipairs(colorOptions) do + -- Compare with small tolerance for floating point + local rMatch = math.abs(color.r - savedColor.r) < 0.01 + local gMatch = math.abs(color.g - savedColor.g) < 0.01 + local bMatch = math.abs(color.b - savedColor.b) < 0.01 + if rMatch and gMatch and bMatch then + selectedIndex = i + break + end + end + colorBoxes[selectedIndex]:SetBackdropBorderColor(1, 1, 1, 1) + colorBoxes[selectedIndex].selected = true + of.selectedColor = selectedIndex + + of.colorOptions = colorOptions + of.colorBoxes = colorBoxes + + yOffset = yOffset - 65 + + -- Divider + local divider1 = content:CreateTexture(nil, "ARTWORK") + divider1:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider") + divider1:SetPoint("TOP", content, "TOP", 103, yOffset) + divider1:SetWidth(830) + divider1:SetHeight(16) + yOffset = yOffset - 30 + + -- Window Behavior Section + local behaviorHeader = content:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + behaviorHeader:SetPoint("TOPLEFT", content, "TOPLEFT", 0, yOffset) + behaviorHeader:SetText("Window Behavior") + yOffset = yOffset - 25 + + -- Remember Open Windows Checkbox + local rememberWindowsCheck = CreateFrame("CheckButton", nil, content, "UICheckButtonTemplate") + rememberWindowsCheck:SetPoint("TOPLEFT", content, "TOPLEFT", 10, yOffset) + rememberWindowsCheck:SetWidth(24) + rememberWindowsCheck:SetHeight(24) + rememberWindowsCheck:SetChecked((ImprovedGuildWindowDB and ImprovedGuildWindowDB.rememberWindows) or false) + + local rememberWindowsLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal") + rememberWindowsLabel:SetPoint("LEFT", rememberWindowsCheck, "RIGHT", 5, 0) + rememberWindowsLabel:SetText("Remember which windows are open") + + of.rememberWindowsCheck = rememberWindowsCheck + yOffset = yOffset - 25 + + -- Allow Moving Side Windows Checkbox + local allowMoveSideCheck = CreateFrame("CheckButton", nil, content, "UICheckButtonTemplate") + allowMoveSideCheck:SetPoint("TOPLEFT", content, "TOPLEFT", 10, yOffset) + allowMoveSideCheck:SetWidth(24) + allowMoveSideCheck:SetHeight(24) + allowMoveSideCheck:SetChecked((ImprovedGuildWindowDB and ImprovedGuildWindowDB.allowMoveSideWindows) or false) + + local allowMoveSideLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal") + allowMoveSideLabel:SetPoint("LEFT", allowMoveSideCheck, "RIGHT", 5, 0) + allowMoveSideLabel:SetText("Allow moving side windows (Member Details / Guild Info)") + + of.allowMoveSideCheck = allowMoveSideCheck + yOffset = yOffset - 25 + + -- Show Offline by Default Checkbox + local showOfflineDefaultCheck = CreateFrame("CheckButton", nil, content, "UICheckButtonTemplate") + showOfflineDefaultCheck:SetPoint("TOPLEFT", content, "TOPLEFT", 10, yOffset) + showOfflineDefaultCheck:SetWidth(24) + showOfflineDefaultCheck:SetHeight(24) + showOfflineDefaultCheck:SetChecked((ImprovedGuildWindowDB and ImprovedGuildWindowDB.showOffline ~= false)) + + local showOfflineDefaultLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal") + showOfflineDefaultLabel:SetPoint("LEFT", showOfflineDefaultCheck, "RIGHT", 5, 0) + showOfflineDefaultLabel:SetText("Show offline members by default (Member Details tab)") + + of.showOfflineDefaultCheck = showOfflineDefaultCheck + yOffset = yOffset - 25 + + -- Remember Sorting Checkbox + local rememberSortingCheck = CreateFrame("CheckButton", nil, content, "UICheckButtonTemplate") + rememberSortingCheck:SetPoint("TOPLEFT", content, "TOPLEFT", 10, yOffset) + rememberSortingCheck:SetWidth(24) + rememberSortingCheck:SetHeight(24) + rememberSortingCheck:SetChecked((ImprovedGuildWindowDB and ImprovedGuildWindowDB.rememberSorting ~= false)) + + local rememberSortingLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal") + rememberSortingLabel:SetPoint("LEFT", rememberSortingCheck, "RIGHT", 5, 0) + rememberSortingLabel:SetText("Remember sorting (column and direction)") + + of.rememberSortingCheck = rememberSortingCheck + yOffset = yOffset - 30 + + -- Divider + local divider2 = content:CreateTexture(nil, "ARTWORK") + divider2:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider") + divider2:SetPoint("TOP", content, "TOP", 103, yOffset) + divider2:SetWidth(830) + divider2:SetHeight(16) + yOffset = yOffset - 25 + + -- Default View Section + local defaultViewHeader = content:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + defaultViewHeader:SetPoint("TOPLEFT", content, "TOPLEFT", 0, yOffset) + defaultViewHeader:SetText("Default View") + yOffset = yOffset - 25 + + -- Default Tab Label + local defaultTabLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal") + defaultTabLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 10, yOffset) + defaultTabLabel:SetText("Default tab on open:") + yOffset = yOffset - 25 + + -- Radio Buttons for Default Tab + local guildMembersRadio = CreateFrame("CheckButton", nil, content, "UIRadioButtonTemplate") + guildMembersRadio:SetPoint("TOPLEFT", content, "TOPLEFT", 20, yOffset) + guildMembersRadio:SetWidth(20) + guildMembersRadio:SetHeight(20) + + local guildMembersLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal") + guildMembersLabel:SetPoint("LEFT", guildMembersRadio, "RIGHT", 5, 0) + guildMembersLabel:SetText("Guild Members (online only)") + + yOffset = yOffset - 25 + + local memberDetailsRadio = CreateFrame("CheckButton", nil, content, "UIRadioButtonTemplate") + memberDetailsRadio:SetPoint("TOPLEFT", content, "TOPLEFT", 20, yOffset) + memberDetailsRadio:SetWidth(20) + memberDetailsRadio:SetHeight(20) + + local memberDetailsLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal") + memberDetailsLabel:SetPoint("LEFT", memberDetailsRadio, "RIGHT", 5, 0) + memberDetailsLabel:SetText("Notes & Rank (all members)") + + -- Set initial radio button states + local defaultTab = (ImprovedGuildWindowDB and ImprovedGuildWindowDB.defaultTab) or "details" + if defaultTab == "details" then + guildMembersRadio:SetChecked(true) + memberDetailsRadio:SetChecked(false) + elseif defaultTab == "detailed" then + -- If set to detailed view, default to guild members for now (no radio option yet) + guildMembersRadio:SetChecked(true) + memberDetailsRadio:SetChecked(false) + else + guildMembersRadio:SetChecked(false) + memberDetailsRadio:SetChecked(true) + end + + -- Radio button scripts + guildMembersRadio:SetScript("OnClick", function() + guildMembersRadio:SetChecked(true) + memberDetailsRadio:SetChecked(false) + end) + + memberDetailsRadio:SetScript("OnClick", function() + guildMembersRadio:SetChecked(false) + memberDetailsRadio:SetChecked(true) + end) + + of.guildMembersRadio = guildMembersRadio + of.memberDetailsRadio = memberDetailsRadio + + -- Author Label + local authorLabel = content:CreateFontString(nil, "OVERLAY", "GameFontWhite") + authorLabel:SetPoint("BOTTOM", of, "BOTTOM", 0, 50) + authorLabel:SetText("Created by: Olzon") + + -- Save Button + local saveBtn = CreateFrame("Button", nil, of, "UIPanelButtonTemplate") + saveBtn:SetPoint("BOTTOM", of, "BOTTOM", 0, 15) + saveBtn:SetWidth(100) + saveBtn:SetHeight(25) + saveBtn:SetText("Save") + saveBtn:SetScript("OnClick", function() + IGW:SaveOptions() + of:Hide() + end) + + IGW.optionsFrame = of + end + + local of = IGW.optionsFrame + if of:IsVisible() then + of:Hide() + else + -- Update background color before showing + if of.bgTexture then + local bgColor = {r = 0.15, g = 0.15, b = 0.15} + if ImprovedGuildWindowDB and ImprovedGuildWindowDB.bgColor then + bgColor = ImprovedGuildWindowDB.bgColor + end + of.bgTexture:SetTexture(bgColor.r, bgColor.g, bgColor.b, 0.9) + end + -- Anchor top of options window to top of main window + of:ClearAllPoints() + of:SetPoint("TOP", frame, "TOP", 0, 0) + of:Show() + end +end + + +-- Save Options from settings window +function IGW:SaveOptions() + local of = IGW.optionsFrame + if not of then return end + + -- Initialize DB if it doesn't exist + if not ImprovedGuildWindowDB then + ImprovedGuildWindowDB = {} + end + + -- Save opacity + local opacityValue = of.opacitySlider:GetValue() + IGW_BG_OPACITY = opacityValue + ImprovedGuildWindowDB.opacity = opacityValue + + -- Apply opacity and color to all windows immediately + local selectedColor = of.colorOptions and of.colorOptions[of.selectedColor or 1] + if selectedColor then + if frame and frame.bgTexture then + frame.bgTexture:SetTexture(selectedColor.r, selectedColor.g, selectedColor.b, opacityValue) + end + if IGW.detailsFrame and IGW.detailsFrame.bgTexture then + IGW.detailsFrame.bgTexture:SetTexture(selectedColor.r, selectedColor.g, selectedColor.b, opacityValue) + end + if IGW.infoFrame and IGW.infoFrame.bgTexture then + IGW.infoFrame.bgTexture:SetTexture(selectedColor.r, selectedColor.g, selectedColor.b, opacityValue) + end + -- Apply to options window (always 90% opaque) + if of and of.bgTexture then + of.bgTexture:SetTexture(selectedColor.r, selectedColor.g, selectedColor.b, 0.9) + end + + -- Save background color + ImprovedGuildWindowDB.bgColor = {r = selectedColor.r, g = selectedColor.g, b = selectedColor.b} + end + + -- Save remember windows setting + ImprovedGuildWindowDB.rememberWindows = of.rememberWindowsCheck:GetChecked() == 1 + + -- Save allow moving side windows setting + ImprovedGuildWindowDB.allowMoveSideWindows = of.allowMoveSideCheck:GetChecked() == 1 + + -- Apply movable state to side windows immediately + if IGW.detailsFrame then + IGW.detailsFrame:SetMovable(ImprovedGuildWindowDB.allowMoveSideWindows) + end + if IGW.infoFrame then + IGW.infoFrame:SetMovable(ImprovedGuildWindowDB.allowMoveSideWindows) + end + + -- Save show offline default setting and apply immediately + ImprovedGuildWindowDB.showOffline = of.showOfflineDefaultCheck:GetChecked() == 1 + showOffline = ImprovedGuildWindowDB.showOffline + if frame and frame.offlineCheck then + frame.offlineCheck:SetChecked(showOffline) + end + + -- Save remember sorting setting + ImprovedGuildWindowDB.rememberSorting = of.rememberSortingCheck:GetChecked() == 1 + + -- Save which windows are currently open (if remember is enabled) + if ImprovedGuildWindowDB.rememberWindows then + ImprovedGuildWindowDB.windowStates = { + detailsOpen = IGW.detailsFrame and IGW.detailsFrame:IsVisible() or false, + infoOpen = IGW.infoFrame and IGW.infoFrame:IsVisible() or false + } + end + + -- Save default tab + if of.guildMembersRadio:GetChecked() then + ImprovedGuildWindowDB.defaultTab = "details" + else + ImprovedGuildWindowDB.defaultTab = "roster" + end + + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00Options saved!|r") +end + + +-- Slash command +SLASH_IMPROVEDGUILDWINDOW1 = "/igw" +SLASH_IMPROVEDGUILDWINDOW2 = "/improvedguild" +SlashCmdList["IMPROVEDGUILDWINDOW"] = function(msg) + msg = string.lower(msg or "") + if msg == "" or msg == "show" or msg == "toggle" then + IGW:ToggleWindow() + elseif msg == "hide" then + frame:Hide() + elseif msg == "debug" then + DEFAULT_CHAT_FRAME:AddMessage("=== IGW Debug Info ===") + DEFAULT_CHAT_FRAME:AddMessage("Total roster data: " .. table.getn(rosterData)) + DEFAULT_CHAT_FRAME:AddMessage("Current filterRank: " .. filterRank) + DEFAULT_CHAT_FRAME:AddMessage("Num ranks: " .. GuildControlGetNumRanks()) + + -- Count members by rank + local rankCounts = {} + for i = 0, 9 do rankCounts[i] = 0 end + for i, m in ipairs(rosterData) do + if m.rankIndex then + rankCounts[m.rankIndex] = (rankCounts[m.rankIndex] or 0) + 1 + end + end + + DEFAULT_CHAT_FRAME:AddMessage("Members per rank:") + for i = 1, GuildControlGetNumRanks() do + local rankName = GuildControlGetRankName(i) + local rankIdx = i - 1 + DEFAULT_CHAT_FRAME:AddMessage(string.format(" %s (idx %d): %d members", + rankName, rankIdx, rankCounts[rankIdx] or 0)) + end + + DEFAULT_CHAT_FRAME:AddMessage("First 5 members:") + for i = 1, math.min(5, table.getn(rosterData)) do + local m = rosterData[i] + DEFAULT_CHAT_FRAME:AddMessage(string.format(" %s: rank=%s, rankIndex=%s", + m.name or "nil", m.rank or "nil", tostring(m.rankIndex or "nil"))) + end + else + DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00Improved Guild Window Commands:|r") + DEFAULT_CHAT_FRAME:AddMessage("/igw - Toggle window") + DEFAULT_CHAT_FRAME:AddMessage("/igw show - Show window") + DEFAULT_CHAT_FRAME:AddMessage("/igw hide - Hide window") + DEFAULT_CHAT_FRAME:AddMessage("/igw debug - Show debug info") + end +end + +-- Global entry point for other addon files (e.g. calendar switching back) +function IGW_ToggleWindow() + IGW:ToggleWindow() +end + +-- Initialize addon +IGW:OnLoad() + +-- Keybind note: Users can set Shift+O in Key Bindings menu under "Improved Guild Window" \ No newline at end of file diff --git a/ImprovedGuildWindow_OC.toc b/ImprovedGuildWindow_OC.toc new file mode 100644 index 0000000..dac238b --- /dev/null +++ b/ImprovedGuildWindow_OC.toc @@ -0,0 +1,9 @@ +## Interface: 11200 +## Title: Improved Guild Window +## Notes: Enhanced guild management for World of Warcraft 1.12.1 (Octo) +## Author: Olzon +## Version: 3.1 +## SavedVariables: ImprovedGuildWindowDB + +Bindings.xml +ImprovedGuildWindow_OC.lua diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a3c6fa --- /dev/null +++ b/README.md @@ -0,0 +1,354 @@ +# Improved Guild Window + +Enhanced guild management for World of Warcraft 1.12.1 (Turtle WoW). + +![A](https://github.com/user-attachments/assets/17f01d09-fcb2-4c93-be6b-f589f8871850) + +## Side Panel Pages +![Side](https://github.com/user-attachments/assets/3152a431-8f0b-4f0d-a86f-8a656d25ed71) + +## Installation + +1. Extract `ImprovedGuildWindow` folder to `World of Warcraft\Interface\AddOns\` +2. Restart WoW or type `/reload` +3. Open with `/igw` or set keybind: ESC → Key Bindings → Improved Guild Window + +## Features + +### Four View Modes +- **Guild Members** - Online members only with location/zone info +- **Notes & Rank** - All members with public notes, officer notes, and last online +- **Detailed View** - All members with date joined and timezone information +- **Guild Info** - Statistics and guild information (side panel) + +### Main Filter Bar +- **Search** - Quick filter by name, class, rank, or notes +- **Rank** - Filter by guild rank +- **Region** - Filter by timezone region (Americas, Oceania, Europe, Asia) +- **Show Offline** - Toggle offline member visibility + +### Advanced Search +Click "Advanced Search" for additional filters: +- Public Note search +- Officer Note search +- All filters work together +- "Clear / Refresh" button resets all filters and updates roster + +### Member Details Panel (Right Side) +Click any member name to view: +- Full member information with race, faction, date joined, and timezone +- Edit public/officer notes (if permitted) +- Other Characters list (requires "Alt of [Name]" in player notes) +- Quick Whisper and Invite buttons + +### Guild Info Panel (Left Side) + +**Page 1:** +- Guild name +- Guild Members count (excludes members with "Alt" in rank name) +- Guild Member Alts count (members with "Alt" in rank name) +- Message of the Day +- Guild information text + +**Page 2:** +- Class distribution bar graph (all levels) +- Level 60 class distribution bar graph +- Online officers list + +**Page 3:** +- Max level crafters online (300+ skill) +- Grouped by profession (clickable names to whisper) +- Shows: Alchemy, Blacksmithing, Enchanting, Engineering, Jewelcrafting, Leatherworking, Tailoring + +**Page 4:** +- Suggested Dungeons based on online members' levels +- Shows up to 22 dungeons sorted by minimum level +- Only shows dungeons with at least 4 online members in range +- Displays player count for each dungeon +- Color-coded by your character's level: green (in range), yellow (±3 levels), red (too low), gray (too high) +- Includes all 31 Turtle WoW dungeons (vanilla + custom content) + +### Options Menu +Click the "Options" button to customize: +- **Background Color** - Choose from 8 color themes +- **Window Opacity** - Adjust transparency (30%-100%) +- **Remember Windows** - Restore open panels between sessions +- **Allow Moving Side Windows** - Toggle lock/unlock for side panels +- **Default View** - Set which tab opens by default +- **Show Offline by Default** - Member Details tab offline visibility +- **Remember Sorting** - Persist column and direction across sessions + +### Sorting & Filtering +- Click column headers to sort +- Default sort: Level descending on first click +- Rank dropdown filter +- Show/Hide offline members toggle +- Settings persist per-tab when "Remember Sorting" is enabled + +## Commands + +- `/igw` - Toggle window +- `/igw show` - Show window +- `/igw hide` - Hide window +- `/igw debug` - Show debug info + +## Officer Note Formatting Guide + +The addon extracts race, faction, date joined, and timezone information from officer notes using a standardized format. This enables enhanced features in the Detailed View tab, Member Details panel, and Region filter. + +### Complete Format + +**Pattern:** `RACE-MMDDYYTTT` + +- **RACE** = 1-2 letter race code followed by dash (-) +- **MMDDYY** = 6-digit date (Month/Day/Year) +- **TTT** = 3-letter timezone code + +**Examples:** +- `Or-030125PST` = Orc player who joined March 1st, 2025 in PST timezone +- `NE-121524EST` = Night Elf who joined December 15th, 2024 in EST timezone + +### Supported Race Codes + +Both single-letter and two-letter codes are supported for clarity: + +**Alliance:** +- **NE-** or **N-** = Night Elf +- **Dw-** or **D-** = Dwarf +- **Hu-** or **H-** = Human +- **Gn-** or **G-** = Gnome +- **HE-** or **He-** = High Elf + +**Horde:** +- **Or-** or **O-** = Orc +- **Tr-** or **T-** = Troll +- **Ta-** = Tauren +- **Un-** or **U-** = Undead +- **Go-** = Goblin + +*Note: Codes are case-insensitive (e.g., HE-, He-, or he- all work)* + +### Supported Timezone Codes + +**Americas:** +- PST (UTC-8), PDT (UTC-7) +- MST (UTC-7), MDT (UTC-6) +- CST (UTC-6), CDT (UTC-5) +- EST (UTC-5), EDT (UTC-4) + +**Oceania:** +- AEST (UTC+10) - Australian Eastern +- NZST (UTC+12) - New Zealand + +**Europe:** +- GMT (UTC+0), UTC (UTC+0) +- CET (UTC+1), EET (UTC+2) + +**Asia:** +- MSK (UTC+3) - Moscow +- IST (UTC+5:30) - India +- JST (UTC+9) - Japan + +### Flexible Formatting + +The addon supports partial formats - you can include only the information you want to track: + +**Full Format:** +- `Or-030125PST` = Race + Date + Timezone +- `NE-121524EST` = Race + Date + Timezone + +**Date + Timezone (no race):** +- `030125PST` = Joined March 1st, 2025 in PST + +**Race + Date (no timezone):** +- `Hu-030125` = Human who joined March 1st, 2025 + +**Race + Timezone (no date):** +- `Dw-PST` = Dwarf player in PST timezone + +**Timezone Only:** +- `PST` = Player in PST timezone (useful for alts) + +**Race Only:** +- `Un-` = Undead player + +### Additional Text + +You can add any additional text after the formatted section: + +- `Or-030125PST Main Tank` ✓ +- `NE- 300 BS/300 LW` ✓ +- `HE- Alt of Mainchar` ✓ +- `Hu-PST Officer` ✓ + +The addon will extract the structured data and preserve all text in the officer note field. + +### Officer Note Best Practices + +1. **Use consistent formatting** across all members for easier management +2. **Main characters** should have full format: `RACE-MMDDYYTTT` (e.g., `Or-030125PST`) +3. **Alt characters** can use minimal format: `RACE-TTT` or just `RACE-` (e.g., `Un-EST` or `Dw-`) +4. **Timezone-only** works well for members who don't want to share join date +5. **Add rank/role info** after the formatted section: `NE-030125PST MT` +6. **Profession info** can be in public notes for crafter detection + +### What Gets Displayed + +**Guild Members Tab:** +- Race column (e.g., "Orc") +- Faction column (A/H with color) + +**Detailed View Tab:** +- Date Joined column (e.g., "03/01/25" or "—") +- Time Zone column (e.g., "PST (UTC-8)" or "—") + +**Member Details Panel:** +- Full member information including all extracted data +- Race shown in "Level: 60 Orc Warrior" format +- Faction icon (Alliance/Horde emblem) +- Date Joined and Time Zone as separate fields + +**Region Filter:** +- Automatically maps timezone to region +- Filter members by: Americas, Oceania, Europe, Asia + +## Configuration + +### Crafter Detection (Page 3) + +Add profession info to character **public notes** for automatic detection on Guild Info Page 3. + +**Supported formats:** +- `BS 300` or `Blacksmithing 300` +- `300 JC` or `300 Jewelcrafting` +- `Alchemy: 300` or `Alch 300` +- Multiple professions: `300 JC/300 LW` + +**Supported abbreviations:** +- **Alchemy**: Alchemy, Alch, Alc +- **Blacksmithing**: Blacksmithing, Blacksmith, Smith, BS, B.S. +- **Enchanting**: Enchanting, Enchant, Ench, Enc +- **Engineering**: Engineering, Engineer, Eng +- **Jewelcrafting**: Jewelcrafting, Jewel, JC, J.C. +- **Leatherworking**: Leatherworking, Leather, LW, L.W. +- **Tailoring**: Tailoring, Tailor, Tail + +Detection requires: +- Skill level 300 or higher +- Player must be online +- Case-insensitive matching + +### Officer Detection + +Edit `OFFICER_RANK_THRESHOLD` in the .lua file (default: 2) +- 0 = Guild Master only +- 1 = Guild Master + first officer rank +- 2 = Guild Master + top 2 ranks (default) + +### Alt Detection + +Add "Alt of [MainName]" to character **public notes** for automatic detection. +Works bidirectionally - view any character to see their alts and main. + +## Version + +**3.1** - Latest stable release + +### Changelog + +**3.1** +- Added clickable dungeon names on Guild Info Page 4 +- Dungeon details popup window with: + - Full dungeon names (e.g., "Scarlet Monastery - Graveyard") + - Location and faction information + - Level range and online player count + - Dungeon description and boss list + - Loading screen backgrounds (175% zoom, 35% overlay) + - Kalimdor fallback background for custom dungeons +- Enhanced race code system: + - Added 2-letter race codes (Hu-, NE-, Dw-, Un-, Or-, Tr-, Gn-) + - Backwards compatible with 1-letter codes + - Case-insensitive support +- Dungeon dialog closes with ESC key or when main window closes +- All text centered in dungeon info window + +**3.0** +- Removed all Calendar features (experimental features discontinued) +- Cleaned up codebase for improved stability +- Guild Info Page 1 now shows separate counts: + - "Guild Members" (excludes ranks with "Alt") + - "Guild Member Alts" (ranks containing "Alt") +- Timezone extraction now works independently of date field +- Support for timezone-only officer notes (e.g., just "PST") +- Comprehensive officer note formatting guide added to README + +**2.9** +- Renamed "Member Details" tab to "Notes & Rank" +- Added new "Detailed View" tab showing: Name, Level, Rank, Date Joined, Time Zone + - Date Joined and Time Zone extracted from officer note format (MMDDYYTTT) + - Timezone displays as "PST (UTC-8)" format + - Defaults to sorting by name ascending + - Shows all members (online and offline) +- Added Region filter dropdown to main filter bar + - Filter options: All, Americas, Oceania, Europe, Asia + - Maps timezones to regions automatically + - Always visible (not part of advanced search) +- Reorganized filter layout + - Moved Clear/Refresh button to advanced search row (right edge) + - Moved Region dropdown to main filter row (next to Rank) + - Evenly spaced 4 elements on filter bar: Search, Rank, Region, Show Offline +- Reorganized main tabs at bottom + - All 4 tabs now evenly distributed across bottom width + - Order: Guild Info → Guild Members → Notes & Rank → Detailed View + - Consistent spacing between all tabs +- Removed Name field from Advanced Search (redundant with main search) +- Fixed timezone/date sorting (prevented nil comparison errors) +- Fixed spacing in Member Details side panel (all rows now 20px spacing) +- Remember Sorting option now properly applies to all 3 main tabs +- Header arrows update when switching tabs to show current sort state + +**2.8** +- Added Guild Info Page 4: Suggested Dungeons +- Enhanced Guild Info Page 2 with Level 60 class distribution +- Fixed keybinding error +- Consistent formatting across all side panel pages + +**2.7** +- Added "Show offline members by default" option +- Added "Remember sorting" option +- Level column defaults to descending on first click +- Clear/Refresh button resets sorting +- Improved sorting persistence + +**2.4** +- Enhanced member details panel layout +- Improved window positioning and drag handling + +**2.3** +- Added Goblin (Go-) and High Elf (He-) race support +- Fixed "Remember Windows" setting +- Default WoW note edit dialogs +- 10 total races now supported + +**2.2** +- Added Race and Faction columns to Guild Members tab +- Race detection from officer notes (dash format) +- Faction column shows "A" (Alliance) or "H" (Horde) +- Clickable crafter names on Guild Info Page 3 + +**2.1** +- Added Guild Info Page 3: Max Level Crafters Online +- 7 crafting professions supported +- Flexible profession detection from public notes + +**2.0** +- Complete visual overhaul with solid backgrounds +- Customizable colors (8 themes) +- Adjustable opacity per window (30-100%) +- Options menu with comprehensive settings +- Remember open windows between sessions +- Movable/lockable side panels + +## Author + +Olzon