diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 500d5d57..e337997b 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -78,6 +78,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool HideMachinistOffhandFromChangedItems { get; set; } = true; public bool DefaultTemporaryMode { get; set; } = false; public bool EnableDirectoryWatch { get; set; } = false; + public bool EnableAutomaticModImport { get; set; } = false; public bool EnableCustomShapes { get; set; } = true; public PcpSettings PcpSettings = new(); public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; diff --git a/Penumbra/Services/FileWatcher.cs b/Penumbra/Services/FileWatcher.cs index 8a2f9402..e7172f58 100644 --- a/Penumbra/Services/FileWatcher.cs +++ b/Penumbra/Services/FileWatcher.cs @@ -1,5 +1,6 @@ using System.Threading.Channels; using OtterGui.Services; +using Penumbra.Services; using Penumbra.Mods.Manager; namespace Penumbra.Services; @@ -11,16 +12,16 @@ public class FileWatcher : IDisposable, IService private readonly Task _consumer; private readonly ConcurrentDictionary _pending = new(StringComparer.OrdinalIgnoreCase); private readonly ModImportManager _modImportManager; + private readonly MessageService _messageService; private readonly Configuration _config; - private readonly bool _enabled; - public FileWatcher(ModImportManager modImportManager, Configuration config) + public FileWatcher(ModImportManager modImportManager, MessageService messageService, Configuration config) { - _config = config; _modImportManager = modImportManager; - _enabled = config.EnableDirectoryWatch; + _messageService = messageService; + _config = config; - if (!_enabled) return; + if (!_config.EnableDirectoryWatch) return; _queue = Channel.CreateBounded(new BoundedChannelOptions(256) { @@ -55,13 +56,13 @@ public class FileWatcher : IDisposable, IService private void OnPath(object? sender, FileSystemEventArgs e) { // Cheap de-dupe: only queue once per filename until processed - if (!_enabled || !_pending.TryAdd(e.FullPath, 0)) return; + if (!_config.EnableDirectoryWatch || !_pending.TryAdd(e.FullPath, 0)) return; _ = _queue.Writer.TryWrite(e.FullPath); } private async Task ConsumerLoopAsync(CancellationToken token) { - if (!_enabled) return; + if (!_config.EnableDirectoryWatch) return; var reader = _queue.Reader; while (await reader.WaitToReadAsync(token).ConfigureAwait(false)) { @@ -101,8 +102,27 @@ public class FileWatcher : IDisposable, IService var len = fi.Length; if (len > 0 && len == lastLen) { - _modImportManager.AddUnpack(path); - return; + if (_config.EnableAutomaticModImport) + { + _modImportManager.AddUnpack(path); + return; + } + else + { + var invoked = false; + Action installRequest = args => + { + if (invoked) return; + invoked = true; + _modImportManager.AddUnpack(path); + }; + + _messageService.PrintModFoundInfo( + Path.GetFileNameWithoutExtension(path), + installRequest); + + return; + } } lastLen = len; @@ -116,7 +136,7 @@ public class FileWatcher : IDisposable, IService public void UpdateDirectory(string newPath) { - if (!_enabled || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath)) return; + if (!_config.EnableDirectoryWatch || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath)) return; _fsw.EnableRaisingEvents = false; _fsw.Path = newPath; @@ -125,7 +145,7 @@ public class FileWatcher : IDisposable, IService public void Dispose() { - if (!_enabled) return; + if (!_config.EnableDirectoryWatch) return; _fsw.EnableRaisingEvents = false; _cts.Cancel(); _fsw.Dispose(); diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index 70ccf47b..6c13fc38 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -1,19 +1,44 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.EventArgs; using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using OtterGui.Log; using OtterGui.Services; +using OtterGui.Text; using Penumbra.GameData.Data; using Penumbra.Mods.Manager; using Penumbra.String.Classes; +using static OtterGui.Classes.MessageService; using Notification = OtterGui.Classes.Notification; namespace Penumbra.Services; +public class InstallNotification(string message, Action installRequest) : IMessage +{ + private readonly Action _installRequest = installRequest; + private bool _invoked = false; + + public string Message { get; } = message; + + public NotificationType NotificationType => NotificationType.Info; + + public uint NotificationDuration => 10000; + + public void OnNotificationActions(INotificationDrawArgs args) + { + if (ImUtf8.ButtonEx("Install"u8, "Install this mod."u8, disabled: _invoked)) + { + _installRequest(true); + _invoked = true; + } + } +} + public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INotificationManager notificationManager) : OtterGui.Classes.MessageService(log, builder, chat, notificationManager), IService { @@ -55,4 +80,11 @@ public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INoti $"Cowardly refusing to load replacement for {originalGamePath.Filename().ToString().ToLowerInvariant()} by {mod.Name}{(messageComplement.Length > 0 ? ":\n" : ".")}{messageComplement}", NotificationType.Warning, 10000)); } + + public void PrintModFoundInfo(string fileName, Action installRequest) + { + AddMessage( + new InstallNotification($"A new mod has been found: {fileName}", installRequest) + ); + } } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index c84214f3..217b6788 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -53,6 +53,7 @@ public class SettingsTab : ITab, IUiService private readonly MigrationSectionDrawer _migrationDrawer; private readonly CollectionAutoSelector _autoSelector; private readonly CleanupService _cleanupService; + private readonly MessageService _messageService; private readonly AttributeHook _attributeHook; private readonly PcpService _pcpService; @@ -69,7 +70,7 @@ public class SettingsTab : ITab, IUiService CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, FileWatcher fileWatcher, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, - MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, + MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, MessageService messageService, AttributeHook attributeHook, PcpService pcpService) { _pluginInterface = pluginInterface; @@ -96,6 +97,7 @@ public class SettingsTab : ITab, IUiService _migrationDrawer = migrationDrawer; _autoSelector = autoSelector; _cleanupService = cleanupService; + _messageService = messageService; _attributeHook = attributeHook; _pcpService = pcpService; } @@ -649,9 +651,12 @@ public class SettingsTab : ITab, IUiService DrawDefaultModImportFolder(); DrawPcpFolder(); DrawDefaultModExportPath(); - Checkbox("Enable Automatic Import of Mods from Directory", - "Enables a File Watcher that automatically listens for Mod files that enter, causing Penumbra to automatically import these mods.", + Checkbox("Enable Directory Watcher", + "Enables a File Watcher that automatically listens for Mod files that enter, causing Penumbra to open a Popup to import these mods.", _config.EnableDirectoryWatch, v => _config.EnableDirectoryWatch = v); + Checkbox("Enable Fully Automatic Import", + "Uses the File Watcher in order to not just open a Popup, but fully automatically import new mods.", + _config.EnableAutomaticModImport, v => _config.EnableAutomaticModImport = v); DrawFileWatcherPath(); }