Files
GlamourBrowser/GlamourBrowser/Ui/MainInterface.cs

624 lines
24 KiB
C#

using Dalamud.Bindings.ImGui;
using Lumina.Excel.Sheets;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Numerics;
namespace GlamourBrowser.Ui {
internal class MainInterface {
internal const int IconSize = 48;
private const int ItemsPerRow = 5;
private const int PaddingSize = 12;
private const int SelectedGearIconSize = 48;
private const int SelectedGearPaddingSize = 12;
private const int ItemHeight = IconSize + PaddingSize + 4;
private enum ItemCategory {
Head,
Body,
Gloves,
Legs,
Feet,
}
private PluginUi Ui { get; }
private Dictionary<ItemCategory, ImmutableList<Item>> ItemsByCategory { get; set; } = new();
private Dictionary<ItemCategory, Item?> SelectedItems { get; set; } = new();
private Dictionary<ItemCategory, List<Item>> FilteredItemsCache { 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 bool _visible;
private string _itemFilter = string.Empty;
private ItemCategory _currentCategory = ItemCategory.Head;
private ItemCategory _previousCategory = ItemCategory.Head;
private string _lastFilterUsed = string.Empty;
private string _dyeFilter = string.Empty;
internal MainInterface(PluginUi ui) {
this.Ui = ui;
this.LoadItemsByCategory();
this.InitializeSelectedItems();
}
private void InitializeSelectedItems() {
SelectedItems[ItemCategory.Head] = null;
SelectedItems[ItemCategory.Body] = null;
SelectedItems[ItemCategory.Gloves] = null;
SelectedItems[ItemCategory.Legs] = 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() {
var equippableItems = DataCache.EquippableItems.Value;
ItemsByCategory[ItemCategory.Head] = equippableItems
.Where(item => {
if (!item.EquipSlotCategory.IsValid) {
return false;
}
var equipSlot = item.EquipSlotCategory.Value;
return equipSlot.Head > 0;
})
.OrderBy(item => item.RowId)
.ToImmutableList();
ItemsByCategory[ItemCategory.Body] = equippableItems
.Where(item => {
if (!item.EquipSlotCategory.IsValid) {
return false;
}
var equipSlot = item.EquipSlotCategory.Value;
return equipSlot.Body > 0;
})
.OrderBy(item => item.RowId)
.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
.Where(item => {
if (!item.EquipSlotCategory.IsValid) {
return false;
}
var equipSlot = item.EquipSlotCategory.Value;
return equipSlot.Legs > 0;
})
.OrderBy(item => item.RowId)
.ToImmutableList();
ItemsByCategory[ItemCategory.Feet] = equippableItems
.Where(item => {
if (!item.EquipSlotCategory.IsValid) {
return false;
}
var equipSlot = item.EquipSlotCategory.Value;
return equipSlot.Feet > 0;
})
.OrderBy(item => item.RowId)
.ToImmutableList();
}
private List<Item> GetFilteredItems() {
var filter = this._itemFilter.ToLowerInvariant();
if (!FilteredItemsCache.ContainsKey(_currentCategory) || _lastFilterUsed != _itemFilter) {
var items = ItemsByCategory[_currentCategory];
var filtered = string.IsNullOrEmpty(this._itemFilter)
? items.ToList()
: items.Where(item => item.Name.ExtractText().ToLowerInvariant().Contains(filter)).ToList();
FilteredItemsCache[_currentCategory] = filtered;
_lastFilterUsed = _itemFilter;
}
return FilteredItemsCache[_currentCategory];
}
internal void Open() {
this._visible = true;
}
internal void Toggle() {
this._visible ^= true;
}
internal void Draw() {
if (!this._visible) {
return;
}
ImGui.SetNextWindowSize(new Vector2(900, 700), ImGuiCond.FirstUseEver);
if (!ImGui.Begin("Glamour Browser", ref this._visible)) {
ImGui.End();
return;
}
this.DrawInner();
ImGui.End();
}
private void DrawInner() {
ImGui.SetNextItemWidth(-1);
if (ImGui.InputTextWithHint("##item-filter", "Search items...", ref this._itemFilter, 512)) {
// Filter is applied below
}
ImGui.Separator();
if (ImGui.BeginTable("main-layout", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.SizingFixedFit)) {
ImGui.TableSetupColumn("items", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("selected", ImGuiTableColumnFlags.WidthFixed, 250);
ImGui.TableNextRow();
ImGui.TableNextColumn();
this.DrawItemsSection();
ImGui.TableNextColumn();
this.DrawSelectedGearSection();
ImGui.EndTable();
}
}
private void DrawItemsSection() {
ImGui.BeginGroup();
ImGui.TextUnformatted("Browse Items");
ImGui.Separator();
this.DrawTabs();
if (ImGui.BeginChild("item list")) {
this.DrawItemGrid();
ImGui.EndChild();
}
ImGui.EndGroup();
}
private void DrawSelectedGearSection() {
ImGui.BeginGroup();
ImGui.TextUnformatted("Selected Gear");
ImGui.Separator();
if (ImGui.BeginChild("selected gear")) {
this.DrawSelectedGearDisplay();
ImGui.EndChild();
}
ImGui.EndGroup();
}
private unsafe void DrawSelectedGearDisplay() {
var categories = new[] { ItemCategory.Head, ItemCategory.Body, ItemCategory.Gloves, ItemCategory.Legs, ItemCategory.Feet };
var categoryLabels = new[] { "Hat", "Top", "Gloves", "Bottom", "Shoes" };
for (int i = 0; i < categories.Length; i++) {
var category = categories[i];
var label = categoryLabels[i];
var selectedItem = SelectedItems[category];
ImGui.TextUnformatted(label);
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 {label}", new Vector2(slotSize));
var cursorAfter = ImGui.GetCursorPos();
if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) {
SelectedItems[category] = null;
}
if (selectedItem != null) {
var icon = this.Ui.GetIcon(selectedItem.Value.Icon);
if (icon != null) {
ImGui.SetCursorPos(cursorBefore + new Vector2(SelectedGearPaddingSize / 2f));
ImGui.Image(icon.Handle, new Vector2(SelectedGearIconSize));
ImGui.SetCursorPos(cursorAfter);
}
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 {
ImGui.SetCursorPos(cursorAfter);
ImGui.TextUnformatted("(empty)");
}
ImGui.Spacing();
}
ImGui.Separator();
ImGui.Spacing();
ImGui.TextDisabled("(Items apply automatically)");
ImGui.Spacing();
if (ImGui.Button("Apply Gear Set", new Vector2(-1, 0))) {
this.ApplySelectedGearToCharacter();
}
}
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() {
if (Service.ObjectTable.LocalPlayer == null) {
Service.ChatGui.PrintError("[Glamour Browser] No character found.");
return;
}
if (!Interop.Glamourer.IsAvailable()) {
Service.ChatGui.PrintError("[Glamour Browser] Glamourer plugin is not available.");
return;
}
try {
int playerIndex = Service.ObjectTable.LocalPlayer.ObjectIndex;
var slotMappings = new Dictionary<ItemCategory, Glamourer.Api.Enums.ApiEquipSlot>
{
{ ItemCategory.Head, Glamourer.Api.Enums.ApiEquipSlot.Head },
{ ItemCategory.Body, Glamourer.Api.Enums.ApiEquipSlot.Body },
{ ItemCategory.Gloves, Glamourer.Api.Enums.ApiEquipSlot.Hands },
{ ItemCategory.Legs, Glamourer.Api.Enums.ApiEquipSlot.Legs },
{ ItemCategory.Feet, Glamourer.Api.Enums.ApiEquipSlot.Feet },
};
foreach (var (category, glamourerSlot) in slotMappings) {
if (SelectedItems[category] is { } item) {
var stain1 = SelectedStain1[category];
var stain2 = SelectedStain2[category];
var stains = new byte[] { stain1, stain2 };
Interop.Glamourer.SetItem(playerIndex, glamourerSlot, item.RowId, stains);
}
}
Service.ChatGui.Print("[Glamour Browser] Gear applied to character!");
} catch (Exception ex) {
Service.Log.Error(ex, "Failed to apply gear to character");
Service.ChatGui.PrintError("[Glamour Browser] Failed to apply gear. Check logs for details.");
}
}
private void DrawTabs() {
if (ImGui.BeginTabBar("item-category-tabs2")) {
DrawTabButton("Hat", ItemCategory.Head);
DrawTabButton("Top", ItemCategory.Body);
DrawTabButton("Gloves", ItemCategory.Gloves);
DrawTabButton("Bottom", ItemCategory.Legs);
DrawTabButton("Shoes", ItemCategory.Feet);
ImGui.EndTabBar();
}
}
private void DrawTabButton(string label, ItemCategory category) {
var isActive = _currentCategory == category;
if (isActive) {
ImGui.PushStyleColor(ImGuiCol.Tab, new Vector4(0.8f, 0f, 0f, 1f));
ImGui.PushStyleColor(ImGuiCol.TabActive, new Vector4(0.8f, 0f, 0f, 1f));
ImGui.PushStyleColor(ImGuiCol.TabHovered, new Vector4(0.9f, 0f, 0f, 1f));
}
if (ImGui.TabItemButton(label, isActive ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None)) {
_currentCategory = category;
}
if (isActive) {
ImGui.PopStyleColor(3);
}
}
private void DrawItemGrid() {
var filteredItems = GetFilteredItems();
if (filteredItems.Count == 0) {
ImGui.TextDisabled("No items found");
return;
}
// Reset scroll if category changed
if (_currentCategory != _previousCategory) {
ImGui.SetScrollY(0);
_previousCategory = _currentCategory;
}
if (!ImGui.BeginTable("item grid", ItemsPerRow, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoKeepColumnsVisible)) {
return;
}
// Virtual scrolling: Calculate visible range
var scrollY = ImGui.GetScrollY();
var visibleHeight = ImGui.GetContentRegionAvail().Y;
var totalRows = (filteredItems.Count + ItemsPerRow - 1) / ItemsPerRow;
var firstVisibleRow = Math.Max(0, (int)(scrollY / ItemHeight));
var lastVisibleRow = Math.Min(totalRows, firstVisibleRow + (int)((visibleHeight / ItemHeight) + 2));
// Reserve space for off-screen rows before visible range
if (firstVisibleRow > 0) {
ImGui.TableNextRow(ImGuiTableRowFlags.None, ItemHeight * firstVisibleRow);
}
// Render only visible items
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 < filteredItems.Count) {
this.DrawItemIcon(filteredItems[itemIndex]);
}
}
}
ImGui.EndTable();
ImGui.Spacing();
ImGui.TextDisabled($"Showing {filteredItems.Count} items");
}
private unsafe void DrawItemIcon(Item item) {
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($"item {item.RowId}", new Vector2(iconSize + paddingSize));
var cursorAfter = ImGui.GetCursorPos();
var icon = this.Ui.GetIcon(item.Icon);
if (icon != null) {
ImGui.SetCursorPos(cursorBefore + new Vector2(paddingSize / 2f));
ImGui.Image(icon.Handle, new Vector2(iconSize));
ImGui.SetCursorPos(cursorAfter);
}
ImGui.EndGroup();
if (ImGui.IsItemHovered()) {
ImGui.BeginTooltip();
ImGui.TextUnformatted($"ID: {item.RowId}");
ImGui.TextUnformatted($"Name: {item.Name.ExtractText()}");
ImGui.EndTooltip();
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) {
SelectedItems[_currentCategory] = item;
this.ApplyItemToCharacter(item, _currentCategory);
}
}
private void ApplyItemToCharacter(Item item, ItemCategory category) {
if (Service.ObjectTable.LocalPlayer == null) {
return;
}
if (!Interop.Glamourer.IsAvailable()) {
return;
}
try {
int playerIndex = Service.ObjectTable.LocalPlayer.ObjectIndex;
var slotMapping = new Dictionary<ItemCategory, Glamourer.Api.Enums.ApiEquipSlot>
{
{ ItemCategory.Head, Glamourer.Api.Enums.ApiEquipSlot.Head },
{ ItemCategory.Body, Glamourer.Api.Enums.ApiEquipSlot.Body },
{ ItemCategory.Gloves, Glamourer.Api.Enums.ApiEquipSlot.Hands },
{ ItemCategory.Legs, Glamourer.Api.Enums.ApiEquipSlot.Legs },
{ ItemCategory.Feet, Glamourer.Api.Enums.ApiEquipSlot.Feet },
};
if (slotMapping.TryGetValue(category, out var glamourerSlot)) {
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) {
Service.Log.Error(ex, "Failed to apply item to character");
}
}
internal void SwitchPlate(Guid plateId, bool scrollTo = false) {
}
}
}