first commit
This commit is contained in:
138
.editorconfig
Normal file
138
.editorconfig
Normal file
@@ -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
|
||||
40
.github/workflows/build.yml
vendored
Normal file
40
.github/workflows/build.yml
vendored
Normal file
@@ -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/*
|
||||
52
.github/workflows/release.yml
vendored
Normal file
52
.github/workflows/release.yml
vendored
Normal file
@@ -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 }}
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.vs/
|
||||
obj/
|
||||
bin/
|
||||
*.user
|
||||
14
README.md
Normal file
14
README.md
Normal file
@@ -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`
|
||||
25
SpotifyHonorific.sln
Normal file
25
SpotifyHonorific.sln
Normal file
@@ -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
|
||||
48
SpotifyHonorific/Activities/ActivityConfig.cs
Normal file
48
SpotifyHonorific/Activities/ActivityConfig.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
28
SpotifyHonorific/Config.cs
Normal file
28
SpotifyHonorific/Config.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
82
SpotifyHonorific/Plugin.cs
Normal file
82
SpotifyHonorific/Plugin.cs
Normal 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();
|
||||
}
|
||||
12
SpotifyHonorific/SpotifyHonorific.csproj
Normal file
12
SpotifyHonorific/SpotifyHonorific.csproj
Normal 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>
|
||||
12
SpotifyHonorific/SpotifyHonorific.json
Normal file
12
SpotifyHonorific/SpotifyHonorific.json
Normal 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"
|
||||
]
|
||||
}
|
||||
285
SpotifyHonorific/Updaters/Updater.cs
Normal file
285
SpotifyHonorific/Updaters/Updater.cs
Normal 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
|
||||
}
|
||||
6
SpotifyHonorific/Updaters/UpdaterContext.cs
Normal file
6
SpotifyHonorific/Updaters/UpdaterContext.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace SpotifyHonorific.Updaters;
|
||||
|
||||
public class UpdaterContext
|
||||
{
|
||||
public double SecsElapsed { get; set; } = 0;
|
||||
}
|
||||
98
SpotifyHonorific/Utils/ImGuiHelper.cs
Normal file
98
SpotifyHonorific/Utils/ImGuiHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
136
SpotifyHonorific/Windows/ConfigWindow.cs
Normal file
136
SpotifyHonorific/Windows/ConfigWindow.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
SpotifyHonorific/packages.lock.json
Normal file
25
SpotifyHonorific/packages.lock.json
Normal 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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
images/image1.png
Normal file
BIN
images/image1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 564 KiB |
BIN
images/image2.png
Normal file
BIN
images/image2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 614 KiB |
BIN
images/image3.png
Normal file
BIN
images/image3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
BIN
images/image4.png
Normal file
BIN
images/image4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
BIN
images/image5.png
Normal file
BIN
images/image5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Reference in New Issue
Block a user