commit 75762504bde54f1ac965f316f8d55927ff763e0a Author: Administrator Date: Sat Jun 7 02:03:54 2025 +0300 first commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1fd78b2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,138 @@ +root = true +# top-most EditorConfig file + +[*] +charset = utf-8 + +end_of_line = lf +insert_final_newline = true + +# 4 space indentation +indent_style = space +indent_size = 4 + +# disable redundant style warnings + +# Microsoft .NET properties +csharp_indent_braces = false +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = false +csharp_new_line_before_open_brace = all +csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +dotnet_code_quality_unused_parameters = non_public +dotnet_naming_rule.event_rule.severity = warning +dotnet_naming_rule.event_rule.style = on_upper_camel_case_style +dotnet_naming_rule.event_rule.symbols = event_symbols +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols +dotnet_naming_rule.private_instance_fields_rule.severity = warning +dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = upper_camel_case_style +dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.on_upper_camel_case_style.capitalization = pascal_case +dotnet_naming_style.on_upper_camel_case_style.required_prefix = On +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case +dotnet_naming_symbols.event_symbols.applicable_accessibilities = * +dotnet_naming_symbols.event_symbols.applicable_kinds = event +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field +dotnet_naming_symbols.private_constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly +dotnet_style_parentheses_in_arithmetic_binary_operators =always_for_clarity:suggestion +dotnet_style_parentheses_in_other_binary_operators =always_for_clarity:suggestion +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion +dotnet_style_parentheses_in_other_operators=always_for_clarity:silent +dotnet_style_object_initializer = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_empty_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_before_open_square_brackets = false +csharp_space_before_comma = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_comma = true +csharp_space_after_cast = false +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = none +csharp_space_between_square_brackets = false + +# ReSharper properties +resharper_align_linq_query = true +resharper_align_multiline_argument = true +resharper_align_multiline_calls_chain = true +resharper_align_multiline_expression = true +resharper_align_multiline_extends_list = true +resharper_align_multiline_for_stmt = true +resharper_align_multline_type_parameter_constrains = true +resharper_align_multline_type_parameter_list = true +resharper_apply_on_completion = true +resharper_auto_property_can_be_made_get_only_global_highlighting = none +resharper_auto_property_can_be_made_get_only_local_highlighting = none +resharper_autodetect_indent_settings = true +resharper_braces_for_ifelse = required_for_multiline +resharper_can_use_global_alias = false +resharper_csharp_align_multiline_parameter = true +resharper_csharp_align_multiple_declaration = true +resharper_csharp_empty_block_style = together_same_line +resharper_csharp_int_align_comments = true +resharper_csharp_new_line_before_while = true +resharper_csharp_wrap_after_declaration_lpar = true +resharper_enforce_line_ending_style = true +resharper_member_can_be_private_global_highlighting = none +resharper_member_can_be_private_local_highlighting = none +resharper_new_line_before_finally = false +resharper_place_accessorholder_attribute_on_same_line = false +resharper_place_field_attribute_on_same_line = false +resharper_show_autodetect_configure_formatting_tip = false +resharper_use_indent_from_vs = false + +# ReSharper inspection severities +resharper_arrange_missing_parentheses_highlighting = hint +resharper_arrange_redundant_parentheses_highlighting = hint +resharper_arrange_this_qualifier_highlighting = hint +resharper_arrange_type_member_modifiers_highlighting = hint +resharper_arrange_type_modifiers_highlighting = hint +resharper_built_in_type_reference_style_for_member_access_highlighting = hint +resharper_built_in_type_reference_style_highlighting = none +resharper_foreach_can_be_converted_to_query_using_another_get_enumerator_highlighting = none +resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_highlighting = none +resharper_invert_if_highlighting = none +resharper_loop_can_be_converted_to_query_highlighting = none +resharper_method_has_async_overload_highlighting = none +resharper_private_field_can_be_converted_to_local_variable_highlighting = none +resharper_redundant_base_qualifier_highlighting = none +resharper_suggest_var_or_type_built_in_types_highlighting = hint +resharper_suggest_var_or_type_elsewhere_highlighting = hint +resharper_suggest_var_or_type_simple_types_highlighting = hint +resharper_unused_auto_property_accessor_global_highlighting = none +csharp_style_deconstructed_variable_declaration=true:silent + +[*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}] +indent_style = space +indent_size = 4 +tab_width = 4 +dotnet_style_parentheses_in_other_operators=always_for_clarity:silent diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..77b4fae --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,40 @@ +name: Build + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + Build: + runs-on: ubuntu-latest + env: + DALAMUD_HOME: /tmp/dalamud + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 9.0.x + + - name: Download Dalamud Latest + run: | + wget https://goatcorp.github.io/dalamud-distrib/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 SpotifyHonorific/SpotifyHonorific.csproj + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: SpotifyHonorific + path: | + SpotifyHonorific/bin/Release/* + !SpotifyHonorific/bin/Release/SpotifyHonorific/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4db28f0 --- /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 + + - name: Set up .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 9.0.x + + - name: Download Dalamud Latest + run: | + wget https://goatcorp.github.io/dalamud-distrib/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 SpotifyHonorific/SpotifyHonorific.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 + uses: actions/upload-release-asset@v1 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: SpotifyHonorific/bin/Release/SpotifyHonorific/latest.zip + asset_name: SpotifyHonorific.zip + asset_content_type: application/zip + env: + GITHUB_TOKEN: ${{ github.token }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7990fe7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.vs/ +obj/ +bin/ +*.user \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..266114d --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# SpotifyHonorific + +Update honorific title based on discord activity informations. + +## Installation + +Installable using my custom repository (instructions here: https://github.com/anya-hichu/DalamudPluginRepo) or from compiled archives. + + +## Commands + +- `/spotifyhonorific config` +- `/spotifyhonorific enable` +- `/spotifyhonorific disable` diff --git a/SpotifyHonorific.sln b/SpotifyHonorific.sln new file mode 100644 index 0000000..0d43b02 --- /dev/null +++ b/SpotifyHonorific.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35919.96 d17.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpotifyHonorific", "SpotifyHonorific\SpotifyHonorific.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF} + EndGlobalSection +EndGlobal diff --git a/SpotifyHonorific/Activities/ActivityConfig.cs b/SpotifyHonorific/Activities/ActivityConfig.cs new file mode 100644 index 0000000..d78d4cb --- /dev/null +++ b/SpotifyHonorific/Activities/ActivityConfig.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace SpotifyHonorific.Activities; + +[Serializable] +public class ActivityConfig +{ + public static readonly int DEFAULT_VERSION = 1; + private static readonly List DEFAULTS = [ + new() { + Name = $"Spotify (V{DEFAULT_VERSION})", + Priority = 1, + TitleTemplate = """ +♪{{- if (Context.SecsElapsed % 30) < 10 -}} + Listening to Spotify +{{- else if (Context.SecsElapsed % 30) < 20 -}} + {{ Activity.SongName | string.truncate 30 }} +{{- else -}} + {{ Activity.Artist | string.truncate 30 }} +{{- end -}}♪ +""" + } + ]; + + + public string Name { get; set; } = string.Empty; + public bool Enabled { get; set; } = true; + public int Priority { get; set; } = 0; + public string TypeName { get; set; } = string.Empty; + public string FilterTemplate { get; set; } = string.Empty; + public string TitleTemplate { get; set; } = string.Empty; + public bool IsPrefix { get; set; } = false; + public Vector3? Color { get; set; } + public Vector3? Glow { get; set; } + + public ActivityConfig Clone() + { + return (ActivityConfig)MemberwiseClone(); + } + + public static List GetDefaults() + { + return DEFAULTS.Select(c => c.Clone()).ToList(); + } +} diff --git a/SpotifyHonorific/Config.cs b/SpotifyHonorific/Config.cs new file mode 100644 index 0000000..6825568 --- /dev/null +++ b/SpotifyHonorific/Config.cs @@ -0,0 +1,28 @@ +using Dalamud.Configuration; +using SpotifyHonorific.Activities; +using System; +using System.Collections.Generic; + +namespace SpotifyHonorific; + +[Serializable] +public class Config : IPluginConfiguration +{ + public int Version { get; set; } = 0; + public bool Enabled { get; set; } = true; + public string Token { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; + public List ActivityConfigs { get; set; } = []; + + public Config() { } + + public Config(List activityConfigs) + { + ActivityConfigs = activityConfigs; + } + + public void Save() + { + Plugin.PluginInterface.SavePluginConfig(this); + } +} diff --git a/SpotifyHonorific/Plugin.cs b/SpotifyHonorific/Plugin.cs new file mode 100644 index 0000000..da4f066 --- /dev/null +++ b/SpotifyHonorific/Plugin.cs @@ -0,0 +1,82 @@ +using Dalamud.Game.Command; +using Dalamud.IoC; +using Dalamud.Plugin; +using Dalamud.Interface.Windowing; +using Dalamud.Plugin.Services; +using SpotifyHonorific.Windows; +using SpotifyHonorific.Activities; +using SpotifyHonorific.Updaters; + +namespace SpotifyHonorific; + +public sealed class Plugin : IDalamudPlugin +{ + [PluginService] internal static IDalamudPluginInterface PluginInterface { get; private set; } = null!; + [PluginService] internal static ICommandManager CommandManager { get; private set; } = null!; + [PluginService] internal static IChatGui ChatGui { get; private set; } = null!; + [PluginService] internal static IPluginLog PluginLog { get; private set; } = null!; + [PluginService] internal static IFramework Framework { get; private set; } = null!; + + private const string CommandName = "/spotifyhonorific"; + private const string CommandHelpMessage = $"Available subcommands for {CommandName} are config, enable and disable"; + + public Config Config { get; init; } + + public readonly WindowSystem WindowSystem = new("SpotifyHonorific"); + private ConfigWindow ConfigWindow { get; init; } + private Updater Updater { get; init; } + + + public Plugin() + { + Config = PluginInterface.GetPluginConfig() as Config ?? new Config(ActivityConfig.GetDefaults()); + Updater = new(Config, Framework, PluginInterface, PluginLog); + ConfigWindow = new ConfigWindow(Config, new(), Updater); + + WindowSystem.AddWindow(ConfigWindow); + CommandManager.AddHandler(CommandName, new CommandInfo(OnCommand) + { + HelpMessage = CommandHelpMessage + }); + + PluginInterface.UiBuilder.Draw += DrawUI; + PluginInterface.UiBuilder.OpenConfigUi += ToggleConfigUI; + PluginInterface.UiBuilder.OpenMainUi += ToggleConfigUI; + } + + public void Dispose() + { + WindowSystem.RemoveAllWindows(); + CommandManager.RemoveHandler(CommandName); + Updater.Dispose(); + } + + private void OnCommand(string command, string args) + { + var subcommand = args.Split(" ", 2)[0]; + if (subcommand == "config") + { + ToggleConfigUI(); + } + else if (subcommand == "enable") + { + Config.Enabled = true; + Config.Save(); + Updater.Start(); + } + else if (subcommand == "disable") + { + Config.Enabled = false; + Config.Save(); + Updater.Stop(); + } + else + { + ChatGui.Print(CommandHelpMessage); + } + } + + private void DrawUI() => WindowSystem.Draw(); + + public void ToggleConfigUI() => ConfigWindow.Toggle(); +} diff --git a/SpotifyHonorific/SpotifyHonorific.csproj b/SpotifyHonorific/SpotifyHonorific.csproj new file mode 100644 index 0000000..f7d7318 --- /dev/null +++ b/SpotifyHonorific/SpotifyHonorific.csproj @@ -0,0 +1,12 @@ + + + + Update honorific title based on spotify informations + false + net9.0-windows7.0 + + + + + + diff --git a/SpotifyHonorific/SpotifyHonorific.json b/SpotifyHonorific/SpotifyHonorific.json new file mode 100644 index 0000000..81b4ea8 --- /dev/null +++ b/SpotifyHonorific/SpotifyHonorific.json @@ -0,0 +1,12 @@ +{ + "Author": "Lozy Rhel", + "Name": "SpotifyHonorific", + "Punchline": "Spotify activity as honorific", + "Description": "Update honorific title based on Spotify activity informations", + "ApplicableVersion": "any", + "Tags": [ + "spotify", + "activity", + "honorific" + ] +} diff --git a/SpotifyHonorific/Updaters/Updater.cs b/SpotifyHonorific/Updaters/Updater.cs new file mode 100644 index 0000000..2fd1a18 --- /dev/null +++ b/SpotifyHonorific/Updaters/Updater.cs @@ -0,0 +1,285 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using Dalamud.Plugin.Services; +using Dalamud.Utility; +using Newtonsoft.Json; +using Scriban; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using System.Timers; + +namespace SpotifyHonorific.Updaters; + +public class Updater : IDisposable +{ + private Config Config { get; init; } + private IFramework Framework { get; init; } + private IDalamudPluginInterface PluginInterface { get; init; } + private IPluginLog PluginLog { get; init; } + private ICallGateSubscriber SetCharacterTitleSubscriber { get; init; } + private ICallGateSubscriber ClearCharacterTitleSubscriber { get; init; } + + private Action? UpdateTitle { get; set; } + private string? UpdatedTitleJson { get; set; } + private UpdaterContext UpdaterContext { get; init; } = new(); + + private Timer MediaCheckTimer { get; init; } + private MediaActivity? CurrentMediaActivity { get; set; } + + public Updater(Config config, IFramework framwork, IDalamudPluginInterface pluginInterface, IPluginLog pluginLog) + { + Config = config; + Framework = framwork; + PluginInterface = pluginInterface; + PluginLog = pluginLog; + + SetCharacterTitleSubscriber = PluginInterface.GetIpcSubscriber("Honorific.SetCharacterTitle"); + ClearCharacterTitleSubscriber = PluginInterface.GetIpcSubscriber("Honorific.ClearCharacterTitle"); + + MediaCheckTimer = new Timer(2000); + MediaCheckTimer.Elapsed += CheckMediaActivity; + MediaCheckTimer.AutoReset = true; + + if (Config.Enabled) + { + Start(); + } + + Framework.Update += OnFrameworkUpdate; + } + + public void Dispose() + { + Framework.Update -= OnFrameworkUpdate; + MediaCheckTimer?.Stop(); + MediaCheckTimer?.Dispose(); + Framework.RunOnFrameworkThread(() => + { + ClearCharacterTitleSubscriber.InvokeAction(0); + }); + } + + public Task Enable(bool value) + { + return value ? Start() : Stop(); + } + + public Task Restart() + { + return Stop().ContinueWith(t => Start()); + } + + public Task Start() + { + if (Config.Enabled) + { + MediaCheckTimer.Start(); + PluginLog.Info("Media activity monitoring started"); + } + return Task.CompletedTask; + } + + public Task Stop() + { + MediaCheckTimer.Stop(); + Framework.RunOnFrameworkThread(() => + { + ClearCharacterTitleSubscriber.InvokeAction(0); + }); + PluginLog.Info("Media activity monitoring stopped"); + return Task.CompletedTask; + } + + public string State() + { + return MediaCheckTimer.Enabled ? "Running" : "Stopped"; + } + + private void CheckMediaActivity(object sender, ElapsedEventArgs e) + { + try + { + var mediaActivity = GetCurrentMediaActivity(); + + if (!MediaActivitiesEqual(CurrentMediaActivity, mediaActivity)) + { + CurrentMediaActivity = mediaActivity; + MediaActivityUpdated(mediaActivity); + } + } + catch (Exception ex) + { + PluginLog.Error($"Error checking media activity: {ex.Message}"); + } + } + + private MediaActivity? GetCurrentMediaActivity() + { + var spotifyTrack = GetSpotifyTrack(); + if (!string.IsNullOrEmpty(spotifyTrack)) + { + string artist = ""; + string songName = spotifyTrack; + var parts = spotifyTrack.Split(new[] { " - " }, 2, StringSplitOptions.None); + if (parts.Length == 2) + { + artist = parts[0]; + songName = parts[1]; + } + + return new MediaActivity + { + Name = "Spotify (V1)", + SongName = songName, + Artist = artist, + Type = ActivityType.Listening + }; + } + + return null; + } + + private string? GetSpotifyTrack() + { + try + { + var spotifyProcesses = Process.GetProcessesByName("Spotify"); + foreach (var process in spotifyProcesses) + { + if (!string.IsNullOrEmpty(process.MainWindowTitle) && + process.MainWindowTitle != "Spotify" && + process.MainWindowTitle != "Spotify Premium" && + process.MainWindowTitle != "Spotify Free") + { + return process.MainWindowTitle; + } + } + } + catch (Exception ex) + { + PluginLog.Debug($"Error getting Spotify track: {ex.Message}"); + } + + return null; + } + + private bool MediaActivitiesEqual(MediaActivity? a, MediaActivity? b) + { + if (a == null && b == null) return true; + if (a == null || b == null) return false; + return a.Name == b.Name && a.SongName == b.SongName && a.Type == b.Type; + } + + private void MediaActivityUpdated(MediaActivity? mediaActivity) + { + PluginLog.Verbose($"MediaActivityUpdated: {JsonConvert.SerializeObject(mediaActivity, Formatting.Indented)}"); + + if (mediaActivity != null) + { + foreach (var activityConfig in Config.ActivityConfigs.Where(c => c.Enabled).OrderByDescending(c => c.Priority)) + { + var matchesType = + (mediaActivity.Type == ActivityType.Listening); + PluginLog.Verbose($"Checking activity config '{activityConfig.Name}' for match: {matchesType}"); + if (matchesType) + { + var matchFilter = true; + if (!activityConfig.FilterTemplate.IsNullOrWhitespace()) + { + var filterTemplate = Template.Parse(activityConfig.FilterTemplate); + var filter = filterTemplate.Render(new { Activity = mediaActivity, Context = UpdaterContext }, member => member.Name); + + if (bool.TryParse(filter, out var parsedFilter)) + { + matchFilter = parsedFilter; + } + else + { + PluginLog.Error($"Unable to parse filter '{filter}' as boolean, skipping result"); + } + } + + if (matchFilter) + { + UpdaterContext.SecsElapsed = 0; + UpdateTitle = () => + { + if (Config.Enabled && activityConfig.Enabled) + { + var titleTemplate = Template.Parse(activityConfig.TitleTemplate); + var title = titleTemplate.Render(new { Activity = mediaActivity, Context = UpdaterContext }, member => member.Name); + + var data = new Dictionary() { + {"Title", title}, + {"IsPrefix", activityConfig.IsPrefix}, + {"Color", activityConfig.Color!}, + {"Glow", activityConfig.Glow!} + }; + + var serializedData = JsonConvert.SerializeObject(data, Formatting.Indented); + if (serializedData != UpdatedTitleJson) + { + PluginLog.Verbose($"Call Honorific SetCharacterTitle IPC with:\n{serializedData}"); + SetCharacterTitleSubscriber.InvokeAction(0, serializedData); + UpdatedTitleJson = serializedData; + } + } + else + { + ClearTitle(); + } + }; + return; + } + } + } + } + + if (UpdateTitle != null || UpdatedTitleJson != null) + { + ClearTitle(); + } + } + + private void OnFrameworkUpdate(IFramework framework) + { + if (Config.Enabled && UpdateTitle != null) + { + UpdateTitle.Invoke(); + UpdaterContext.SecsElapsed += framework.UpdateDelta.TotalSeconds; + } + } + + private void ClearTitle() + { + PluginLog.Verbose("Call Honorific ClearCharacterTitle IPC"); + Framework.RunOnFrameworkThread(() => + { + ClearCharacterTitleSubscriber.InvokeAction(0); + }); + UpdaterContext.SecsElapsed = 0; + UpdateTitle = null; + UpdatedTitleJson = null; + } +} + +public class MediaActivity +{ + public string Name { get; set; } = ""; + public string SongName { get; set; } = ""; + public string Artist { get; set; } = ""; + public ActivityType Type { get; set; } +} + +public enum ActivityType +{ + Playing, + Streaming, + Listening, + Watching, + Custom, + Competing +} diff --git a/SpotifyHonorific/Updaters/UpdaterContext.cs b/SpotifyHonorific/Updaters/UpdaterContext.cs new file mode 100644 index 0000000..acb2be1 --- /dev/null +++ b/SpotifyHonorific/Updaters/UpdaterContext.cs @@ -0,0 +1,6 @@ +namespace SpotifyHonorific.Updaters; + +public class UpdaterContext +{ + public double SecsElapsed { get; set; } = 0; +} diff --git a/SpotifyHonorific/Utils/ImGuiHelper.cs b/SpotifyHonorific/Utils/ImGuiHelper.cs new file mode 100644 index 0000000..cb0e833 --- /dev/null +++ b/SpotifyHonorific/Utils/ImGuiHelper.cs @@ -0,0 +1,98 @@ + + +using Dalamud.Interface.Utility; +using ImGuiNET; +using System.Numerics; + +namespace SpotifyHonorific.Utils; + +public class ImGuiHelper +{ + // Source: https://github.com/Caraxi/Honorific/blob/1.4.1.0/ConfigWindow.cs#L826 + private Vector3 editingColour = Vector3.One; + public bool DrawColorPicker(string label, ref Vector3? color, Vector2 checkboxSize) + { + var modified = false; + bool comboOpen; + ImGui.SetNextItemWidth(checkboxSize.X * 2); + if (color == null) + { + ImGui.PushStyleColor(ImGuiCol.FrameBg, 0xFFFFFFFF); + ImGui.PushStyleColor(ImGuiCol.FrameBgActive, 0xFFFFFFFF); + ImGui.PushStyleColor(ImGuiCol.FrameBgHovered, 0xFFFFFFFF); + var p = ImGui.GetCursorScreenPos(); + var dl = ImGui.GetWindowDrawList(); + comboOpen = ImGui.BeginCombo(label, " ", ImGuiComboFlags.HeightLargest); + dl.AddLine(p, p + new Vector2(checkboxSize.X), 0xFF0000FF, 3f * ImGuiHelpers.GlobalScale); + ImGui.PopStyleColor(3); + } + else + { + ImGui.PushStyleColor(ImGuiCol.FrameBg, new Vector4(color.Value, 1)); + ImGui.PushStyleColor(ImGuiCol.FrameBgActive, new Vector4(color.Value, 1)); + ImGui.PushStyleColor(ImGuiCol.FrameBgHovered, new Vector4(color.Value, 1)); + comboOpen = ImGui.BeginCombo(label, " ", ImGuiComboFlags.HeightLargest); + ImGui.PopStyleColor(3); + } + + if (comboOpen) + { + if (ImGui.IsWindowAppearing()) + { + editingColour = color ?? Vector3.One; + } + if (ImGui.ColorButton($"##ColorPickClear", Vector4.One, ImGuiColorEditFlags.NoTooltip)) + { + color = null; + modified = true; + ImGui.CloseCurrentPopup(); + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Clear selected colour"); + ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); + } + var dl = ImGui.GetWindowDrawList(); + dl.AddLine(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), 0xFF0000FF, 3f * ImGuiHelpers.GlobalScale); + + if (color != null) + { + ImGui.SameLine(); + if (ImGui.ColorButton($"##ColorPick_old", new Vector4(color.Value, 1), ImGuiColorEditFlags.NoTooltip)) + { + ImGui.CloseCurrentPopup(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Revert to previous selection"); + ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); + } + } + + ImGui.SameLine(); + + if (ImGui.ColorButton("Confirm", new Vector4(editingColour, 1), ImGuiColorEditFlags.NoTooltip, new Vector2(ImGui.GetContentRegionAvail().X, ImGui.GetItemRectSize().Y))) + { + color = editingColour; + modified = true; + ImGui.CloseCurrentPopup(); + } + var size = ImGui.GetItemRectSize(); + + if (ImGui.IsItemHovered()) + { + dl.AddRectFilled(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), 0x33333333); + ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); + } + + var textSize = ImGui.CalcTextSize("Confirm"); + dl.AddText(ImGui.GetItemRectMin() + size / 2 - textSize / 2, ImGui.ColorConvertFloat4ToU32(new Vector4(editingColour, 1)) ^ 0x00FFFFFF, "Confirm"); + ImGui.ColorPicker3($"##ColorPick", ref editingColour, ImGuiColorEditFlags.NoSidePreview | ImGuiColorEditFlags.NoSmallPreview); + + ImGui.EndCombo(); + } + + return modified; + } +} diff --git a/SpotifyHonorific/Windows/ConfigWindow.cs b/SpotifyHonorific/Windows/ConfigWindow.cs new file mode 100644 index 0000000..0582b87 --- /dev/null +++ b/SpotifyHonorific/Windows/ConfigWindow.cs @@ -0,0 +1,136 @@ +using Dalamud.Interface.Windowing; +using Dalamud.Utility; +using SpotifyHonorific.Updaters; +using SpotifyHonorific.Utils; +using ImGuiNET; +using System.Numerics; + +namespace SpotifyHonorific.Windows; + +public class ConfigWindow : Window +{ + private Config Config { get; init; } + private ImGuiHelper ImGuiHelper { get; init; } + private Updater Updater { get; init; } + + public ConfigWindow(Config config, ImGuiHelper imGuiHelper, Updater updater) : base("Spotify Honorific Config (Modified By Lozy Rhel)##configWindow") + { + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new Vector2(760, 420), + MaximumSize = new Vector2(float.MaxValue, float.MaxValue) + }; + + Config = config; + ImGuiHelper = imGuiHelper; + Updater = updater; + } + + public override void Draw() + { + var enabled = Config.Enabled; + if (ImGui.Checkbox("Enabled##enabled", ref enabled)) + { + Config.Enabled = enabled; + Config.Save(); + Updater.Enable(enabled); + } + + + + if (ImGui.BeginTabBar("activityConfigsTabBar")) + { + foreach (var activityConfig in Config.ActivityConfigs) + { + var activityConfigId = $"activityConfigs{activityConfig.GetHashCode()}"; + + var name = activityConfig.Name; + if (ImGui.BeginTabItem($"{(name.IsNullOrWhitespace() ? "(Blank)" : name)}###{activityConfigId}TabItem")) + { + ImGui.Indent(10); + + var activityConfigEnabled = activityConfig.Enabled; + if (ImGui.Checkbox($"Enabled###{activityConfigId}enabled", ref activityConfigEnabled)) + { + activityConfig.Enabled = activityConfigEnabled; + Config.Save(); + } + + + + if (ImGui.InputText($"Name###{activityConfigId}Name", ref name, ushort.MaxValue)) + { + activityConfig.Name = name; + Config.Save(); + } + + var priority = activityConfig.Priority; + if (ImGui.InputInt($"Priority###{activityConfigId}Priority", ref priority, 1)) + { + activityConfig.Priority = priority; + Config.Save(); + } + + var typeName = activityConfig.TypeName; + + var filterTemplate = activityConfig.FilterTemplate; + var filterTemplateInput = ImGui.InputTextMultiline($"Filter Template (scriban)###{activityConfigId}FilterTemplate", ref filterTemplate, ushort.MaxValue, new(ImGui.GetWindowWidth() - 170, 50)); + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Expects parsable boolean as output if provided\nSyntax reference available on https://github.com/scriban/scriban"); + } + if (filterTemplateInput) + { + activityConfig.FilterTemplate = filterTemplate; + Config.Save(); + } + + var titleTemplate = activityConfig.TitleTemplate; + var titleTemplateInput = ImGui.InputTextMultiline($"Title Template (scriban)###{activityConfigId}TitleTemplate", ref titleTemplate, ushort.MaxValue, new(ImGui.GetWindowWidth() - 170, ImGui.GetWindowHeight() - ImGui.GetCursorPosY() - 40)); + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Expects single line as output\nSyntax reference available on https://github.com/scriban/scriban"); + } + if (titleTemplateInput) + { + activityConfig.TitleTemplate = titleTemplate; + Config.Save(); + } + + var isPrefix = activityConfig.IsPrefix; + if (ImGui.Checkbox($"Prefix###{activityConfigId}Prefix", ref isPrefix)) + { + activityConfig.IsPrefix = isPrefix; + Config.Save(); + } + ImGui.SameLine(); + ImGui.Spacing(); + ImGui.SameLine(); + + var checkboxSize = new Vector2(ImGui.GetTextLineHeightWithSpacing(), ImGui.GetTextLineHeightWithSpacing()); + + var color = activityConfig.Color; + if (ImGuiHelper.DrawColorPicker($"Color###{activityConfigId}Color", ref color, checkboxSize)) + { + activityConfig.Color = color; + Config.Save(); + } + + ImGui.SameLine(); + ImGui.Spacing(); + ImGui.SameLine(); + var glow = activityConfig.Glow; + if (ImGuiHelper.DrawColorPicker($"Glow###{activityConfigId}Glow", ref glow, checkboxSize)) + { + activityConfig.Glow = glow; + Config.Save(); + } + + ImGui.Unindent(); + ImGui.EndTabItem(); + } + } + ImGui.EndTabBar(); + } + } +} diff --git a/SpotifyHonorific/packages.lock.json b/SpotifyHonorific/packages.lock.json new file mode 100644 index 0000000..ab73c73 --- /dev/null +++ b/SpotifyHonorific/packages.lock.json @@ -0,0 +1,25 @@ +{ + "version": 1, + "dependencies": { + "net9.0-windows7.0": { + "DalamudPackager": { + "type": "Direct", + "requested": "[12.0.0, )", + "resolved": "12.0.0", + "contentHash": "J5TJLV3f16T/E2H2P17ClWjtfEBPpq3yxvqW46eN36JCm6wR+EaoaYkqG9Rm5sHqs3/nK/vKjWWyvEs/jhKoXw==" + }, + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.2.25, )", + "resolved": "1.2.25", + "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" + }, + "Scriban": { + "type": "Direct", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "MZOtxtcehrCGiVwHpdcZQSe04Zy4IJfltVZdmlr1nFvSvEXnu50SWa7fonC0bqfMyTnNhQcY9BmEt882P129qw==" + } + } + } +} \ No newline at end of file diff --git a/images/image1.png b/images/image1.png new file mode 100644 index 0000000..a377ee9 Binary files /dev/null and b/images/image1.png differ diff --git a/images/image2.png b/images/image2.png new file mode 100644 index 0000000..2a81f0b Binary files /dev/null and b/images/image2.png differ diff --git a/images/image3.png b/images/image3.png new file mode 100644 index 0000000..bc530e4 Binary files /dev/null and b/images/image3.png differ diff --git a/images/image4.png b/images/image4.png new file mode 100644 index 0000000..9a76e3d Binary files /dev/null and b/images/image4.png differ diff --git a/images/image5.png b/images/image5.png new file mode 100644 index 0000000..4d484e6 Binary files /dev/null and b/images/image5.png differ