7 Commits

Author SHA1 Message Date
2025f4e0d2 yes
All checks were successful
Build and Release / Build (push) Successful in 30s
Add support for glasses as a bonus equipment slot

- Introduce ItemCategory.Glasses and add a "Glasses" tab to the UI
- Load and display glasses items with filtering and virtual scrolling
- Allow users to preview, select, and apply glasses to their character
- Add interop for setting bonus items (glasses) via Glamourer API
- Update Apply Gear Set logic and icon handling for glasses
- Improve filter caching to support per-category filters
2026-01-04 01:21:22 +02:00
cd847a4fb7 Fix refresh issue
All checks were successful
Build and Release / Build (push) Successful in 35s
2026-01-03 13:41:29 +02:00
c9d2b9ce94 Integrate Allagan Tools for item info in UI
Added AT interop to check plugin availability and trigger `/moreinfo` command for selected items. UI now displays a button next to each item category label to open more information via AT, with tooltips and error handling if AT is not installed. Initialization for AT interop added to plugin startup.
2026-01-03 02:39:55 +02:00
f6d06a652d Rename project from GlamourBrowser 2026-01-02 23:59:37 +02:00
935a89847f Add dye selection functionality and related UI updates
All checks were successful
Build and Release / Build (push) Successful in 25s
2026-01-01 14:30:20 +02:00
7baa5965e4 Merge branch 'main' of ssh://gitea.lozy.pink:22222/lozy360/GlamourBrowser
All checks were successful
Build and Release / Build (push) Successful in 25s
2026-01-01 13:55:50 +02:00
667c54bc83 Reorder item categories and update related logic for gloves and body items 2026-01-01 13:55:44 +02:00
10 changed files with 521 additions and 44 deletions

3
.gitignore vendored
View File

@@ -367,3 +367,6 @@ FodyWeavers.xsd
Glamaholic/.github/ Glamaholic/.github/
.idea/ .idea/
/.claude
/.claude/settings.local.json
/nul

View File

