first commit

This commit is contained in:
2025-06-07 02:03:54 +03:00
commit 75762504bd
21 changed files with 1005 additions and 0 deletions

View File

@@ -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<ActivityConfig> 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<ActivityConfig> GetDefaults()
{
return DEFAULTS.Select(c => c.Clone()).ToList();
}
}

View File

@@ -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<ActivityConfig> ActivityConfigs { get; set; } = [];
public Config() { }
public Config(List<ActivityConfig> activityConfigs)
{
ActivityConfigs = activityConfigs;
}
public void Save()
{
Plugin.PluginInterface.SavePluginConfig(this);
}
}

View File

@@ -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();
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Dalamud.NET.Sdk/12.0.2">
<PropertyGroup>
<Description>Update honorific title based on spotify informations</Description>
<IsPackable>false</IsPackable>
<TargetFramework>net9.0-windows7.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Scriban" Version="6.0.0" />
</ItemGroup>
</Project>

View File

@@ -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"
]
}

View File

@@ -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<int, string, object> SetCharacterTitleSubscriber { get; init; }
private ICallGateSubscriber<int, object> 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<int, string, object>("Honorific.SetCharacterTitle");
ClearCharacterTitleSubscriber = PluginInterface.GetIpcSubscriber<int, object>("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<string, object>() {
{"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
}

View File

@@ -0,0 +1,6 @@
namespace SpotifyHonorific.Updaters;
public class UpdaterContext
{
public double SecsElapsed { get; set; } = 0;
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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=="
}
}
}
}