commit a5d3b2092df2fbc9de7ce7a0295ca855e01ccb00 Author: Administrator Date: Thu Jan 1 13:06:22 2026 +0200 first commit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..978ac73 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,52 @@ +name: Build and Release + +on: + push: + tags: + - "*.*.*.*" + +jobs: + Build: + runs-on: ubuntu-latest + env: + DALAMUD_HOME: /tmp/dalamud + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Set up .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 10.0.x + + - name: Download Dalamud Latest + run: | + wget https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -O ${{ env.DALAMUD_HOME }}.zip + unzip ${{ env.DALAMUD_HOME }}.zip -d ${{ env.DALAMUD_HOME }} + + - name: Restore Project + run: dotnet restore + + - name: Build Project + run: dotnet build --configuration Release GlamourBrowser/GlamourBrowser.csproj -p:AssemblyVersion=${{ github.ref_name }} + + - name: Create Release + uses: actions/create-release@v1 + id: create_release + with: + draft: false + prerelease: false + release_name: ${{ github.ref_name }} + tag_name: ${{ github.ref_name }} + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Upload Release Asset with curl + run: | + curl \ + -X POST \ + -H "Authorization: token ${{ gitea.token }}" \ + -H "Content-Type: application/zip" \ + --data-binary @bin/GlamourBrowser/latest.zip \ + "${{ steps.create_release.outputs.upload_url }}?name=latest.zip" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f79f39 --- /dev/null +++ b/.gitignore @@ -0,0 +1,369 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# Packaging +pack/ + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +Glamaholic/.github/ + +.idea/ diff --git a/GlamourBrowser.sln b/GlamourBrowser.sln new file mode 100644 index 0000000..3ec0470 --- /dev/null +++ b/GlamourBrowser.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.1.11312.151 d18.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GlamourBrowser", "GlamourBrowser\GlamourBrowser.csproj", "{83E74102-7DAC-4789-911B-C1FB9BBCC6AC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {83E74102-7DAC-4789-911B-C1FB9BBCC6AC}.Debug|x64.ActiveCfg = Debug|x64 + {83E74102-7DAC-4789-911B-C1FB9BBCC6AC}.Debug|x64.Build.0 = Debug|x64 + {83E74102-7DAC-4789-911B-C1FB9BBCC6AC}.Release|x64.ActiveCfg = Release|x64 + {83E74102-7DAC-4789-911B-C1FB9BBCC6AC}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D65BCBC3-1695-40F1-AA8D-396B135AC631} + EndGlobalSection +EndGlobal diff --git a/GlamourBrowser/.editorconfig b/GlamourBrowser/.editorconfig new file mode 100644 index 0000000..5d1a3be --- /dev/null +++ b/GlamourBrowser/.editorconfig @@ -0,0 +1,253 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_prefer_top_level_statements = true:silent + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = false +csharp_new_line_before_else = false +csharp_new_line_before_finally = false +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = none +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +[*.{cs,vb}] +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf \ No newline at end of file diff --git a/GlamourBrowser/Commands.cs b/GlamourBrowser/Commands.cs new file mode 100644 index 0000000..a1a0c82 --- /dev/null +++ b/GlamourBrowser/Commands.cs @@ -0,0 +1,24 @@ +using Dalamud.Game.Command; +using System; + +namespace Glamaholic { + internal class Commands : IDisposable { + private Plugin Plugin { get; } + + internal Commands(Plugin plugin) { + this.Plugin = plugin; + + Service.CommandManager.AddHandler("/gbrowser", new CommandInfo(this.OnCommand) { + HelpMessage = $"Toggle visibility of the {Plugin.Name} window", + }); + } + + public void Dispose() { + Service.CommandManager.RemoveHandler("/gbrowser"); + } + + private void OnCommand(string command, string arguments) { + this.Plugin.Ui.ToggleMainInterface(); + } + } +} diff --git a/GlamourBrowser/Configuration.cs b/GlamourBrowser/Configuration.cs new file mode 100644 index 0000000..05a4ae0 --- /dev/null +++ b/GlamourBrowser/Configuration.cs @@ -0,0 +1,22 @@ +using Dalamud.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Glamaholic { + [Serializable] + internal class Configuration : IPluginConfiguration { + private const int CURRENT_VERSION = 1; + + public int Version { get; set; } = CURRENT_VERSION; + + internal static Configuration LoadAndMigrate(System.IO.FileInfo fileInfo) { + if (!fileInfo.Exists) + return new Configuration(); + + return new Configuration(); + } + } + + +} diff --git a/GlamourBrowser/DataCache.cs b/GlamourBrowser/DataCache.cs new file mode 100644 index 0000000..271af9a --- /dev/null +++ b/GlamourBrowser/DataCache.cs @@ -0,0 +1,26 @@ +using Dalamud.Game; +using Lumina.Excel.Sheets; +using Lumina.Extensions; +using System; +using System.Collections.Immutable; +using System.Linq; + +namespace Glamaholic { + internal class DataCache { + public static Lazy> EquippableItems { get; } = + new(() => Service.DataManager.GetExcelSheet(ClientLanguage.English)! + .Where(row => row.EquipSlotCategory.RowId != 0 && + row.EquipSlotCategory.Value!.SoulCrystal == 0) + .ToImmutableList()); + /* + public static Lazy> StainLookup { get; } = + new (() => + Service.DataManager.GetExcelSheet(ClientLanguage.English)! + .Where(static row => row.RowId != 0 && !row.Name.IsEmpty) + .ToImmutableDictionary(static row => + row.Name.ExtractText().Trim().ToLower(), static row => (byte) row.RowId)); + + public static int GetNumStainSlots(uint itemId) => + Service.DataManager.GetExcelSheet(ClientLanguage.English)!.GetRowOrDefault(itemId)?.DyeCount ?? 0;*/ + } +} diff --git a/GlamourBrowser/GlamourBrowser.csproj b/GlamourBrowser/GlamourBrowser.csproj new file mode 100644 index 0000000..6ff82de --- /dev/null +++ b/GlamourBrowser/GlamourBrowser.csproj @@ -0,0 +1,37 @@ + + + + net10.0-windows7.0 + 1.12.1 + true + enable + false + true + true + + + + x64 + + + + $(DALAMUD_HOME) + + + + $(HOME)/dalamud + + + + + + + + + + + + + + + diff --git a/GlamourBrowser/GlamourBrowser.yaml b/GlamourBrowser/GlamourBrowser.yaml new file mode 100644 index 0000000..049b613 --- /dev/null +++ b/GlamourBrowser/GlamourBrowser.yaml @@ -0,0 +1,8 @@ +name: GlamourBrowser +author: Anna, Caitlyn +description: | + Create and save as many glamour plates as you want. Activate up to 15 of them + at once at the Glamour Dresser. Supports exporting and importing plates for + easy sharing. +punchline: Save and swap your glamour plates. +repo_url: https://github.com/caitlyn-gg/Glamaholic diff --git a/GlamourBrowser/Interop/GameData/GlamourerEquipment.cs b/GlamourBrowser/Interop/GameData/GlamourerEquipment.cs new file mode 100644 index 0000000..c89d143 --- /dev/null +++ b/GlamourBrowser/Interop/GameData/GlamourerEquipment.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; + +namespace HeightAdjuster.Interop.Glamourer; + +public class GlamourerEquipment { + public GlamourerItem MainHand = new(); + public GlamourerItem OffHand = new(); + public GlamourerItem Head = new(); + public GlamourerItem Body = new(); + public GlamourerItem Hands = new(); + public GlamourerItem Legs = new(); + public GlamourerItem Feet = new(); + public GlamourerItem Ears = new(); + public GlamourerItem Neck = new(); + public GlamourerItem Wrists = new(); + public GlamourerItem RFinger = new(); + public GlamourerItem LFinger = new(); + + + public IEnumerable<(EquipSlot slot, GlamourerItem)> Items { + get { + yield return (EquipSlot.Head, Head); + yield return (EquipSlot.Body, Body); + yield return (EquipSlot.Hands, Hands); + yield return (EquipSlot.Legs, Legs); + yield return (EquipSlot.Feet, Feet); + yield return (EquipSlot.Ears, Ears); + yield return (EquipSlot.Neck, Neck); + yield return (EquipSlot.Wrists, Wrists); + yield return (EquipSlot.RFinger, RFinger); + yield return (EquipSlot.LFinger, LFinger); + yield return (EquipSlot.MainHand, MainHand); + yield return (EquipSlot.OffHand, OffHand); + } + } +} + +public enum EquipSlot : byte { + Unknown = 0, + MainHand = 1, + OffHand = 2, + Head = 3, + Body = 4, + Hands = 5, + Belt = 6, + Legs = 7, + Feet = 8, + Ears = 9, + Neck = 10, + Wrists = 11, + RFinger = 12, + BothHand = 13, + LFinger = 14, + HeadBody = 15, + BodyHandsLegsFeet = 16, + SoulCrystal = 17, + LegsFeet = 18, + FullBody = 19, + BodyHands = 20, + BodyLegsFeet = 21, + ChestHands = 22, + ChestLegs = 23, + Nothing = 24, + All = 25, +} \ No newline at end of file diff --git a/GlamourBrowser/Interop/GameData/GlamourerItem.cs b/GlamourBrowser/Interop/GameData/GlamourerItem.cs new file mode 100644 index 0000000..91963d6 --- /dev/null +++ b/GlamourBrowser/Interop/GameData/GlamourerItem.cs @@ -0,0 +1,10 @@ +namespace HeightAdjuster.Interop.Glamourer; + +public class GlamourerItem { + public uint ItemId; + public bool Crest; + public bool ApplyStain; + public bool ApplyCrest; + public byte Stain; + public byte Stain2; +} diff --git a/GlamourBrowser/Interop/GameData/GlamourerState.cs b/GlamourBrowser/Interop/GameData/GlamourerState.cs new file mode 100644 index 0000000..8614e70 --- /dev/null +++ b/GlamourBrowser/Interop/GameData/GlamourerState.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json.Linq; + +namespace HeightAdjuster.Interop.Glamourer; + +public class GlamourerState { + public GlamourerEquipment Equipment = new(); + + public static implicit operator GlamourerState?(JObject? jObject) { + return jObject == null ? new GlamourerState() : jObject.ToObject(); + } +} diff --git a/GlamourBrowser/Interop/Glamourer.cs b/GlamourBrowser/Interop/Glamourer.cs new file mode 100644 index 0000000..1b65fff --- /dev/null +++ b/GlamourBrowser/Interop/Glamourer.cs @@ -0,0 +1,104 @@ +using Dalamud.Plugin; +using Glamourer.Api.Enums; +using Glamourer.Api.IpcSubscribers; +using HeightAdjuster.Interop.Glamourer; +using System; +using System.Collections.Generic; + +namespace Glamaholic.Interop { + internal class Glamourer { + private static SetItem _SetItem { get; set; } = null!; + private static GetState _GetState { get; set; } = null!; + private static RevertState _RevertState { get; set; } = null!; + + private static bool Initialized { get; set; } = false; + private static bool Available { get; set; } = false; + + public static void SetItem(int playerIndex, ApiEquipSlot slot, uint itemId, byte[] stains) { + if (!IsAvailable()) + return; + + try { + Service.Framework.Run(() => { + // Get current state to preserve existing stains + var (_, stateJson) = _GetState.Invoke(playerIndex); + GlamourerState? state = stateJson; + + // Extract stains from current state based on slot + byte stain1 = 0; + byte stain2 = 0; + + if (state?.Equipment != null) { + var currentItem = GetCurrentItemFromSlot(state.Equipment, slot); + if (currentItem != null) { + stain1 = currentItem.Stain; + stain2 = currentItem.Stain2; + } + } + + // Use extracted stains if no stains were provided + var stainList = stains.Length > 0 + ? new List(stains) + : new List { stain1, stain2 }; + + _SetItem.Invoke(playerIndex, slot, itemId, stainList); + }); + } catch (Exception) { } + } + + private static GlamourerItem? GetCurrentItemFromSlot(GlamourerEquipment equipment, ApiEquipSlot slot) { + return slot switch { + ApiEquipSlot.MainHand => equipment.MainHand, + ApiEquipSlot.OffHand => equipment.OffHand, + ApiEquipSlot.Head => equipment.Head, + ApiEquipSlot.Body => equipment.Body, + ApiEquipSlot.Hands => equipment.Hands, + ApiEquipSlot.Legs => equipment.Legs, + ApiEquipSlot.Feet => equipment.Feet, + ApiEquipSlot.Ears => equipment.Ears, + ApiEquipSlot.Neck => equipment.Neck, + ApiEquipSlot.Wrists => equipment.Wrists, + ApiEquipSlot.RFinger => equipment.RFinger, + ApiEquipSlot.LFinger => equipment.LFinger, + _ => null, + }; + } + + public static void Initialize(IDalamudPluginInterface pluginInterface) { + if (Initialized) + return; + + _SetItem = new SetItem(pluginInterface); + _GetState = new GetState(pluginInterface); + _RevertState = new RevertState(pluginInterface); + + Initialized = true; + + RefreshStatus(pluginInterface); + } + + public static void RefreshStatus(IDalamudPluginInterface pluginInterface) { + var prev = Available; + + Available = false; + + foreach (var plugin in pluginInterface.InstalledPlugins) { + if (plugin.Name == "Glamourer") { + Available = plugin.IsLoaded; + break; + } + } + + if (prev == Available) + return; + } + + public static bool IsAvailable() { + return Available && IsIPCValid(); + } + + public static bool IsIPCValid() { + return _SetItem.Valid && _RevertState.Valid; + } + } +} diff --git a/GlamourBrowser/Plugin.cs b/GlamourBrowser/Plugin.cs new file mode 100644 index 0000000..5442bcf --- /dev/null +++ b/GlamourBrowser/Plugin.cs @@ -0,0 +1,51 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using System; + +namespace Glamaholic { + public class Plugin : IDalamudPlugin { + internal static string Name => "GlamourBrowser"; + + internal Configuration Config { get; } + internal PluginUi Ui { get; } + private Commands Commands { get; } + + private DateTime LastInteropCheckTime { get; set; } = DateTime.Now; + +#pragma warning disable 8618 + public Plugin(IDalamudPluginInterface pluginInterface) { + pluginInterface.Create(); + + this.Config = Configuration.LoadAndMigrate(Service.Interface!.ConfigFile); + + this.Ui = new PluginUi(this); + this.Commands = new Commands(this); + + Interop.Glamourer.Initialize(Service.Interface); + Service.Framework.Update += OnFrameworkUpdate; + } + + private void OnFrameworkUpdate(IFramework framework) { + var now = DateTime.Now; + if (now.Subtract(LastInteropCheckTime).TotalSeconds < 5) + return; + + Interop.Glamourer.RefreshStatus(Service.Interface); + + LastInteropCheckTime = now; + } +#pragma warning restore 8618 + + public void Dispose() { + this.Commands.Dispose(); + this.Ui.Dispose(); + + Service.Framework.Update -= OnFrameworkUpdate; + } + + + internal void SaveConfig() { + Service.Interface.SavePluginConfig(this.Config); + } + } +} diff --git a/GlamourBrowser/PluginUi.cs b/GlamourBrowser/PluginUi.cs new file mode 100644 index 0000000..e8d6fab --- /dev/null +++ b/GlamourBrowser/PluginUi.cs @@ -0,0 +1,46 @@ +using Dalamud.Interface.Textures.TextureWraps; +using Glamaholic.Ui; +using System; + +namespace Glamaholic { + internal class PluginUi : IDisposable { + internal Plugin Plugin { get; } + + private MainInterface MainInterface { get; } + + internal PluginUi(Plugin plugin) { + this.Plugin = plugin; + this.MainInterface = new MainInterface(this); + + Service.Interface.UiBuilder.Draw += this.Draw; + Service.Interface.UiBuilder.OpenConfigUi += this.OpenMainInterface; + Service.Interface.UiBuilder.OpenMainUi += this.OpenMainInterface; + } + + public void Dispose() { + Service.Interface.UiBuilder.OpenMainUi -= this.OpenMainInterface; + Service.Interface.UiBuilder.OpenConfigUi -= this.OpenMainInterface; + Service.Interface.UiBuilder.Draw -= this.Draw; + } + + internal void OpenMainInterface() { + this.MainInterface.Open(); + } + + internal void ToggleMainInterface() { + this.MainInterface.Toggle(); + } + + internal IDalamudTextureWrap? GetIcon(ushort id) { + var icon = Service.TextureProvider.GetFromGameIcon(new Dalamud.Interface.Textures.GameIconLookup(id)).GetWrapOrDefault(); + return icon; + } + + private void Draw() { + this.MainInterface.Draw(); + } + + internal void SwitchPlate(Guid plateId, bool scrollTo = false) => + this.MainInterface.SwitchPlate(plateId, scrollTo); + } +} \ No newline at end of file diff --git a/GlamourBrowser/Service.cs b/GlamourBrowser/Service.cs new file mode 100644 index 0000000..e4e8e92 --- /dev/null +++ b/GlamourBrowser/Service.cs @@ -0,0 +1,34 @@ +using Dalamud.Game; +using Dalamud.IoC; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; + +namespace Glamaholic { + internal class Service { + [PluginService] internal static IPluginLog Log { get; private set; } = null!; + + [PluginService] internal static IDalamudPluginInterface Interface { get; private set; } = null!; + + [PluginService] internal static IChatGui ChatGui { get; private set; } = null!; + + [PluginService] internal static IClientState ClientState { get; private set; } = null!; + [PluginService] internal static IPlayerState PlayerState { get; private set; } = null!; + [PluginService] internal static IObjectTable ObjectTable { get; private set; } = null!; + + [PluginService] internal static ICommandManager CommandManager { get; private set; } = null!; + + [PluginService] internal static IDataManager DataManager { get; private set; } = null!; + + [PluginService] internal static IFramework Framework { get; private set; } = null!; + + [PluginService] internal static IGameGui GameGui { get; private set; } = null!; + + [PluginService] internal static ISigScanner SigScanner { get; private set; } = null!; + + [PluginService] internal static ITextureProvider TextureProvider { get; private set; } = null!; + + [PluginService] internal static IGameInteropProvider GameInteropProvider { get; private set; } = null!; + + [PluginService] internal static IAddonLifecycle AddonLifecycle { get; private set; } = null!; + } +} diff --git a/GlamourBrowser/Ui/MainInterface.cs b/GlamourBrowser/Ui/MainInterface.cs new file mode 100644 index 0000000..c874212 --- /dev/null +++ b/GlamourBrowser/Ui/MainInterface.cs @@ -0,0 +1,454 @@ +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 Glamaholic.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, + Gloves, + Body, + Legs, + Feet, + } + + private PluginUi Ui { get; } + private Dictionary> ItemsByCategory { get; set; } = new(); + private Dictionary SelectedItems { get; set; } = new(); + private Dictionary> FilteredItemsCache { get; set; } = new(); + private Dictionary ScrollPositions { 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; + + internal MainInterface(PluginUi ui) { + this.Ui = ui; + this.LoadItemsByCategory(); + this.InitializeSelectedItems(); + } + + private void InitializeSelectedItems() { + SelectedItems[ItemCategory.Head] = null; + SelectedItems[ItemCategory.Gloves] = null; + SelectedItems[ItemCategory.Body] = null; + SelectedItems[ItemCategory.Legs] = null; + SelectedItems[ItemCategory.Feet] = null; + } + + 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.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 + .Where(item => { + if (!item.EquipSlotCategory.IsValid) { + return false; + } + var equipSlot = item.EquipSlotCategory.Value; + return equipSlot.Body > 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 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.Gloves, ItemCategory.Body, ItemCategory.Legs, ItemCategory.Feet }; + var categoryLabels = new[] { "Hat", "Gloves", "Top", "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()); + } 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 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.Head, Glamourer.Api.Enums.ApiEquipSlot.Head }, + { ItemCategory.Gloves, Glamourer.Api.Enums.ApiEquipSlot.Hands }, + { ItemCategory.Body, Glamourer.Api.Enums.ApiEquipSlot.Body }, + { ItemCategory.Legs, Glamourer.Api.Enums.ApiEquipSlot.Legs }, + { ItemCategory.Feet, Glamourer.Api.Enums.ApiEquipSlot.Feet }, + }; + + foreach (var (category, glamourerSlot) in slotMappings) { + if (SelectedItems[category].HasValue) { + var item = SelectedItems[category].Value; + Interop.Glamourer.SetItem(playerIndex, glamourerSlot, item.RowId, []); + } + } + + 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-tabs")) { + DrawTabButton("Hat", ItemCategory.Head); + DrawTabButton("Gloves", ItemCategory.Gloves); + DrawTabButton("Top", ItemCategory.Body); + 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.Head, Glamourer.Api.Enums.ApiEquipSlot.Head }, + { ItemCategory.Gloves, Glamourer.Api.Enums.ApiEquipSlot.Hands }, + { ItemCategory.Body, Glamourer.Api.Enums.ApiEquipSlot.Body }, + { ItemCategory.Legs, Glamourer.Api.Enums.ApiEquipSlot.Legs }, + { ItemCategory.Feet, Glamourer.Api.Enums.ApiEquipSlot.Feet }, + }; + + if (slotMapping.TryGetValue(category, out var glamourerSlot)) { + Interop.Glamourer.SetItem(playerIndex, glamourerSlot, item.RowId, []); + } + } catch (Exception ex) { + Service.Log.Error(ex, "Failed to apply item to character"); + } + } + + internal void SwitchPlate(Guid plateId, bool scrollTo = false) { + } + } +} diff --git a/GlamourBrowser/packages.lock.json b/GlamourBrowser/packages.lock.json new file mode 100644 index 0000000..0642416 --- /dev/null +++ b/GlamourBrowser/packages.lock.json @@ -0,0 +1,25 @@ +{ + "version": 1, + "dependencies": { + "net10.0-windows7.0": { + "DalamudPackager": { + "type": "Direct", + "requested": "[14.0.1, )", + "resolved": "14.0.1", + "contentHash": "y0WWyUE6dhpGdolK3iKgwys05/nZaVf4ZPtIjpLhJBZvHxkkiE23zYRo7K7uqAgoK/QvK5cqF6l3VG5AbgC6KA==" + }, + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.2.39, )", + "resolved": "1.2.39", + "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg==" + }, + "Glamourer.Api": { + "type": "Direct", + "requested": "[2.8.0, )", + "resolved": "2.8.0", + "contentHash": "dCxycU+lA0qraE70ZoRvM4GQAPq/K+qL/bg6t/kxKPox5GWaiunKOTXNOG2hOvgEda5WtFy6e3c9OuIM6L3faQ==" + } + } + } +} \ No newline at end of file