@@ -1,7 +1,7 @@
using Dalamud.Game.Command; using Dalamud.Game.Command;
using System; using System;
namespace Glamaholic { namespace GlamourBrowser {
internal class Commands : IDisposable { internal class Commands : IDisposable {
private Plugin Plugin { get; } private Plugin Plugin { get; }

View File

@@ -3,7 +3,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace Glamaholic { namespace GlamourBrowser {
[Serializable] [Serializable]
internal class Configuration : IPluginConfiguration { internal class Configuration : IPluginConfiguration {
private const int CURRENT_VERSION = 1; private const int CURRENT_VERSION = 1;

View File

@@ -5,14 +5,19 @@ using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
namespace Glamaholic { namespace GlamourBrowser {
internal class DataCache { internal class DataCache {
public static Lazy<ImmutableList<Item>> EquippableItems { get; } = public static Lazy<ImmutableList<Item>> EquippableItems { get; } =
new(() => Service.DataManager.GetExcelSheet<Item>(ClientLanguage.English)! new(() => Service.DataManager.GetExcelSheet<Item>(ClientLanguage.English)!
.Where(row => row.EquipSlotCategory.RowId != 0 && .Where(row => row.EquipSlotCategory.RowId != 0 &&
row.EquipSlotCategory.Value!.SoulCrystal == 0) row.EquipSlotCategory.Value!.SoulCrystal == 0)
.ToImmutableList()); .ToImmutableList());
/*
public static Lazy<ImmutableList<Glasses>> GlassesItems { get; } =
new(() => Service.DataManager.GetExcelSheet<Glasses>(ClientLanguage.English)!
.Where(row => row.Name.ByteLength > 0)
.ToImmutableList());
public static Lazy<ImmutableDictionary<string, byte>> StainLookup { get; } = public static Lazy<ImmutableDictionary<string, byte>> StainLookup { get; } =
new (() => new (() =>
Service.DataManager.GetExcelSheet<Stain>(ClientLanguage.English)! Service.DataManager.GetExcelSheet<Stain>(ClientLanguage.English)!
@@ -20,7 +25,17 @@ namespace Glamaholic {
.ToImmutableDictionary(static row => .ToImmutableDictionary(static row =>
row.Name.ExtractText().Trim().ToLower(), static row => (byte) row.RowId)); row.Name.ExtractText().Trim().ToLower(), static row => (byte) row.RowId));
public static Lazy<ImmutableList<(byte Id, string Name, uint Color, bool Gloss)>> AllStains { get; } =
new(() => Service.DataManager.GetExcelSheet<Stain>(ClientLanguage.English)!
.Where(row => row.RowId != 0 && !row.Name.IsEmpty)
.Select(row => ((byte)row.RowId, row.Name.ExtractText(), SeColorToRgba(row.Color), row.IsMetallic))
.ToImmutableList());
// Convert SE's BGR color format to RGBA
private static uint SeColorToRgba(uint color)
=> ((color & 0xFF) << 16) | ((color >> 16) & 0xFF) | (color & 0xFF00) | 0xFF000000;
public static int GetNumStainSlots(uint itemId) => public static int GetNumStainSlots(uint itemId) =>
Service.DataManager.GetExcelSheet<Item>(ClientLanguage.English)!.GetRowOrDefault(itemId)?.DyeCount ?? 0;*/ Service.DataManager.GetExcelSheet<Item>(ClientLanguage.English)!.GetRowOrDefault(itemId)?.DyeCount ?? 0;
} }
} }

View File

@@ -0,0 +1,60 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Services;
using HeightAdjuster.Interop.Glamourer;
using System;
using System.Collections.Generic;
namespace GlamourBrowser.Interop {
internal class AT {
private static bool Initialized { get; set; } = false;
private static bool Available { get; set; } = false;
private static ICommandManager _commandManager;
private static IChatGui _chatGui;
public static void Initialize(ICommandManager commandManager, IDalamudPluginInterface pluginInterface, IChatGui chatGui) {
if (Initialized)
return;
_commandManager = commandManager;
_chatGui = chatGui;
Initialized = true;
RefreshStatus(pluginInterface);
}
public static void RefreshStatus(IDalamudPluginInterface pluginInterface) {
var prev = Available;
Available = false;
foreach (var plugin in pluginInterface.InstalledPlugins) {
if (plugin.InternalName == "InventoryTools") {
Available = plugin.IsLoaded;
break;
}
}
if (prev == Available)
return;
}
public static void OpenMoreInformationSub(string itemId) {
if (IsAvailable()) {
_commandManager.ProcessCommand("/moreinfo " + itemId);
} else {
_chatGui.PrintError("Allagan Tools was not detected, please install it to get more information about this item.");
}
}
public static bool IsAvailable() {
return Available;
}
}
}

View File

@@ -5,9 +5,10 @@ using HeightAdjuster.Interop.Glamourer;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Glamaholic.Interop { namespace GlamourBrowser.Interop {
internal class Glamourer { internal class Glamourer {
private static SetItem _SetItem { get; set; } = null!; private static SetItem _SetItem { get; set; } = null!;
private static SetBonusItem _SetBonusItem { get; set; } = null!;
private static GetState _GetState { get; set; } = null!; private static GetState _GetState { get; set; } = null!;
private static RevertState _RevertState { get; set; } = null!; private static RevertState _RevertState { get; set; } = null!;
@@ -64,11 +65,23 @@ namespace Glamaholic.Interop {
}; };
} }
public static void SetBonusItem(int playerIndex, ApiBonusSlot slot, uint itemId) {
if (!IsAvailable())
return;
try {
Service.Framework.Run(() => {
_SetBonusItem.Invoke(playerIndex, slot, itemId);
});
} catch (Exception) { }
}
public static void Initialize(IDalamudPluginInterface pluginInterface) { public static void Initialize(IDalamudPluginInterface pluginInterface) {
if (Initialized) if (Initialized)
return; return;
_SetItem = new SetItem(pluginInterface); _SetItem = new SetItem(pluginInterface);
_SetBonusItem = new SetBonusItem(pluginInterface);
_GetState = new GetState(pluginInterface); _GetState = new GetState(pluginInterface);
_RevertState = new RevertState(pluginInterface); _RevertState = new RevertState(pluginInterface);

View File

@@ -2,7 +2,7 @@ using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using System; using System;
namespace Glamaholic { namespace GlamourBrowser {
public class Plugin : IDalamudPlugin { public class Plugin : IDalamudPlugin {
internal static string Name => "GlamourBrowser"; internal static string Name => "GlamourBrowser";
@@ -22,6 +22,7 @@ namespace Glamaholic {
this.Commands = new Commands(this); this.Commands = new Commands(this);
Interop.Glamourer.Initialize(Service.Interface); Interop.Glamourer.Initialize(Service.Interface);
Interop.AT.Initialize(Service.CommandManager, Service.Interface, Service.ChatGui);
Service.Framework.Update += OnFrameworkUpdate; Service.Framework.Update += OnFrameworkUpdate;
} }
@@ -31,6 +32,7 @@ namespace Glamaholic {
return; return;
Interop.Glamourer.RefreshStatus(Service.Interface); Interop.Glamourer.RefreshStatus(Service.Interface);
Interop.AT.RefreshStatus(Service.Interface);
LastInteropCheckTime = now; LastInteropCheckTime = now;
} }

View File

@@ -1,8 +1,8 @@
using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps;
using Glamaholic.Ui; using GlamourBrowser.Ui;
using System; using System;
namespace Glamaholic { namespace GlamourBrowser {
internal class PluginUi : IDisposable { internal class PluginUi : IDisposable {
internal Plugin Plugin { get; } internal Plugin Plugin { get; }
@@ -31,7 +31,7 @@ namespace Glamaholic {
this.MainInterface.Toggle(); this.MainInterface.Toggle();
} }
internal IDalamudTextureWrap? GetIcon(ushort id) { internal IDalamudTextureWrap? GetIcon(uint id) {
var icon = Service.TextureProvider.GetFromGameIcon(new Dalamud.Interface.Textures.GameIconLookup(id)).GetWrapOrDefault(); var icon = Service.TextureProvider.GetFromGameIcon(new Dalamud.Interface.Textures.GameIconLookup(id)).GetWrapOrDefault();
return icon; return icon;
} }

View File

@@ -3,7 +3,7 @@ using Dalamud.IoC;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
namespace Glamaholic { namespace GlamourBrowser {
internal class Service { internal class Service {
[PluginService] internal static IPluginLog Log { get; private set; } = null!; [PluginService] internal static IPluginLog Log { get; private set; } = null!;

View File

@@ -1,5 +1,5 @@
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Glamourer.Api.Enums;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -7,7 +7,7 @@ using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
namespace Glamaholic.Ui { namespace GlamourBrowser.Ui {
internal class MainInterface { internal class MainInterface {
internal const int IconSize = 48; internal const int IconSize = 48;
private const int ItemsPerRow = 5; private const int ItemsPerRow = 5;
@@ -18,10 +18,11 @@ namespace Glamaholic.Ui {
private enum ItemCategory { private enum ItemCategory {
Head, Head,
Gloves,
Body, Body,
Gloves,
Legs, Legs,
Feet, Feet,
Glasses,
} }
private PluginUi Ui { get; } private PluginUi Ui { get; }
@@ -29,12 +30,16 @@ namespace Glamaholic.Ui {
private Dictionary<ItemCategory, Item?> SelectedItems { get; set; } = new(); private Dictionary<ItemCategory, Item?> SelectedItems { get; set; } = new();
private Dictionary<ItemCategory, List<Item>> FilteredItemsCache { get; set; } = new(); private Dictionary<ItemCategory, List<Item>> FilteredItemsCache { get; set; } = new();
private Dictionary<ItemCategory, float> ScrollPositions { get; set; } = new(); private Dictionary<ItemCategory, float> ScrollPositions { get; set; } = new();
private Dictionary<ItemCategory, byte> SelectedStain1 { get; set; } = new();
private Dictionary<ItemCategory, byte> SelectedStain2 { get; set; } = new();
private Glasses? SelectedGlasses { get; set; } = null;
private bool _visible; private bool _visible;
private string _itemFilter = string.Empty; private string _itemFilter = string.Empty;
private ItemCategory _currentCategory = ItemCategory.Head; private ItemCategory _currentCategory = ItemCategory.Head;
private ItemCategory _previousCategory = ItemCategory.Head; private ItemCategory _previousCategory = ItemCategory.Head;
private string _lastFilterUsed = string.Empty; private Dictionary<ItemCategory, string> _lastFilterUsed = new();
private string _dyeFilter = string.Empty;
internal MainInterface(PluginUi ui) { internal MainInterface(PluginUi ui) {
this.Ui = ui; this.Ui = ui;
@@ -44,10 +49,23 @@ namespace Glamaholic.Ui {
private void InitializeSelectedItems() { private void InitializeSelectedItems() {
SelectedItems[ItemCategory.Head] = null; SelectedItems[ItemCategory.Head] = null;
SelectedItems[ItemCategory.Gloves] = null;
SelectedItems[ItemCategory.Body] = null; SelectedItems[ItemCategory.Body] = null;
SelectedItems[ItemCategory.Gloves] = null;
SelectedItems[ItemCategory.Legs] = null; SelectedItems[ItemCategory.Legs] = null;
SelectedItems[ItemCategory.Feet] = null; SelectedItems[ItemCategory.Feet] = null;
// Initialize dye selections (0 = no dye)
SelectedStain1[ItemCategory.Head] = 0;
SelectedStain1[ItemCategory.Body] = 0;
SelectedStain1[ItemCategory.Gloves] = 0;
SelectedStain1[ItemCategory.Legs] = 0;
SelectedStain1[ItemCategory.Feet] = 0;
SelectedStain2[ItemCategory.Head] = 0;
SelectedStain2[ItemCategory.Body] = 0;
SelectedStain2[ItemCategory.Gloves] = 0;
SelectedStain2[ItemCategory.Legs] = 0;
SelectedStain2[ItemCategory.Feet] = 0;
} }
private void LoadItemsByCategory() { private void LoadItemsByCategory() {
@@ -64,17 +82,6 @@ namespace Glamaholic.Ui {
.OrderBy(item => item.RowId) .OrderBy(item => item.RowId)
.ToImmutableList(); .ToImmutableList();
ItemsByCategory[ItemCategory.Gloves] = equippableItems
.Where(item => {
if (!item.EquipSlotCategory.IsValid) {
return false;
}
var equipSlot = item.EquipSlotCategory.Value;
return equipSlot.Gloves > 0;
})
.OrderBy(item => item.RowId)
.ToImmutableList();
ItemsByCategory[ItemCategory.Body] = equippableItems ItemsByCategory[ItemCategory.Body] = equippableItems
.Where(item => { .Where(item => {
if (!item.EquipSlotCategory.IsValid) { if (!item.EquipSlotCategory.IsValid) {
@@ -86,6 +93,17 @@ namespace Glamaholic.Ui {
.OrderBy(item => item.RowId) .OrderBy(item => item.RowId)
.ToImmutableList(); .ToImmutableList();
ItemsByCategory[ItemCategory.Gloves] = equippableItems
.Where(item => {
if (!item.EquipSlotCategory.IsValid) {
return false;
}
var equipSlot = item.EquipSlotCategory.Value;
return equipSlot.Gloves > 0;
})
.OrderBy(item => item.RowId)
.ToImmutableList();
ItemsByCategory[ItemCategory.Legs] = equippableItems ItemsByCategory[ItemCategory.Legs] = equippableItems
.Where(item => { .Where(item => {
if (!item.EquipSlotCategory.IsValid) { if (!item.EquipSlotCategory.IsValid) {
@@ -110,16 +128,26 @@ namespace Glamaholic.Ui {
} }
private List<Item> GetFilteredItems() { private List<Item> GetFilteredItems() {
// Glasses are handled separately, so return empty list if on Glasses tab
if (_currentCategory == ItemCategory.Glasses) {
return new List<Item>();
}
var filter = this._itemFilter.ToLowerInvariant(); var filter = this._itemFilter.ToLowerInvariant();
if (!FilteredItemsCache.ContainsKey(_currentCategory) || _lastFilterUsed != _itemFilter) { // Check if we need to recalculate the filter for this category
bool needsRecalculation = !FilteredItemsCache.ContainsKey(_currentCategory) ||
!_lastFilterUsed.ContainsKey(_currentCategory) ||
_lastFilterUsed[_currentCategory] != _itemFilter;
if (needsRecalculation) {
var items = ItemsByCategory[_currentCategory]; var items = ItemsByCategory[_currentCategory];
var filtered = string.IsNullOrEmpty(this._itemFilter) var filtered = string.IsNullOrEmpty(this._itemFilter)
? items.ToList() ? items.ToList()
: items.Where(item => item.Name.ExtractText().ToLowerInvariant().Contains(filter)).ToList(); : items.Where(item => item.Name.ExtractText().ToLowerInvariant().Contains(filter)).ToList();
FilteredItemsCache[_currentCategory] = filtered; FilteredItemsCache[_currentCategory] = filtered;
_lastFilterUsed = _itemFilter; _lastFilterUsed[_currentCategory] = _itemFilter;
} }
return FilteredItemsCache[_currentCategory]; return FilteredItemsCache[_currentCategory];
@@ -204,8 +232,8 @@ namespace Glamaholic.Ui {
} }
private unsafe void DrawSelectedGearDisplay() { private unsafe void DrawSelectedGearDisplay() {
var categories = new[] { ItemCategory.Head, ItemCategory.Gloves, ItemCategory.Body, ItemCategory.Legs, ItemCategory.Feet }; var categories = new[] { ItemCategory.Head, ItemCategory.Body, ItemCategory.Gloves, ItemCategory.Legs, ItemCategory.Feet };
var categoryLabels = new[] { "Hat", "Gloves", "Top", "Bottom", "Shoes" }; var categoryLabels = new[] { "Hat", "Top", "Gloves", "Bottom", "Shoes" };
for (int i = 0; i < categories.Length; i++) { for (int i = 0; i < categories.Length; i++) {
var category = categories[i]; var category = categories[i];
@@ -214,6 +242,19 @@ namespace Glamaholic.Ui {
ImGui.TextUnformatted(label); ImGui.TextUnformatted(label);
ImGui.SameLine();
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(2, 2));
if (ImGui.Button($"?##info-{label}", new Vector2(20, 20))) {
if (selectedItem.HasValue) {
Interop.AT.OpenMoreInformationSub(selectedItem.Value.RowId.ToString());
}
}
ImGui.PopStyleVar();
if (ImGui.IsItemHovered()) {
ImGui.SetTooltip("Click to see item location");
}
var drawCursor = ImGui.GetCursorScreenPos(); var drawCursor = ImGui.GetCursorScreenPos();
var slotSize = SelectedGearIconSize + SelectedGearPaddingSize; var slotSize = SelectedGearIconSize + SelectedGearPaddingSize;
var borderColour = *ImGui.GetStyleColorVec4(ImGuiCol.Border); var borderColour = *ImGui.GetStyleColorVec4(ImGuiCol.Border);
@@ -241,6 +282,24 @@ namespace Glamaholic.Ui {
} }
ImGui.TextUnformatted(selectedItem.Value.Name.ExtractText()); ImGui.TextUnformatted(selectedItem.Value.Name.ExtractText());
// Draw dye controls - always show both slots
ImGui.PushItemWidth(-1);
// Primary dye
if (DrawDyeCombo($"##dye1-{label}", SelectedStain1, category, true)) {
// Apply the dye change immediately
this.ApplyItemToCharacter(selectedItem.Value, category);
}
// Secondary dye
if (DrawDyeCombo($"##dye2-{label}", SelectedStain2, category, false)) {
// Apply the dye change immediately
this.ApplyItemToCharacter(selectedItem.Value, category);
}
ImGui.PopItemWidth();
} else { } else {
ImGui.SetCursorPos(cursorAfter); ImGui.SetCursorPos(cursorAfter);
ImGui.TextUnformatted("(empty)"); ImGui.TextUnformatted("(empty)");
@@ -249,6 +308,9 @@ namespace Glamaholic.Ui {
ImGui.Spacing(); ImGui.Spacing();
} }
// Draw glasses/bonus slot
DrawGlassesSlot();
ImGui.Separator(); ImGui.Separator();
ImGui.Spacing(); ImGui.Spacing();
@@ -261,6 +323,195 @@ namespace Glamaholic.Ui {
} }
} }
private unsafe void DrawGlassesSlot() {
ImGui.TextUnformatted("Glasses");
ImGui.SameLine();
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(2, 2));
if (ImGui.Button("?##info-Glasses", new Vector2(20, 20))) {
if (SelectedGlasses.HasValue) {
Interop.AT.OpenMoreInformationSub(SelectedGlasses.Value.RowId.ToString());
}
}
ImGui.PopStyleVar();
if (ImGui.IsItemHovered()) {
ImGui.SetTooltip("Click to see item location");
}
var drawCursor = ImGui.GetCursorScreenPos();
var slotSize = SelectedGearIconSize + SelectedGearPaddingSize;
var borderColour = *ImGui.GetStyleColorVec4(ImGuiCol.Border);
ImGui.GetWindowDrawList().AddRect(
drawCursor,
drawCursor + new Vector2(slotSize),
ImGui.ColorConvertFloat4ToU32(borderColour)
);
var cursorBefore = ImGui.GetCursorPos();
ImGui.InvisibleButton("gear-slot Glasses", new Vector2(slotSize));
var cursorAfter = ImGui.GetCursorPos();
if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) {
SelectedGlasses = null;
// Remove glasses from character (item ID 0 means remove/nothing)
Interop.Glamourer.SetBonusItem(0, ApiBonusSlot.Glasses, 0);
}
if (SelectedGlasses != null) {
try {
var icon = this.Ui.GetIcon((uint)SelectedGlasses.Value.Icon);
if (icon != null) {
ImGui.SetCursorPos(cursorBefore + new Vector2(SelectedGearPaddingSize / 2f));
ImGui.Image(icon.Handle, new Vector2(SelectedGearIconSize));
ImGui.SetCursorPos(cursorAfter);
}
} catch {
// Icon not found, skip rendering it
ImGui.SetCursorPos(cursorAfter);
}
ImGui.TextUnformatted(SelectedGlasses.Value.Singular.ExtractText());
} else {
ImGui.SetCursorPos(cursorAfter);
ImGui.TextUnformatted("(empty)");
}
ImGui.Spacing();
}
private unsafe bool DrawDyeCombo(string id, Dictionary<ItemCategory, byte> stainDict, ItemCategory category, bool isPrimary) {
var allStains = DataCache.AllStains.Value;
var currentStain = stainDict[category];
// Find the current stain info
string previewName = "None";
uint previewColor = 0;
bool previewGloss = false;
if (currentStain != 0) {
var stain = allStains.FirstOrDefault(s => s.Id == currentStain);
if (stain != default) {
previewName = stain.Name;
previewColor = stain.Color;
previewGloss = stain.Gloss;
}
}
// Calculate contrast color for text
var textColor = GetContrastColor(previewColor);
bool changed = false;
// Draw preview button with color
if (previewColor != 0) {
ImGui.PushStyleColor(ImGuiCol.FrameBg, previewColor);
ImGui.PushStyleColor(ImGuiCol.Text, textColor);
}
if (ImGui.BeginCombo(id, previewName, ImGuiComboFlags.HeightLarge)) {
if (previewColor != 0) {
ImGui.PopStyleColor(2);
}
// Search filter
ImGui.SetNextItemWidth(-1);
ImGui.InputTextWithHint("##dyefilter", "Search dyes...", ref _dyeFilter, 256);
ImGui.Separator();
// Filter dyes based on search
IEnumerable<(byte Id, string Name, uint Color, bool Gloss)> filteredStains = string.IsNullOrEmpty(_dyeFilter)
? allStains
: allStains.Where(s => s.Name.Contains(_dyeFilter, StringComparison.OrdinalIgnoreCase));
// None option
var noneColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 1.0f));
ImGui.PushStyleColor(ImGuiCol.Button, noneColor);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, noneColor + 0x202020);
ImGui.PushStyleColor(ImGuiCol.ButtonActive, noneColor + 0x404040);
if (ImGui.Button("None", new Vector2(-1, 0))) {
stainDict[category] = 0;
changed = true;
ImGui.CloseCurrentPopup();
}
ImGui.PopStyleColor(3);
// All filtered dyes
foreach (var stain in filteredStains) {
var stainTextColor = GetContrastColor(stain.Color);
ImGui.PushStyleColor(ImGuiCol.Button, stain.Color);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, AdjustBrightness(stain.Color, 1.1f));
ImGui.PushStyleColor(ImGuiCol.ButtonActive, AdjustBrightness(stain.Color, 0.9f));
ImGui.PushStyleColor(ImGuiCol.Text, stainTextColor);
if (ImGui.Button($"{stain.Name}##{stain.Id}", new Vector2(-1, 0))) {
stainDict[category] = stain.Id;
changed = true;
ImGui.CloseCurrentPopup();
}
// Draw gloss overlay
if (stain.Gloss) {
var drawList = ImGui.GetWindowDrawList();
var min = ImGui.GetItemRectMin();
var max = ImGui.GetItemRectMax();
drawList.AddRectFilledMultiColor(min, max,
0x50FFFFFF, 0x50000000, 0x50FFFFFF, 0x50000000);
}
// Selection indicator
if (currentStain == stain.Id) {
var drawList = ImGui.GetWindowDrawList();
var min = ImGui.GetItemRectMin();
var max = ImGui.GetItemRectMax();
drawList.AddRect(min, max, 0xFF2020D0, 0, ImDrawFlags.None, 2.0f);
}
ImGui.PopStyleColor(4);
}
ImGui.EndCombo();
} else {
if (previewColor != 0) {
ImGui.PopStyleColor(2);
}
// Draw gloss overlay on preview
if (previewGloss) {
var drawList = ImGui.GetWindowDrawList();
var min = ImGui.GetItemRectMin();
var max = ImGui.GetItemRectMax();
drawList.AddRectFilledMultiColor(min, max,
0x50FFFFFF, 0x50000000, 0x50FFFFFF, 0x50000000);
}
}
// Right-click to clear
if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && currentStain != 0) {
stainDict[category] = 0;
changed = true;
}
return changed;
}
private static uint GetContrastColor(uint rgba) {
float r = ((rgba >> 0) & 0xFF) / 255.0f;
float g = ((rgba >> 8) & 0xFF) / 255.0f;
float b = ((rgba >> 16) & 0xFF) / 255.0f;
float luminance = 0.299f * r + 0.587f * g + 0.114f * b;
return luminance > 0.5f ? 0xFF000000 : 0xFFFFFFFF;
}
private static uint AdjustBrightness(uint rgba, float factor) {
byte r = (byte)Math.Min(255, ((rgba >> 0) & 0xFF) * factor);
byte g = (byte)Math.Min(255, ((rgba >> 8) & 0xFF) * factor);
byte b = (byte)Math.Min(255, ((rgba >> 16) & 0xFF) * factor);
byte a = (byte)((rgba >> 24) & 0xFF);
return (uint)(r | (g << 8) | (b << 16) | (a << 24));
}
private void ApplySelectedGearToCharacter() { private void ApplySelectedGearToCharacter() {
if (Service.ObjectTable.LocalPlayer == null) { if (Service.ObjectTable.LocalPlayer == null) {
Service.ChatGui.PrintError("[Glamour Browser] No character found."); Service.ChatGui.PrintError("[Glamour Browser] No character found.");
@@ -278,19 +529,26 @@ namespace Glamaholic.Ui {
var slotMappings = new Dictionary<ItemCategory, Glamourer.Api.Enums.ApiEquipSlot> var slotMappings = new Dictionary<ItemCategory, Glamourer.Api.Enums.ApiEquipSlot>
{ {
{ ItemCategory.Head, Glamourer.Api.Enums.ApiEquipSlot.Head }, { ItemCategory.Head, Glamourer.Api.Enums.ApiEquipSlot.Head },
{ ItemCategory.Gloves, Glamourer.Api.Enums.ApiEquipSlot.Hands },
{ ItemCategory.Body, Glamourer.Api.Enums.ApiEquipSlot.Body }, { ItemCategory.Body, Glamourer.Api.Enums.ApiEquipSlot.Body },
{ ItemCategory.Gloves, Glamourer.Api.Enums.ApiEquipSlot.Hands },
{ ItemCategory.Legs, Glamourer.Api.Enums.ApiEquipSlot.Legs }, { ItemCategory.Legs, Glamourer.Api.Enums.ApiEquipSlot.Legs },
{ ItemCategory.Feet, Glamourer.Api.Enums.ApiEquipSlot.Feet }, { ItemCategory.Feet, Glamourer.Api.Enums.ApiEquipSlot.Feet },
}; };
foreach (var (category, glamourerSlot) in slotMappings) { foreach (var (category, glamourerSlot) in slotMappings) {
if (SelectedItems[category].HasValue) { if (SelectedItems[category] is { } item) {
var item = SelectedItems[category].Value; var stain1 = SelectedStain1[category];
Interop.Glamourer.SetItem(playerIndex, glamourerSlot, item.RowId, []); var stain2 = SelectedStain2[category];
var stains = new byte[] { stain1, stain2 };
Interop.Glamourer.SetItem(playerIndex, glamourerSlot, item.RowId, stains);
} }
} }
// Apply glasses if selected
if (SelectedGlasses.HasValue) {
Interop.Glamourer.SetBonusItem(playerIndex, Glamourer.Api.Enums.ApiBonusSlot.Glasses, SelectedGlasses.Value.RowId);
}
Service.ChatGui.Print("[Glamour Browser] Gear applied to character!"); Service.ChatGui.Print("[Glamour Browser] Gear applied to character!");
} catch (Exception ex) { } catch (Exception ex) {
Service.Log.Error(ex, "Failed to apply gear to character"); Service.Log.Error(ex, "Failed to apply gear to character");
@@ -299,12 +557,13 @@ namespace Glamaholic.Ui {
} }
private void DrawTabs() { private void DrawTabs() {
if (ImGui.BeginTabBar("item-category-tabs")) { if (ImGui.BeginTabBar("item-category-tabs2")) {
DrawTabButton("Hat", ItemCategory.Head); DrawTabButton("Hat", ItemCategory.Head);
DrawTabButton("Gloves", ItemCategory.Gloves);
DrawTabButton("Top", ItemCategory.Body); DrawTabButton("Top", ItemCategory.Body);
DrawTabButton("Gloves", ItemCategory.Gloves);
DrawTabButton("Bottom", ItemCategory.Legs); DrawTabButton("Bottom", ItemCategory.Legs);
DrawTabButton("Shoes", ItemCategory.Feet); DrawTabButton("Shoes", ItemCategory.Feet);
DrawTabButton("Glasses", ItemCategory.Glasses);
ImGui.EndTabBar(); ImGui.EndTabBar();
} }
@@ -328,7 +587,129 @@ namespace Glamaholic.Ui {
} }
} }
private void DrawGlassesGrid() {
var glassesItems = DataCache.GlassesItems.Value;
var filteredGlasses = string.IsNullOrEmpty(this._itemFilter)
? glassesItems
: glassesItems.Where(item => item.Singular.ExtractText().ToLowerInvariant().Contains(this._itemFilter.ToLowerInvariant()));
var glassesList = filteredGlasses.ToList();
if (glassesList.Count == 0) {
ImGui.TextDisabled("No glasses found");
return;
}
// Reset scroll if category changed
if (_currentCategory != _previousCategory) {
ImGui.SetScrollY(0);
_previousCategory = _currentCategory;
}
if (!ImGui.BeginTable("glasses grid", ItemsPerRow, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoKeepColumnsVisible)) {
return;
}
// Virtual scrolling
var scrollY = ImGui.GetScrollY();
var visibleHeight = ImGui.GetContentRegionAvail().Y;
var totalRows = (glassesList.Count + ItemsPerRow - 1) / ItemsPerRow;
var firstVisibleRow = Math.Max(0, (int)(scrollY / ItemHeight));
var lastVisibleRow = Math.Min(totalRows, firstVisibleRow + (int)((visibleHeight / ItemHeight) + 2));
if (firstVisibleRow > 0) {
ImGui.TableNextRow(ImGuiTableRowFlags.None, ItemHeight * firstVisibleRow);
}
for (int row = firstVisibleRow; row < lastVisibleRow; row++) {
ImGui.TableNextRow();
for (int col = 0; col < ItemsPerRow; col++) {
var itemIndex = row * ItemsPerRow + col;
ImGui.TableNextColumn();
if (itemIndex < glassesList.Count) {
this.DrawGlassesIcon(glassesList[itemIndex]);
}
}
}
ImGui.EndTable();
ImGui.Spacing();
ImGui.TextDisabled($"Showing {glassesList.Count} glasses");
}
private unsafe void DrawGlassesIcon(Glasses glasses) {
var drawCursor = ImGui.GetCursorScreenPos();
var iconSize = IconSize;
var paddingSize = PaddingSize;
ImGui.BeginGroup();
var borderColour = *ImGui.GetStyleColorVec4(ImGuiCol.Border);
ImGui.GetWindowDrawList().AddRect(
drawCursor,
drawCursor + new Vector2(iconSize + paddingSize),
ImGui.ColorConvertFloat4ToU32(borderColour)
);
var cursorBefore = ImGui.GetCursorPos();
ImGui.InvisibleButton($"glasses {glasses.RowId}", new Vector2(iconSize + paddingSize));
var cursorAfter = ImGui.GetCursorPos();
try {
var icon = this.Ui.GetIcon((uint)glasses.Icon);
if (icon != null) {
ImGui.SetCursorPos(cursorBefore + new Vector2(paddingSize / 2f));
ImGui.Image(icon.Handle, new Vector2(iconSize));
ImGui.SetCursorPos(cursorAfter);
}
} catch {
// Icon not found, skip rendering it
ImGui.SetCursorPos(cursorAfter);
}
ImGui.EndGroup();
if (ImGui.IsItemHovered()) {
ImGui.BeginTooltip();
ImGui.TextUnformatted($"ID: {glasses.RowId}");
ImGui.TextUnformatted($"Name: {glasses.Singular.ExtractText()}");
ImGui.EndTooltip();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) {
SelectedGlasses = glasses;
this.ApplyGlassesToCharacter(glasses);
}
}
private void ApplyGlassesToCharacter(Glasses glasses) {
if (Service.ObjectTable.LocalPlayer == null) {
return;
}
if (!Interop.Glamourer.IsAvailable()) {
return;
}
try {
int playerIndex = Service.ObjectTable.LocalPlayer.ObjectIndex;
Interop.Glamourer.SetBonusItem(playerIndex, Glamourer.Api.Enums.ApiBonusSlot.Glasses, glasses.RowId);
} catch (Exception ex) {
Service.Log.Error(ex, "Failed to apply glasses to character");
}
}
private void DrawItemGrid() { private void DrawItemGrid() {
// Handle glasses separately
if (_currentCategory == ItemCategory.Glasses) {
DrawGlassesGrid();
return;
}
var filteredItems = GetFilteredItems(); var filteredItems = GetFilteredItems();
if (filteredItems.Count == 0) { if (filteredItems.Count == 0) {
@@ -434,14 +815,17 @@ namespace Glamaholic.Ui {
var slotMapping = new Dictionary<ItemCategory, Glamourer.Api.Enums.ApiEquipSlot> var slotMapping = new Dictionary<ItemCategory, Glamourer.Api.Enums.ApiEquipSlot>
{ {
{ ItemCategory.Head, Glamourer.Api.Enums.ApiEquipSlot.Head }, { ItemCategory.Head, Glamourer.Api.Enums.ApiEquipSlot.Head },
{ ItemCategory.Gloves, Glamourer.Api.Enums.ApiEquipSlot.Hands },
{ ItemCategory.Body, Glamourer.Api.Enums.ApiEquipSlot.Body }, { ItemCategory.Body, Glamourer.Api.Enums.ApiEquipSlot.Body },
{ ItemCategory.Gloves, Glamourer.Api.Enums.ApiEquipSlot.Hands },
{ ItemCategory.Legs, Glamourer.Api.Enums.ApiEquipSlot.Legs }, { ItemCategory.Legs, Glamourer.Api.Enums.ApiEquipSlot.Legs },
{ ItemCategory.Feet, Glamourer.Api.Enums.ApiEquipSlot.Feet }, { ItemCategory.Feet, Glamourer.Api.Enums.ApiEquipSlot.Feet },
}; };
if (slotMapping.TryGetValue(category, out var glamourerSlot)) { if (slotMapping.TryGetValue(category, out var glamourerSlot)) {
Interop.Glamourer.SetItem(playerIndex, glamourerSlot, item.RowId, []); var stain1 = SelectedStain1[category];
var stain2 = SelectedStain2[category];
var stains = new byte[] { stain1, stain2 };
Interop.Glamourer.SetItem(playerIndex, glamourerSlot, item.RowId, stains);
} }
} catch (Exception ex) { } catch (Exception ex) {
Service.Log.Error(ex, "Failed to apply item to character"); Service.Log.Error(ex, "Failed to apply item to character");