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.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; using Questionable.Windows.Utils; 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? _editingPresetKey; private string _editPresetName = string.Empty; private string _editPresetDescription = string.Empty; private string? _lastClipboardText; private PresetExportData? _clipboardPreset; private List? _clipboardAllPresets; 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() { DrawSaveSection(); UiThemeUtils.SectionSpacing(); DrawSavedPresets(); UiThemeUtils.SectionSpacing(); DrawBottomButtons(); } private void DrawSaveSection() { int count = _questController.ManualPriorityQuests.Count; float x = ImGui.GetContentRegionAvail().X; float x2 = ImGui.GetStyle().ItemSpacing.X; float width = (x - x2) / 2f; if (_showSaveDialog) { ImGui.SetNextItemWidth(x); ImGui.InputTextWithHint("##PresetName", "Preset name...", ref _newPresetName, 64); ImGui.SetNextItemWidth(x); ImGui.InputTextWithHint("##PresetDescription", "Description (optional)...", ref _newPresetDescription, 256); using (ImRaii.Disabled(string.IsNullOrWhiteSpace(_newPresetName) || count == 0)) { if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Save, "Save", width)) { SaveCurrentPreset(); } } ImGui.SameLine(); if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Times, "Cancel", width)) { _showSaveDialog = false; _newPresetName = string.Empty; _newPresetDescription = string.Empty; } return; } using (ImRaii.Disabled(count == 0)) { if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Plus, $"Save Current Queue ({count} quests)", x)) { _showSaveDialog = true; } } if (count == 0 && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { ImGui.SetTooltip("Add quests to the priority queue first."); } } private void DrawSavedPresets() { Dictionary savedPresets = _configuration.General.SavedPresets; if (savedPresets.Count == 0) { string[] array = new string[3] { "No saved presets yet.", "Save your current priority queue using the", "button above." }; float x = ImGui.GetContentRegionAvail().X; using (ImRaii.PushColor(ImGuiCol.Text, UiThemeUtils.MutedTextColor)) { string[] array2 = array; foreach (string obj in array2) { float x2 = ImGui.CalcTextSize(obj).X; ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (x - x2) / 2f); ImGui.TextUnformatted(obj); } return; } } bool flag = true; foreach (var (key, preset) in savedPresets.OrderBy((KeyValuePair keyValuePair2) => keyValuePair2.Value.Name)) { if (!flag) { ImGui.Spacing(); } flag = false; 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)) { (Vector2 ContentStartPos, float AvailableWidth, ImDrawListPtr DrawList) tuple = UiThemeUtils.BeginCard(); Vector2 item = tuple.ContentStartPos; float item2 = tuple.AvailableWidth; ImDrawListPtr item3 = tuple.DrawList; List availableQuests = GetAvailableQuests(preset.QuestIds); int count = preset.QuestIds.Count; string text = $"{count} {((count == 1) ? "quest" : "quests")}"; ImGui.GetCursorPosX(); float num = ImGui.GetContentRegionAvail().X - 12f; bool flag = _expandedPreset == key; float frameHeight = ImGui.GetFrameHeight(); Vector2 cursorScreenPos = ImGui.GetCursorScreenPos(); using (ImRaii.PushColor(ImGuiCol.Button, Vector4.Zero)) { using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UiThemeUtils.SubtleHoverColor)) { using (ImRaii.PushColor(ImGuiCol.ButtonActive, UiThemeUtils.SubtlePressColor)) { ImU8String label = new ImU8String(2, 1); label.AppendLiteral("##"); label.AppendFormatted(key); if (ImGui.Button(label, new Vector2(num, frameHeight))) { _expandedPreset = (flag ? null : key); flag = _expandedPreset == key; } } } } ImDrawListPtr windowDrawList = ImGui.GetWindowDrawList(); string text2 = (flag ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(); Vector2 vector; using (ImRaii.PushFont(UiBuilder.IconFont)) { vector = ImGui.CalcTextSize(text2); } float y = cursorScreenPos.Y + (frameHeight - ImGui.GetTextLineHeight()) / 2f; float y2 = cursorScreenPos.Y + (frameHeight - vector.Y) / 2f; uint colorU = ImGui.GetColorU32(ImGuiCol.Text); Vector2 vector2 = ImGui.CalcTextSize(text); float x = ImGui.GetStyle().FramePadding.X; float num2 = cursorScreenPos.X + x + vector.X + 6f; float num3 = cursorScreenPos.X + num - vector2.X - x; windowDrawList.AddText(UiBuilder.IconFont, ImGui.GetFontSize(), new Vector2(cursorScreenPos.X + x, y2), colorU, text2); windowDrawList.AddText(new Vector2(num2, y), colorU, UiThemeUtils.TruncateToWidth(preset.Name, num3 - num2 - ImGui.GetStyle().ItemSpacing.X)); windowDrawList.AddText(new Vector2(num3, y), ImGui.ColorConvertFloat4ToU32(UiThemeUtils.MutedTextColor), text); if (flag) { if (_editingPresetKey == key) { float x2 = ImGui.GetContentRegionAvail().X; ImGui.SetNextItemWidth(x2); ImGui.InputTextWithHint("##EditName", "Preset name...", ref _editPresetName, 64); ImGui.SetNextItemWidth(x2); ImGui.InputTextWithHint("##EditDesc", "Description (optional)...", ref _editPresetDescription, 256); float width = (x2 - ImGui.GetStyle().ItemSpacing.X) / 2f; using (ImRaii.Disabled(string.IsNullOrWhiteSpace(_editPresetName))) { if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Save, "Save", width)) { preset.Name = _editPresetName.Trim(); preset.Description = _editPresetDescription.Trim(); _pluginInterface.SavePluginConfig(_configuration); _editingPresetKey = null; } } ImGui.SameLine(); if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Times, "Cancel", width)) { _editingPresetKey = null; } } else { using (ImRaii.PushColor(ImGuiCol.Text, UiThemeUtils.DimTextColor)) { foreach (ElementId questId in preset.QuestIds) { if (_questData.TryGetQuestInfo(questId, out IQuestInfo questInfo)) { UiThemeUtils.WrappedText(questInfo.Name); if (ImGui.IsItemHovered()) { _questTooltipComponent.Draw(questInfo); } } else { UiThemeUtils.WrappedText(questId.ToString()); } } } ImGui.Spacing(); float width2 = (num - ImGui.GetStyle().ItemSpacing.X * 3f) / 4f; using (ImRaii.Disabled(availableQuests.Count == 0)) { if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Plus, "Add", width2)) { AddPresetToPriority(availableQuests); } } ImGui.SameLine(); if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Upload, "Export", width2)) { ExportPresetToClipboard(preset); } ImGui.SameLine(); if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Pen, "Edit", width2)) { _editingPresetKey = key; _editPresetName = preset.Name; _editPresetDescription = preset.Description; } ImGui.SameLine(); using (ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl))) { if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Trash, "Delete", width2)) { _presetToDelete = key; } } if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { ImGui.SetTooltip("Hold CTRL to delete"); } } } UiThemeUtils.EndCard(item, item2, item3); } } private void DrawBottomButtons() { PresetExportData presetExportData = ParseClipboardPreset(); List list = ParseClipboardAllPresets(); float x = ImGui.GetContentRegionAvail().X; float x2 = ImGui.GetStyle().ItemSpacing.X; float width = (x - x2) / 2f; using (ImRaii.Disabled(presetExportData == null && list == null)) { string text = ((list != null) ? $"Import ({list.Count})" : "Import"); if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Download, text, width)) { if (list != null) { ImportAllPresetsFromClipboard(list); } else if (presetExportData != null) { ImportPresetFromClipboard(presetExportData); } } } if (presetExportData == null && list == null && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { ImGui.SetTooltip("Copy a valid preset string to clipboard first."); } ImGui.SameLine(); int count = _configuration.General.SavedPresets.Count; using (ImRaii.Disabled(count == 0)) { if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Upload, $"Export ({count})", width)) { ExportAllPresetsToClipboard(); } } if (count == 0 && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { ImGui.SetTooltip("No saved presets to export."); } } 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 void RefreshClipboardPresets() { string text = ThrottledClipboard.GetText(); if (text == _lastClipboardText) { return; } _lastClipboardText = text; _clipboardPreset = null; _clipboardAllPresets = null; try { if (text.StartsWith("qst:preset:", StringComparison.InvariantCulture)) { string s = text.Substring("qst:preset:".Length); string value = Encoding.UTF8.GetString(Convert.FromBase64String(s)); _clipboardPreset = JsonConvert.DeserializeObject(value); } else if (text.StartsWith("qst:presets:", StringComparison.InvariantCulture)) { string s2 = text.Substring("qst:presets:".Length); string value2 = Encoding.UTF8.GetString(Convert.FromBase64String(s2)); _clipboardAllPresets = JsonConvert.DeserializeObject>(value2); } } catch (Exception exception) { _logger.LogDebug(exception, "Failed to parse presets from clipboard"); } } private PresetExportData? ParseClipboardPreset() { RefreshClipboardPresets(); return _clipboardPreset; } private List? ParseClipboardAllPresets() { RefreshClipboardPresets(); return _clipboardAllPresets; } private bool PresetNameExists(string name) { return _configuration.General.SavedPresets.Values.Any((Configuration.SavedQuestPreset p) => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)); } private void ImportPresetFromClipboard(PresetExportData exportData) { try { if (PresetNameExists(exportData.Name)) { _chatGui.Print("Preset '" + exportData.Name + "' already exists, skipping.", "Questionable", 576); return; } int skippedCount; List list = ElementId.FromStrings(exportData.QuestIds, out skippedCount); if (list.Count == 0) { _chatGui.PrintError("Preset '" + exportData.Name + "' contains no importable quests.", "Questionable", 576); return; } 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, skipped {Skipped} unknown ids", savedQuestPreset.Name, list.Count, skippedCount); string text = $"Imported preset '{savedQuestPreset.Name}' with {list.Count} quests."; if (skippedCount > 0) { text += $" Skipped {skippedCount} unknown quest ids."; } _chatGui.Print(text, "Questionable", 576); } catch (Exception ex) { _logger.LogError(ex, "Failed to import preset from clipboard"); _chatGui.PrintError("Failed to import preset: " + ex.Message, "Questionable", 576); } } private void ImportAllPresetsFromClipboard(List allExportData) { try { int num = 0; int num2 = 0; int num3 = 0; HashSet hashSet = _configuration.General.SavedPresets.Values.Select((Configuration.SavedQuestPreset p) => p.Name).ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (PresetExportData allExportDatum in allExportData) { if (hashSet.Contains(allExportDatum.Name)) { num2++; continue; } int skippedCount; List list = ElementId.FromStrings(allExportDatum.QuestIds, out skippedCount); num3 += skippedCount; if (list.Count == 0) { num2++; continue; } string key = GeneratePresetKey(allExportDatum.Name); Configuration.SavedQuestPreset value = new Configuration.SavedQuestPreset { Name = allExportDatum.Name, Description = allExportDatum.Description, QuestIds = list, CreatedAt = DateTime.UtcNow }; _configuration.General.SavedPresets[key] = value; num++; } if (num > 0) { _pluginInterface.SavePluginConfig(_configuration); } string text = $"Imported {num} presets from clipboard."; if (num2 > 0) { text += $" Skipped {num2} (already exist or empty)."; } if (num3 > 0) { text += $" Skipped {num3} unknown quest ids."; } _logger.LogInformation("Imported {Imported} presets from clipboard, skipped {Skipped} presets and {SkippedIds} unknown ids", num, num2, num3); _chatGui.Print(text, "Questionable", 576); } catch (Exception ex) { _logger.LogError(ex, "Failed to import presets from clipboard"); _chatGui.PrintError("Failed to import presets: " + ex.Message, "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 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 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(); } }