Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a24eddeb99 | |||
| 2025f4e0d2 |
@@ -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)!
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,18 +128,28 @@ 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
59
README.md
Normal 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
|
||||
Reference in New Issue
Block a user