Files
DesktopMagic/src/DesktopMagic/Plugins/PluginManager.xaml.cs
T

816 lines
30 KiB
C#

using DesktopMagic.DataContexts;
using DesktopMagic.Dialogs;
using DesktopMagic.Helpers;
using Modio;
using Modio.Filters;
using Modio.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Xml.Linq;
using File = System.IO.File;
using Path = System.IO.Path;
namespace DesktopMagic.Plugins;
/// <summary>
/// Interaction logic for PluginManager.xaml
/// </summary>
public partial class PluginManager : Page
{
private const int ModIoGameId = 5665;
private const string ModIoApiKey = "88e6ea774c3a502b06114e7fee0829ac";
private readonly HttpClient httpClient = new();
private readonly PluginManagerDataContext pluginManagerDataContext = new();
private readonly string pluginsPath = Path.Combine(App.ApplicationDataPath, "Plugins");
private readonly string pluginDevelopmentPath = Path.Combine(App.ApplicationDataPath, "PluginDevelopment");
private readonly Manager _manager = Manager.Instance;
private bool changed = false;
private readonly DispatcherTimer searchTimer = new()
{
Interval = TimeSpan.FromMilliseconds(300),
};
private Client modIoClient;
public PluginManager()
{
Resources.MergedDictionaries.Add(App.LanguageDictionary);
string? modIoAccessToken = MainWindowDataContext.GetSettings().ModIoAccessToken;
if (modIoAccessToken is null)
{
modIoClient = new Client(new Credentials(ModIoApiKey));
App.Logger.LogInfo("Initialized mod.io client without authentication", source: "PluginManager");
}
else
{
modIoClient = new Client(new Credentials(ModIoApiKey, modIoAccessToken));
pluginManagerDataContext.IsAuthenticated = true;
App.Logger.LogInfo("Initialized mod.io client with authentication", source: "PluginManager");
}
InitializeComponent();
DataContext = pluginManagerDataContext;
searchTimer.Tick += async (sender, e) =>
{
searchTimer.Stop();
await SearchAllPlugins(pluginManagerDataContext.AllPluginsSearchText);
};
Loaded += PluginManager_Loaded;
Unloaded += PluginManager_Unloaded;
}
private async void PluginManager_Loaded(object sender, RoutedEventArgs e)
{
await InitializePluginManager();
}
private void PluginManager_Unloaded(object sender, RoutedEventArgs e)
{
searchTimer.Stop();
if (changed)
{
_manager.ReloadPlugins();
}
}
private async Task InitializePluginManager()
{
App.Logger.LogInfo("Initializing Plugin Manager", source: "PluginManager");
HashSet<uint> pluginIds = [];
pluginManagerDataContext.IsLoading = true;
pluginManagerDataContext.InstalledPlugins.Clear();
pluginManagerDataContext.AllPlugins.Clear();
foreach (string pluginPath in Directory.GetDirectories(pluginsPath))
{
string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
if (!File.Exists(pluginMetadataPath))
{
App.Logger.LogWarn($"Plugin metadata not found at: {pluginMetadataPath}", source: "PluginManager");
continue;
}
PluginMetadata? pluginMetadata = JsonSerializer.Deserialize<PluginMetadata>(await File.ReadAllTextAsync(pluginMetadataPath));
if (pluginMetadata is null)
{
App.Logger.LogWarn($"Failed to deserialize plugin metadata from: {pluginMetadataPath}", source: "PluginManager");
continue;
}
string csprojPath = GetCsprojPath(pluginMetadata.Name);
App.Logger.LogInfo($"Loaded plugin: {pluginMetadata.Name} (ID: {pluginMetadata.Id})", source: "PluginManager");
pluginManagerDataContext.InstalledPlugins.Add(new PluginEntryDataContext(pluginMetadata, new CommandHandler(async () => await Remove(pluginPath, pluginMetadata.Id)), PluginEntryDataContext.Mode.Uninstall, pluginPath, csprojPath));
_ = pluginIds.Add(pluginMetadata.Id);
if (pluginMetadata.IsLocalPlugin)
{
continue;
}
try
{
Mod mod = await modIoClient.Games[ModIoGameId].Mods[pluginMetadata.Id].Get();
if (DateTimeOffset.FromUnixTimeSeconds(mod.DateUpdated).DateTime > pluginMetadata.Updated && pluginMetadata.SupportsUnloading)
{
App.Logger.LogInfo($"Plugin {pluginMetadata.Id} has an update available, reinstalling", source: "PluginManager");
await Remove(pluginPath, pluginMetadata.Id);
await Install(mod);
pluginManagerDataContext.IsLoading = true;
}
}
catch (Exception ex)
{
App.Logger.LogError($"Failed to check for plugin {pluginMetadata.Id} updates: {ex.Message}", source: "PluginManager");
}
}
Filter filter = ModFilter.Popular.Desc().Limit(100);
try
{
App.Logger.LogInfo("Fetching popular plugins from mod.io", source: "PluginManager");
IAsyncEnumerable<Mod> mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
await foreach (Mod mod in mods)
{
if (pluginIds.Contains(mod.Id))
{
continue;
}
pluginManagerDataContext.AllPlugins.Add(new PluginEntryDataContext(new(mod), new CommandHandler(async () => await Install(mod)), PluginEntryDataContext.Mode.Install));
}
App.Logger.LogInfo($"Loaded {pluginManagerDataContext.AllPlugins.Count} available plugins", source: "PluginManager");
}
catch (Exception ex)
{
App.Logger.LogError($"Failed to fetch plugins from mod.io: {ex.Message}", source: "PluginManager");
}
await SyncPlugins();
pluginManagerDataContext.IsLoading = false;
App.Logger.LogInfo("Plugin Manager initialization complete", source: "PluginManager");
}
public async Task Remove(string pluginPath, uint id)
{
App.Logger.LogInfo($"Removing plugin with ID {id} from path: {pluginPath}", source: "PluginManager");
pluginManagerDataContext.IsLoading = true;
changed = true;
PluginEntryDataContext? pluginEntryDataContext = pluginManagerDataContext.InstalledPlugins.FirstOrDefault(p => p.Id == id);
if (Directory.Exists(pluginPath))
{
try
{
Directory.Delete(pluginPath, true);
App.Logger.LogInfo($"Successfully deleted plugin directory: {pluginPath}", source: "PluginManager");
}
catch (Exception ex)
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Plugin Manager",
Content = ex.Message,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
App.Logger.LogError(ex.Message, source: "PluginManager");
}
}
if (pluginEntryDataContext is not null)
{
_ = pluginManagerDataContext.InstalledPlugins.Remove(pluginEntryDataContext);
App.Logger.LogInfo($"Removed plugin {id} from installed plugins list", source: "PluginManager");
}
if (pluginManagerDataContext.IsAuthenticated)
{
try
{
await modIoClient.Games[ModIoGameId].Mods.Unsubscribe(id);
App.Logger.LogInfo($"Unsubscribed from plugin {id} on mod.io", source: "PluginManager");
}
catch (Exception ex)
{
App.Logger.LogError($"Failed to unsubscribe from plugin {id}: {ex.Message}", source: "PluginManager");
}
}
pluginManagerDataContext.IsLoading = false;
}
[GeneratedRegex(@"[^a-zA-Z0-9]")]
private static partial Regex IdentifierNameRegex();
private async Task Install(Mod mod)
{
App.Logger.LogInfo($"Installing plugin: {mod.Name} (ID: {mod.Id})", source: "PluginManager");
pluginManagerDataContext.IsLoading = true;
changed = true;
if (mod.Modfile?.Download?.BinaryUrl is null)
{
App.Logger.LogWarn($"Plugin {mod.Id} has no download URL", source: "PluginManager");
return;
}
string pluginGuid = Guid.NewGuid().ToString();
string pluginPath = Path.Combine(pluginsPath, pluginGuid);
string zipFilePath = Path.Combine(pluginsPath, pluginGuid + ".zip");
try
{
App.Logger.LogInfo($"Downloading plugin from: {mod.Modfile.Download.BinaryUrl}", source: "PluginManager");
using (Stream fileStream = await httpClient.GetStreamAsync(mod.Modfile.Download.BinaryUrl))
{
using FileStream outputFileStream = new FileStream(zipFilePath, FileMode.Create);
await fileStream.CopyToAsync(outputFileStream);
}
App.Logger.LogInfo($"Plugin downloaded to: {zipFilePath}", source: "PluginManager");
using (ZipArchive zipArchive = ZipFile.OpenRead(zipFilePath))
{
zipArchive.ExtractToDirectory(pluginPath);
}
App.Logger.LogInfo($"Plugin extracted to: {pluginPath}", source: "PluginManager");
string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
await File.WriteAllTextAsync(pluginMetadataPath, JsonSerializer.Serialize(new PluginMetadata(mod)));
File.Delete(zipFilePath);
if (!File.Exists(Path.Combine(pluginPath, "main.dll")) && !File.Exists(Path.Combine(pluginPath, "main.html")))
{
App.Logger.LogError($"Plugin {mod.Id} does not contain main.dll or main.html", source: "PluginManager");
await Remove(pluginPath, mod.Id);
pluginManagerDataContext.IsLoading = false;
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Plugin Manager",
Content = "The plugin you are trying to install does not contain a \"main.dll\" or \"main.html\" file. Please contact the plugin author.",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
return;
}
PluginEntryDataContext? pluginEntryDataContext = pluginManagerDataContext.AllPlugins.FirstOrDefault(p => p.Id == mod.Id);
if (pluginEntryDataContext is not null)
{
_ = pluginManagerDataContext.AllPlugins.Remove(pluginEntryDataContext);
}
if (pluginManagerDataContext.IsAuthenticated)
{
try
{
await modIoClient.Games[ModIoGameId].Mods.Subscribe(mod.Id);
App.Logger.LogInfo($"Subscribed to plugin {mod.Id} on mod.io", source: "PluginManager");
}
catch (Exception ex)
{
App.Logger.LogError($"Failed to subscribe to plugin {mod.Id}: {ex.Message}", source: "PluginManager");
}
}
pluginManagerDataContext.InstalledPlugins.Add(new PluginEntryDataContext(new PluginMetadata(mod), new CommandHandler(async () => await Remove(pluginPath, mod.Id)), PluginEntryDataContext.Mode.Uninstall, pluginPath));
App.Logger.LogInfo($"Successfully installed plugin: {mod.Name} (ID: {mod.Id})", source: "PluginManager");
}
catch (Exception ex)
{
App.Logger.LogError($"Failed to install plugin {mod.Id}: {ex.Message}", source: "PluginManager");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Plugin Manager",
Content = $"Failed to install plugin: {ex.Message}",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
}
pluginManagerDataContext.IsLoading = false;
}
private void Image_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
string uri = "https://mod.io/g/DesktopMagic";
ProcessStartInfo psi = new ProcessStartInfo
{
UseShellExecute = true,
FileName = uri
};
_ = Process.Start(psi);
App.Logger.LogInfo($"Opened mod.io page: {uri}", source: "PluginManager");
}
private void AllPluginsSearchTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
pluginManagerDataContext.IsSearching = true;
searchTimer.Stop();
searchTimer.Start();
}
private void InstalledPluginsSearchTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
if (string.IsNullOrWhiteSpace(pluginManagerDataContext.InstalledPluginsSearchText))
{
foreach (PluginEntryDataContext pluginEntryDataContext in pluginManagerDataContext.InstalledPlugins)
{
pluginEntryDataContext.IsVisible = true;
}
}
else
{
foreach (PluginEntryDataContext pluginEntryDataContext in pluginManagerDataContext.InstalledPlugins)
{
pluginEntryDataContext.IsVisible = pluginEntryDataContext.Name.Contains(pluginManagerDataContext.InstalledPluginsSearchText, StringComparison.OrdinalIgnoreCase);
}
}
}
private async Task SearchAllPlugins(string searchString)
{
App.Logger.LogInfo($"Searching plugins with query: {searchString}", source: "PluginManager");
pluginManagerDataContext.AllPlugins.Clear();
if (!searchString.Contains('*'))
{
searchString = $"*{searchString.Trim()}*";
}
Filter filter = ModFilter.Name.Like($"{searchString}").And(ModFilter.Popular.Desc()).Limit(100);
try
{
IAsyncEnumerable<Mod> mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
await foreach (Mod mod in mods)
{
if (pluginManagerDataContext.InstalledPlugins.Any(p => p.Id == mod.Id))
{
continue;
}
pluginManagerDataContext.AllPlugins.Add(new PluginEntryDataContext(new(mod), new CommandHandler(async () => await Install(mod)), PluginEntryDataContext.Mode.Install));
}
App.Logger.LogInfo($"Search completed: {pluginManagerDataContext.AllPlugins.Count} plugins found", source: "PluginManager");
}
catch (Exception ex)
{
App.Logger.LogError($"Plugin search failed: {ex.Message}", source: "PluginManager");
}
pluginManagerDataContext.IsSearching = false;
}
private async void ReloadPluginsButton_Click(object sender, RoutedEventArgs e)
{
_manager.ReloadPlugins();
await InitializePluginManager();
changed = false;
}
private async void CreatePluginButton_Click(object sender, RoutedEventArgs e)
{
try
{
await CreateNewPlugin();
}
catch (Exception ex)
{
App.Logger.LogError(ex.Message, source: "PluginManager");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Plugin Manager",
Content = ex.Message,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
}
}
private async Task CreateNewPlugin()
{
App.Logger.LogInfo("Creating new plugin", source: "PluginManager");
if (!FileUtilities.ExistsOnPath("dotnet.exe"))
{
App.Logger.LogError(".NET SDK not found on PATH", source: "PluginManager");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Plugin Manager",
Content = "The .NET SDK is required to create a plugin. Please install it and try again.",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
return;
}
string pluginGuid = Guid.NewGuid().ToString();
string pluginPath = Path.Combine(pluginsPath, pluginGuid);
uint pluginId = (uint)Random.Shared.Next(1000, 9999);
CreatePluginDialog inputDialog = new()
{
Owner = Window.GetWindow(this),
};
if (inputDialog.ShowDialog() != true)
{
App.Logger.LogInfo("Plugin creation cancelled by user", source: "PluginManager");
return;
}
string pluginName = inputDialog.ResponseText;
string pluginSafeName = GetPluginSafeName(pluginName);
App.Logger.LogInfo($"Creating plugin with name: {pluginName} (safe name: {pluginSafeName})", source: "PluginManager");
if (!Directory.Exists(pluginDevelopmentPath))
{
_ = Directory.CreateDirectory(pluginDevelopmentPath);
}
string pluginProjectPath = Path.Combine(pluginDevelopmentPath, pluginSafeName);
if (Directory.Exists(pluginProjectPath))
{
App.Logger.LogWarn($"Plugin project already exists: {pluginProjectPath}", source: "PluginManager");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Plugin Manager",
Content = "A plugin with the same name already exists. Please choose a different name.",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
return;
}
pluginManagerDataContext.IsLoading = true;
PluginMetadata pluginMetadata = new(pluginName, pluginId);
_ = Directory.CreateDirectory(pluginPath);
_ = Directory.CreateDirectory(pluginProjectPath);
string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
await File.WriteAllTextAsync(pluginMetadataPath, JsonSerializer.Serialize(pluginMetadata));
if (inputDialog.IsWebPlugin)
{
await CreateNewWebPlugin(pluginPath, pluginName);
changed = true;
await InitializePluginManager();
pluginManagerDataContext.IsLoading = false;
return;
}
App.Logger.LogInfo($"Creating .NET project at: {pluginProjectPath}", source: "PluginManager");
string cmd = $"new classlib -n {pluginSafeName} -o {pluginProjectPath} -f net8.0 --target-framework-override net8.0-windows7";
Process process = Process.Start("dotnet", cmd);
await process.WaitForExitAsync();
App.Logger.LogInfo("Installing NuGet package: DesktopMagic.Api", source: "PluginManager");
process = Process.Start("dotnet", $"add {pluginProjectPath} package DesktopMagic.Api");
await process.WaitForExitAsync();
File.Move(Path.Combine(pluginProjectPath, "Class1.cs"), Path.Combine(pluginProjectPath, $"{pluginSafeName}.cs"));
string code = $@"using DesktopMagic.Api;
using System.Drawing;
namespace {pluginSafeName};
public class {pluginSafeName}Plugin : Plugin
{{
public override Bitmap Main()
{{
Bitmap bmp = new Bitmap(2000, 1000);
bmp.SetResolution(100, 100); // Set DPI to avoid scaling issues.
using (Graphics g = Graphics.FromImage(bmp))
{{
g.DrawString(""Hello World!"", new Font(Application.Theme.Font, 100), new SolidBrush(Application.Theme.PrimaryColor), new PointF(0, 0)); // Draw ""Hello World"" to the image.
}}
return bmp; // Return the image.
}}
}}
";
string csprojPath = Path.Combine(pluginProjectPath, $"{pluginSafeName}.csproj");
await File.WriteAllTextAsync(Path.Combine(pluginProjectPath, $"{pluginSafeName}.cs"), code);
XDocument doc = XDocument.Load(csprojPath);
XElement? propertyGroup = doc.Root?.Element("PropertyGroup");
if (propertyGroup is not null)
{
XElement? outputPathElement = propertyGroup.Element("OutputPath");
if (outputPathElement is not null)
{
outputPathElement.Value = pluginPath;
}
else
{
propertyGroup.Add(new XElement("OutputPath", pluginPath));
}
XElement? appendTargetFrameworkToOutputPathElement = propertyGroup.Element("AppendTargetFrameworkToOutputPath");
if (appendTargetFrameworkToOutputPathElement is not null)
{
appendTargetFrameworkToOutputPathElement.Value = "false";
}
else
{
propertyGroup.Add(new XElement("AppendTargetFrameworkToOutputPath", "false"));
}
XElement? targetNameElement = propertyGroup.Element("TargetName");
if (targetNameElement is not null)
{
targetNameElement.Value = "main";
}
else
{
propertyGroup.Add(new XElement("TargetName", "main"));
}
doc.Save(csprojPath);
App.Logger.LogInfo("Updated .csproj file with custom build settings", source: "PluginManager");
}
else
{
App.Logger.LogError("PropertyGroup element not found in .csproj", source: "PluginManager");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Error",
Content = "PropertyGroup element not found in .csproj",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
return;
}
App.Logger.LogInfo("Building plugin project", source: "PluginManager");
process = Process.Start("dotnet", $"build {csprojPath} -c Release");
await process.WaitForExitAsync();
changed = true;
await InitializePluginManager();
string? associatedProgram = FileUtilities.GetAssociatedProgram(".csproj");
if (associatedProgram is null)
{
App.Logger.LogInfo($"Opening plugin project in Explorer: {pluginProjectPath}", source: "PluginManager");
_ = Process.Start("explorer.exe", pluginProjectPath);
}
else
{
App.Logger.LogInfo($"Opening plugin project in IDE: {associatedProgram}", source: "PluginManager");
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = associatedProgram,
Arguments = csprojPath,
};
_ = Process.Start(psi);
}
App.Logger.LogInfo($"Successfully created plugin: {pluginName}", source: "PluginManager");
pluginManagerDataContext.IsLoading = false;
}
private async Task CreateNewWebPlugin(string pluginPath, string pluginName)
{
App.Logger.LogInfo($"Creating web plugin at: {pluginPath}", source: "PluginManager");
string mainHtml = $@"<!DOCTYPE html>
<html>
<head>
<meta charset=""utf-8"">
<style>
body {{ margin: 0; padding: 16px; font-family: var(--font-family); }}
h1 {{ color: var(--primary-color); }}
</style>
</head>
<body>
<h1 id=""greeting"">{pluginName}</h1>
<script>
function render() {{
const s = window.desktopMagic.getSettings();
document.getElementById('greeting').innerHTML = s.message;
document.getElementById('greeting').style.color = s.color;
document.getElementById('greeting').style.fontWeight = s.bold ? 'bold' : 'normal';
}}
window.desktopMagic.onSettingChanged = (id, value) => render();
render();
</script>
</body>
</html>";
var settings = new List<Dictionary<string, object?>>
{
new() { ["id"] = "message", ["name"] = "Greeting Message", ["type"] = "textbox", ["default"] = "Hello, World!" },
new() { ["id"] = "bold", ["name"] = "Bold Text", ["type"] = "checkbox", ["default"] = true },
new() { ["id"] = "color", ["name"] = "Text Color", ["type"] = "colorpicker", ["default"] = "#FF5722" }
};
string settingsJson = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync(Path.Combine(pluginPath, "settings.json"), settingsJson);
await File.WriteAllTextAsync(Path.Combine(pluginPath, "main.html"), mainHtml);
App.Logger.LogInfo($"Successfully created web plugin: {pluginName}", source: "PluginManager");
}
private async void LogInButton_Click(object sender, RoutedEventArgs e)
{
if (pluginManagerDataContext.IsAuthenticated)
{
App.Logger.LogInfo("Logging out from mod.io", source: "PluginManager");
MainWindowDataContext.GetSettings().ModIoAccessToken = null;
pluginManagerDataContext.IsAuthenticated = false;
return;
}
App.Logger.LogInfo("Starting mod.io authentication", source: "PluginManager");
try
{
InputDialog inputDialog = new((string)FindResource("enterModIoEmail"), "Plugin Manager")
{
Owner = Window.GetWindow(this),
};
if (inputDialog.ShowDialog() != true)
{
App.Logger.LogInfo("Authentication cancelled by user", source: "PluginManager");
return;
}
App.Logger.LogInfo($"Requesting authentication code for email: {inputDialog.ResponseText}", source: "PluginManager");
await modIoClient.Auth.RequestCode(ModIoApiKey, inputDialog.ResponseText);
inputDialog = new((string)FindResource("enterModIoAccessToken"), "Plugin Manager")
{
Owner = Window.GetWindow(this),
};
if (inputDialog.ShowDialog() != true)
{
App.Logger.LogInfo("Authentication cancelled by user", source: "PluginManager");
return;
}
pluginManagerDataContext.IsLoading = true;
AccessToken accessToken = await modIoClient.Auth.SecurityCode(ModIoApiKey, inputDialog.ResponseText);
if (accessToken.Value is not null)
{
modIoClient = new Client(new Credentials(ModIoApiKey, accessToken.Value));
App.Logger.LogInfo("Successfully authenticated with mod.io", source: "PluginManager");
}
MainWindowDataContext.GetSettings().ModIoAccessToken = accessToken.Value;
pluginManagerDataContext.IsAuthenticated = true;
App.Logger.LogInfo("Subscribing to installed plugins on mod.io", source: "PluginManager");
foreach (PluginEntryDataContext plugin in pluginManagerDataContext.InstalledPlugins)
{
if (!plugin.IsLocalPlugin)
{
try
{
await modIoClient.Games[ModIoGameId].Mods.Subscribe(plugin.Id);
App.Logger.LogInfo($"Subscribed to plugin {plugin.Id}", source: "PluginManager");
}
catch (Exception ex)
{
App.Logger.LogError($"Failed to subscribe to plugin {plugin.Id}: {ex.Message}", source: "PluginManager");
}
}
}
await SyncPlugins();
}
catch (Exception ex)
{
App.Logger.LogError($"Authentication failed: {ex.Message}", source: "PluginManager");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Plugin Manager",
Content = ex.Message,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
}
pluginManagerDataContext.IsLoading = false;
}
private async Task SyncPlugins()
{
if (!pluginManagerDataContext.IsAuthenticated)
{
return;
}
App.Logger.LogInfo("Syncing plugins with mod.io subscriptions", source: "PluginManager");
try
{
IReadOnlyList<Mod> mods = await modIoClient.User.GetSubscriptions(ModFilter.GameId.Eq(ModIoGameId)).ToList();
App.Logger.LogInfo($"Found {mods.Count} subscribed plugins on mod.io", source: "PluginManager");
List<PluginEntryDataContext> pluginsToRemove = [];
foreach (PluginEntryDataContext plugin in pluginManagerDataContext.InstalledPlugins)
{
if (!plugin.IsLocalPlugin && !mods.Any(m => m.Id == plugin.Id))
{
App.Logger.LogInfo($"Plugin {plugin.Id} is no longer subscribed, marking for removal", source: "PluginManager");
pluginsToRemove.Add(plugin);
}
}
foreach (PluginEntryDataContext plugin in pluginsToRemove)
{
if (plugin.Path is null)
{
continue;
}
await Remove(plugin.Path, plugin.Id);
}
IEnumerable<Mod> notInstalledMods = mods.Where(m => !pluginManagerDataContext.InstalledPlugins.Any(p => p.Id == m.Id));
foreach (Mod mod in notInstalledMods)
{
App.Logger.LogInfo($"Installing subscribed plugin: {mod.Name} (ID: {mod.Id})", source: "PluginManager");
await Install(mod);
}
App.Logger.LogInfo("Plugin sync completed", source: "PluginManager");
}
catch (Exception ex)
{
App.Logger.LogError($"Plugin sync failed: {ex.Message}", source: "PluginManager");
}
}
private static string GetPluginSafeName(string pluginName)
{
string pluginSafeName = pluginName.ToLower().Replace("_", " ");
TextInfo info = CultureInfo.CurrentCulture.TextInfo;
pluginSafeName = info.ToTitleCase(pluginSafeName);
pluginSafeName = IdentifierNameRegex().Replace(pluginSafeName, "");
return pluginSafeName;
}
private string GetCsprojPath(string pluginName)
{
string pluginSafeName = GetPluginSafeName(pluginName);
return Path.Combine(pluginDevelopmentPath, pluginSafeName, $"{pluginSafeName}.csproj");
}
}