2 Commits

Author SHA1 Message Date
a24eddeb99 added README.md file with project overview and setup instructions.
All checks were successful
Build and Release / Build (push) Successful in 29s
Removed item lookup for glasses
2026-01-04 01:31:59 +02:00
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
5 changed files with 273 additions and 8 deletions

View File

@@ -13,6 +13,11 @@ namespace GlamourBrowser {
row.EquipSlotCategory.Value!.SoulCrystal == 0)
.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; } =
new (() =>
Service.DataManager.GetExcelSheet<Stain>(ClientLanguage.English)!

View File

@@ -8,6 +8,7 @@ using System.Collections.Generic;
namespace GlamourBrowser.Interop {
internal class Glamourer {
private static SetItem _SetItem { get; set; } = null!;
private static SetBonusItem _SetBonusItem { get; set; } = null!;
private static GetState _GetState { get; set; } = null!;
private static RevertState _RevertState { get; set; } = null!;
@@ -64,11 +65,23 @@ namespace GlamourBrowser.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) {
if (Initialized)
return;
_SetItem = new SetItem(pluginInterface);
_SetBonusItem = new SetBonusItem(pluginInterface);
_GetState = new GetState(pluginInterface);
_RevertState = new RevertState(pluginInterface);

View File

@@ -31,7 +31,7 @@ namespace GlamourBrowser {
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();
return icon;
}

View File

@@ -1,5 +1,5 @@
using Dalamud.Bindings.ImGui;
using Glamourer.Api.Enums;
using Lumina.Excel.Sheets;
using System;
using System.Collections.Generic;
@@ -22,6 +22,7 @@ namespace GlamourBrowser.Ui {
Gloves,
Legs,
Feet,
Glasses,
}
private PluginUi Ui { get; }
@@ -31,12 +32,13 @@ namespace GlamourBrowser.Ui {
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 string _itemFilter = string.Empty;
private ItemCategory _currentCategory = 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) {
@@ -126,16 +128,26 @@ namespace GlamourBrowser.Ui {
}
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();
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 filtered = string.IsNullOrEmpty(this._itemFilter)
? items.ToList()
: items.Where(item => item.Name.ExtractText().ToLowerInvariant().Contains(filter)).ToList();
FilteredItemsCache[_currentCategory] = filtered;
_lastFilterUsed = _itemFilter;
_lastFilterUsed[_currentCategory] = _itemFilter;
}
return FilteredItemsCache[_currentCategory];
@@ -296,6 +308,9 @@ namespace GlamourBrowser.Ui {
ImGui.Spacing();
}
// Draw glasses/bonus slot
DrawGlassesSlot();
ImGui.Separator();
ImGui.Spacing();
@@ -308,6 +323,51 @@ namespace GlamourBrowser.Ui {
}
}
private unsafe void DrawGlassesSlot() {
ImGui.TextUnformatted("Glasses");
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];
@@ -471,6 +531,11 @@ namespace GlamourBrowser.Ui {
}
}
// 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!");
} catch (Exception ex) {
Service.Log.Error(ex, "Failed to apply gear to character");
@@ -485,6 +550,7 @@ namespace GlamourBrowser.Ui {
DrawTabButton("Gloves", ItemCategory.Gloves);
DrawTabButton("Bottom", ItemCategory.Legs);
DrawTabButton("Shoes", ItemCategory.Feet);
DrawTabButton("Glasses", ItemCategory.Glasses);
ImGui.EndTabBar();
}
@@ -508,7 +574,129 @@ namespace GlamourBrowser.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() {
// Handle glasses separately
if (_currentCategory == ItemCategory.Glasses) {
DrawGlassesGrid();
return;
}
var filteredItems = GetFilteredItems();
if (filteredItems.Count == 0) {

59
README.md Normal file
View File

@@ -0,0 +1,59 @@
# GlamourBrowser
A Dalamud plugin for Final Fantasy XIV that provides an intuitive visual browser for equipment items and glamour customization.
## Features
- **Visual Item Browser** - Browse all equippable items with icon previews organized by equipment slot
- **Real-time Preview** - Click any item to instantly apply it to your character via Glamourer
- **Dye System** - Full dye/stain control with color previews and search functionality for each equipment slot
- **Glasses Support** - Browse and apply bonus items (glasses/facewear) with proper icon display
- **Search & Filter** - Quickly find items with the built-in search that persists across category tabs
- **Allagan Tools Integration** - View detailed item information and locations directly from the browser
- **Gear Set Management** - Apply complete sets of equipment with dyes in one click
## Requirements
- **Glamourer** - Required for applying equipment changes to your character
- **Allagan Tools** (Optional) - Enhances the experience with item location lookup
## Installing
GlamourBrowser can be installed through a custom Dalamud repository:
1. Open Dalamud Settings in-game with `/xlsettings`
2. Go to "Experimental" tab
3. Add the following custom repository URL:
```
https://gitea.lozy.pink/lozy360/DalamudPluginRepo/raw/branch/main/custom.json
```
4. Click the "+" button to save
5. Open the Plugin Installer and search for "GlamourBrowser"
## Usage
### Basic Operation
1. Open the plugin with `/gbrowser` or through the plugin menu
2. Use the category tabs (Head, Body, Gloves, Legs, Feet, Glasses) to browse items
3. Click any item to instantly apply it to your character
4. Right-click items in the "Selected Gear" panel to remove them
### Dye Customization
- Each equipment slot in "Selected Gear" shows two dye dropdowns
- Click a dye dropdown to see color previews and search for specific dyes
- Right-click a dye to remove it
- Dyes are applied automatically when you change them
### Search & Filtering
- Use the search bar at the top to filter items by name
- The filter persists independently for each category tab
- Works for both regular equipment and glasses
## Credits
- **Glamourer** - For providing the API that makes equipment preview possible
- **Allagan Tools** - For item information and location lookup integration
- **Dalamud & XIVLauncher** - For the plugin framework