using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin; using Dalamud.Plugin.Services; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Questionable.Controller; using Questionable.Data; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; namespace Questionable.Windows.QuestComponents; internal sealed class SavedPresetsComponent { private sealed class PresetExportData { public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public List QuestIds { get; set; } = new List(); } private readonly QuestController _questController; private readonly QuestFunctions _questFunctions; private readonly QuestData _questData; private readonly QuestRegistry _questRegistry; private readonly Configuration _configuration; private readonly IDalamudPluginInterface _pluginInterface; private readonly IChatGui _chatGui; private readonly UiUtils _uiUtils; private readonly QuestTooltipComponent _questTooltipComponent; private readonly ILogger _logger; private string _newPresetName = string.Empty; private string _newPresetDescription = string.Empty; private bool _showSaveDialog; private string? _presetToDelete; private string? _expandedPreset; private string? _renamingPresetKey; private string _renamePresetName = string.Empty; private string _renamePresetDescription = string.Empty; private const string PresetClipboardPrefix = "qst:preset:"; private const string AllPresetsClipboardPrefix = "qst:presets:"; public SavedPresetsComponent(QuestController questController, QuestFunctions questFunctions, QuestData questData, QuestRegistry questRegistry, Configuration configuration, IDalamudPluginInterface pluginInterface, IChatGui chatGui, UiUtils uiUtils, QuestTooltipComponent questTooltipComponent, ILogger logger) { _questController = questController; _questFunctions = questFunctions; _questData = questData; _questRegistry = questRegistry; _configuration = configuration; _pluginInterface = pluginInterface; _chatGui = chatGui; _uiUtils = uiUtils; _questTooltipComponent = questTooltipComponent; _logger = logger; } public void Draw() { ImGui.TextWrapped("Save your current priority queue as a preset for quick access later. Saved presets are stored in your configuration and persist between sessions."); ImGui.Spacing(); DrawSaveSection(); ImGui.Spacing(); using (ImRaii.IEndObject endObject = ImRaii.Child("SavedPresetsList", new Vector2(-1f, -27f), border: true)) { if (endObject) { DrawSavedPresets(); } } DrawBottomButtons(); } private void DrawSaveSection() { int count = _questController.ManualPriorityQuests.Count; if (_showSaveDialog) { ImGui.Text("Save Current Priority Queue as Preset:"); ImGui.SetNextItemWidth(200f); ImGui.InputTextWithHint("##PresetName", "Preset name...", ref _newPresetName, 64); ImGui.SameLine(); ImGui.SetNextItemWidth(250f); ImGui.InputTextWithHint("##PresetDescription", "Description (optional)...", ref _newPresetDescription, 256); ImGui.SameLine(); using (ImRaii.Disabled(string.IsNullOrWhiteSpace(_newPresetName) || count == 0)) { if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Save, "Save")) { SaveCurrentPreset(); } } ImGui.SameLine(); if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Cancel")) { _showSaveDialog = false; _newPresetName = string.Empty; _newPresetDescription = string.Empty; } if (count == 0) { ImGui.TextColored(ImGuiColors.DalamudYellow, "Add quests to the priority queue first."); } return; } using (ImRaii.Disabled(count == 0)) { if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, $"Save Current Queue ({count} quests)")) { _showSaveDialog = true; } } if (count == 0 && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { ImGui.SetTooltip("Add quests to the priority queue first to save them as a preset."); } } private void DrawSavedPresets() { Dictionary savedPresets = _configuration.General.SavedPresets; if (savedPresets.Count == 0) { ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); ImGui.TextWrapped("No saved presets yet. Save your current priority queue using the button above."); ImGui.PopStyleColor(); return; } foreach (var (key, preset) in savedPresets.OrderBy((KeyValuePair x) => x.Value.Name)) { DrawPresetEntry(key, preset); } if (_presetToDelete != null) { if (savedPresets.Remove(_presetToDelete)) { _pluginInterface.SavePluginConfig(_configuration); _logger.LogInformation("Deleted saved preset '{PresetName}'", _presetToDelete); } _presetToDelete = null; } } private void DrawPresetEntry(string key, Configuration.SavedQuestPreset preset) { using (ImRaii.PushId(key)) { List availableQuests = GetAvailableQuests(preset.QuestIds); List completedQuests = GetCompletedQuests(preset.QuestIds); List alreadyPriorityQuests = GetAlreadyPriorityQuests(preset.QuestIds); string text = preset.Name; List list = new List(); if (availableQuests.Count > 0) { list.Add($"{availableQuests.Count} available"); } if (alreadyPriorityQuests.Count > 0) { list.Add($"{alreadyPriorityQuests.Count} priority"); } if (completedQuests.Count > 0) { list.Add($"{completedQuests.Count} completed"); } if (list.Count > 0) { text = text + " (" + string.Join(", ", list) + ")"; } bool flag = _expandedPreset == key; ImU8String label = new ImU8String(3, 2); label.AppendFormatted(text); label.AppendLiteral("###"); label.AppendFormatted(key); if (ImGui.CollapsingHeader(label, flag ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None)) { _expandedPreset = key; using (ImRaii.PushIndent()) { if (_renamingPresetKey == key) { ImGui.Text("Name:"); ImGui.SameLine(); ImGui.SetNextItemWidth(300f); ImGui.InputTextWithHint("##RenamePreset", "Preset name...", ref _renamePresetName, 64); ImGui.Text("Description:"); ImGui.SameLine(); ImGui.SetNextItemWidth(300f); ImGui.InputTextWithHint("##EditDescription", "Description (optional)...", ref _renamePresetDescription, 256); using (ImRaii.Disabled(string.IsNullOrWhiteSpace(_renamePresetName))) { if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Check, "Save Changes")) { RenamePreset(key, _renamePresetName, _renamePresetDescription); } } ImGui.SameLine(); if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Cancel")) { _renamingPresetKey = null; _renamePresetName = string.Empty; _renamePresetDescription = string.Empty; } } else { if (!string.IsNullOrEmpty(preset.Description)) { ImGui.TextWrapped(preset.Description); } Vector4 col = ImGuiColors.DalamudGrey; ImU8String text2 = new ImU8String(26, 2); text2.AppendLiteral("Created: "); text2.AppendFormatted(preset.CreatedAt.ToLocalTime(), "g"); text2.AppendLiteral(" | Total quests: "); text2.AppendFormatted(preset.QuestIds.Count); ImGui.TextColored(in col, text2); } ImGui.Spacing(); using (ImRaii.Disabled(availableQuests.Count == 0)) { if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, $"Add Available ({availableQuests.Count})")) { AddPresetToPriority(availableQuests); } } if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { if (availableQuests.Count == 0) { ImGui.SetTooltip("No available quests to add (all completed or already in priority)."); } else { ImU8String tooltip = new ImU8String(44, 1); tooltip.AppendLiteral("Add "); tooltip.AppendFormatted(availableQuests.Count); tooltip.AppendLiteral(" available quests to the priority queue."); ImGui.SetTooltip(tooltip); } } ImGui.SameLine(); using (ImRaii.Disabled(alreadyPriorityQuests.Count == 0)) { if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Minus, $"Remove from Priority ({alreadyPriorityQuests.Count})")) { RemovePresetFromPriority(alreadyPriorityQuests); } } if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { if (alreadyPriorityQuests.Count == 0) { ImGui.SetTooltip("No quests from this preset are in the priority queue."); } else { ImU8String tooltip2 = new ImU8String(39, 1); tooltip2.AppendLiteral("Remove "); tooltip2.AppendFormatted(alreadyPriorityQuests.Count); tooltip2.AppendLiteral(" quests from the priority queue."); ImGui.SetTooltip(tooltip2); } } if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Upload, "Export")) { ExportPresetToClipboard(preset); } if (ImGui.IsItemHovered()) { ImGui.SetTooltip("Export this preset to clipboard for sharing."); } ImGui.SameLine(); using (ImRaii.Disabled(_renamingPresetKey != null)) { if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Pen, "Edit")) { _renamingPresetKey = key; _renamePresetName = preset.Name; _renamePresetDescription = preset.Description; } } if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { ImGui.SetTooltip("Edit preset name and description."); } ImGui.SameLine(); int count = _questController.ManualPriorityQuests.Count; using (ImRaii.Disabled(count == 0)) { if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Sync, "Update")) { UpdatePresetQuests(key, preset); } } if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { if (count == 0) { ImGui.SetTooltip("Add quests to the priority queue first."); } else { ImU8String tooltip3 = new ImU8String(71, 1); tooltip3.AppendLiteral("Replace this preset's quests with the current priority queue ("); tooltip3.AppendFormatted(count); tooltip3.AppendLiteral(" quests)."); ImGui.SetTooltip(tooltip3); } } ImGui.SameLine(); using (ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl))) { if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Trash, "Delete")) { _presetToDelete = key; } } if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { ImGui.SetTooltip("Hold CTRL to delete this preset."); } ImGui.Spacing(); ImGui.Separator(); foreach (ElementId item in availableQuests) { if (_questData.TryGetQuestInfo(item, out IQuestInfo questInfo) && _uiUtils.ChecklistItem(questInfo.Name ?? "", ImGuiColors.DalamudYellow, FontAwesomeIcon.Running)) { _questTooltipComponent.Draw(questInfo); } } foreach (ElementId item2 in alreadyPriorityQuests) { if (_questData.TryGetQuestInfo(item2, out IQuestInfo questInfo2) && _uiUtils.ChecklistItem(questInfo2.Name ?? "", ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight)) { _questTooltipComponent.Draw(questInfo2); } } foreach (ElementId item3 in completedQuests) { if (_questData.TryGetQuestInfo(item3, out IQuestInfo questInfo3) && _uiUtils.ChecklistItem(questInfo3.Name ?? "", ImGuiColors.ParsedGreen, FontAwesomeIcon.Check)) { _questTooltipComponent.Draw(questInfo3); } } foreach (ElementId unknownQuest in GetUnknownQuests(preset.QuestIds)) { _uiUtils.ChecklistItem($"Unknown Quest ({unknownQuest})", ImGuiColors.DalamudGrey, FontAwesomeIcon.Question); } ImGui.Spacing(); } } else if (_expandedPreset == key) { _expandedPreset = null; } if (ImGui.IsItemHovered() && !string.IsNullOrEmpty(preset.Description)) { ImGui.BeginTooltip(); ImGui.TextUnformatted(preset.Description); ImGui.EndTooltip(); } } } private void DrawBottomButtons() { PresetExportData presetExportData = ParseClipboardPreset(); List list = ParseClipboardAllPresets(); using (ImRaii.Disabled(presetExportData == null)) { if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Download, "Import Preset") && presetExportData != null) { ImportPresetFromClipboard(presetExportData); } } if (presetExportData == null && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { ImGui.SetTooltip("Copy a valid preset string to clipboard first."); } ImGui.SameLine(); using (ImRaii.Disabled(list == null || list.Count == 0)) { if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.FileImport, "Import All" + ((list != null && list.Count > 0) ? $" ({list.Count})" : "")) && list != null) { ImportAllPresetsFromClipboard(list); } } if ((list == null || list.Count == 0) && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { ImGui.SetTooltip("Copy a valid 'all presets' export string to clipboard first."); } ImGui.SameLine(); using (ImRaii.Disabled(_configuration.General.SavedPresets.Count == 0)) { if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.FileExport, $"Export All ({_configuration.General.SavedPresets.Count})")) { ExportAllPresetsToClipboard(); } } if (_configuration.General.SavedPresets.Count == 0 && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { ImGui.SetTooltip("No saved presets to export."); } ImGui.SameLine(); using (ImRaii.Disabled(_configuration.General.SavedPresets.Count == 0 || !ImGui.IsKeyDown(ImGuiKey.ModCtrl))) { if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Trash, "Clear All")) { _configuration.General.SavedPresets.Clear(); _pluginInterface.SavePluginConfig(_configuration); _logger.LogInformation("Cleared all saved presets"); } } if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { if (_configuration.General.SavedPresets.Count == 0) { ImGui.SetTooltip("No saved presets to clear."); } else { ImGui.SetTooltip("Hold CTRL to clear all saved presets."); } } } private void SaveCurrentPreset() { List list = _questController.ManualPriorityQuests.Select((Quest q) => q.Id).ToList(); string key = GeneratePresetKey(_newPresetName); Configuration.SavedQuestPreset savedQuestPreset = new Configuration.SavedQuestPreset { Name = _newPresetName.Trim(), Description = _newPresetDescription.Trim(), QuestIds = list, CreatedAt = DateTime.UtcNow }; _configuration.General.SavedPresets[key] = savedQuestPreset; _pluginInterface.SavePluginConfig(_configuration); _logger.LogInformation("Saved preset '{PresetName}' with {Count} quests", savedQuestPreset.Name, list.Count); _chatGui.Print($"Saved preset '{savedQuestPreset.Name}' with {list.Count} quests.", "Questionable", 576); _newPresetName = string.Empty; _newPresetDescription = string.Empty; _showSaveDialog = false; } private string GeneratePresetKey(string name) { string text = "user_" + name.Trim().ToUpperInvariant().Replace(' ', '_'); string text2 = text; int num = 1; while (_configuration.General.SavedPresets.ContainsKey(text2)) { text2 = $"{text}_{num}"; num++; } return text2; } private void ExportPresetToClipboard(Configuration.SavedQuestPreset preset) { string s = JsonConvert.SerializeObject(new PresetExportData { Name = preset.Name, Description = preset.Description, QuestIds = preset.QuestIds.Select((ElementId x) => x.ToString()).ToList() }); ImGui.SetClipboardText("qst:preset:" + Convert.ToBase64String(Encoding.UTF8.GetBytes(s))); _chatGui.Print("Exported preset '" + preset.Name + "' to clipboard.", "Questionable", 576); } private void ExportAllPresetsToClipboard() { List list = _configuration.General.SavedPresets.Values.Select((Configuration.SavedQuestPreset preset) => new PresetExportData { Name = preset.Name, Description = preset.Description, QuestIds = preset.QuestIds.Select((ElementId x) => x.ToString()).ToList() }).ToList(); string s = JsonConvert.SerializeObject(list); ImGui.SetClipboardText("qst:presets:" + Convert.ToBase64String(Encoding.UTF8.GetBytes(s))); _chatGui.Print($"Exported {list.Count} presets to clipboard.", "Questionable", 576); } private static PresetExportData? ParseClipboardPreset() { try { string text = ImGui.GetClipboardText().Trim(); if (text.StartsWith("qst:preset:", StringComparison.InvariantCulture)) { string text2 = text; int length = "qst:preset:".Length; string s = text2.Substring(length, text2.Length - length); return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(s))); } } catch (Exception) { } return null; } private static List? ParseClipboardAllPresets() { try { string text = ImGui.GetClipboardText().Trim(); if (text.StartsWith("qst:presets:", StringComparison.InvariantCulture)) { string text2 = text; int length = "qst:presets:".Length; string s = text2.Substring(length, text2.Length - length); return JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(Convert.FromBase64String(s))); } } catch (Exception) { } return null; } private void ImportPresetFromClipboard(PresetExportData exportData) { List list = exportData.QuestIds.Select((string id) => ElementId.FromString(id)).ToList(); string key = GeneratePresetKey(exportData.Name); Configuration.SavedQuestPreset savedQuestPreset = new Configuration.SavedQuestPreset { Name = exportData.Name, Description = exportData.Description, QuestIds = list, CreatedAt = DateTime.UtcNow }; _configuration.General.SavedPresets[key] = savedQuestPreset; _pluginInterface.SavePluginConfig(_configuration); _logger.LogInformation("Imported preset '{PresetName}' with {Count} quests from clipboard", savedQuestPreset.Name, list.Count); _chatGui.Print($"Imported preset '{savedQuestPreset.Name}' with {list.Count} quests.", "Questionable", 576); } private void ImportAllPresetsFromClipboard(List allExportData) { int num = 0; foreach (PresetExportData allExportDatum in allExportData) { List questIds = allExportDatum.QuestIds.Select((string id) => ElementId.FromString(id)).ToList(); string key = GeneratePresetKey(allExportDatum.Name); Configuration.SavedQuestPreset value = new Configuration.SavedQuestPreset { Name = allExportDatum.Name, Description = allExportDatum.Description, QuestIds = questIds, CreatedAt = DateTime.UtcNow }; _configuration.General.SavedPresets[key] = value; num++; } _pluginInterface.SavePluginConfig(_configuration); _logger.LogInformation("Imported {Count} presets from clipboard", num); _chatGui.Print($"Imported {num} presets from clipboard.", "Questionable", 576); } private void AddPresetToPriority(List questIds) { int num = 0; foreach (ElementId questId in questIds) { if (_questController.AddQuestPriority(questId)) { num++; } } if (num > 0) { _chatGui.Print($"Added {num} quests to priority queue.", "Questionable", 576); } _logger.LogInformation("Added {Count} quests from saved preset to priority list", num); } private void RemovePresetFromPriority(List questIds) { int num = 0; foreach (ElementId questId in questIds) { Quest quest = _questController.ManualPriorityQuests.FirstOrDefault((Quest q) => q.Id.Equals(questId)); if (quest != null) { _questController.ManualPriorityQuests.Remove(quest); num++; } } if (num > 0) { _questController.SavePriorityQuests(); _chatGui.Print($"Removed {num} quests from priority queue.", "Questionable", 576); _logger.LogInformation("Removed {Count} quests from priority list", num); } } private void UpdatePresetQuests(string key, Configuration.SavedQuestPreset preset) { List list = _questController.ManualPriorityQuests.Select((Quest q) => q.Id).ToList(); int count = preset.QuestIds.Count; preset.QuestIds = list; _pluginInterface.SavePluginConfig(_configuration); _logger.LogInformation("Updated preset '{PresetName}' from {OldCount} to {NewCount} quests", preset.Name, count, list.Count); _chatGui.Print($"Updated preset '{preset.Name}' with {list.Count} quests (was {count}).", "Questionable", 576); } private void RenamePreset(string key, string newName, string newDescription) { if (_configuration.General.SavedPresets.TryGetValue(key, out Configuration.SavedQuestPreset value)) { string name = value.Name; value.Name = newName.Trim(); value.Description = newDescription.Trim(); _pluginInterface.SavePluginConfig(_configuration); _logger.LogInformation("Updated preset '{OldName}' to '{NewName}'", name, value.Name); } _renamingPresetKey = null; _renamePresetName = string.Empty; _renamePresetDescription = string.Empty; } private List GetAvailableQuests(List questIds) { return (from questId in questIds where _questFunctions.IsReadyToAcceptQuest(questId) || _questFunctions.IsQuestAccepted(questId) where !_questController.ManualPriorityQuests.Any((Quest q) => q.Id.Equals(questId)) where _questRegistry.IsKnownQuest(questId) select questId).ToList(); } private List GetCompletedQuests(List questIds) { return questIds.Where((ElementId questId) => _questFunctions.IsQuestComplete(questId)).ToList(); } private List GetAlreadyPriorityQuests(List questIds) { return (from questId in questIds where _questController.ManualPriorityQuests.Any((Quest q) => q.Id.Equals(questId)) where !_questFunctions.IsQuestComplete(questId) select questId).ToList(); } private List GetUnknownQuests(List questIds) { IQuestInfo questInfo; return questIds.Where((ElementId questId) => !_questData.TryGetQuestInfo(questId, out questInfo)).ToList(); } }