diff --git a/ConfigApp/EffectsTreeMenuItem.cs b/ConfigApp/EffectsTreeMenuItem.cs index 3e04fe0a3..8b0709387 100644 --- a/ConfigApp/EffectsTreeMenuItem.cs +++ b/ConfigApp/EffectsTreeMenuItem.cs @@ -87,7 +87,7 @@ public ICommand OnConfigureCommand { get => new TreeMenuItemAction(OnConfigureClick); } - + public TreeMenuItem(string text, TreeMenuItem? parent = null) { Text = text; diff --git a/ConfigApp/Tabs/WorkshopTab.cs b/ConfigApp/Tabs/WorkshopTab.cs index 02c2b3653..557cf4d10 100644 --- a/ConfigApp/Tabs/WorkshopTab.cs +++ b/ConfigApp/Tabs/WorkshopTab.cs @@ -8,6 +8,7 @@ using System.Windows.Data; using System.Windows.Markup; using System.Windows.Media; +using ConfigApp.Workshop; using Microsoft.CSharp.RuntimeBinder; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -66,24 +67,33 @@ private void HandleWorkshopSubmissionsSearchFilter() var view = CollectionViewSource.GetDefaultView(m_WorkshopSubmissionItems); view.Filter = (submissionItem) => { - if (submissionItem is not WorkshopSubmissionItem item || transformedText is null) + if (submissionItem is not WorkshopSubmissionItem item || transformedText is null || transformedText == "") return true; - var texts = new string?[] + foreach (var term in item.SearchTerms) { - item.Name, - item.Author, - item.Description - }; - - foreach (var text in texts) - { - if (text is not null && text.ToLower().Contains(transformedText)) + if (term.Term.ToLower().Contains(transformedText)) return true; - }; + } return false; }; + + foreach (var item in m_WorkshopSubmissionItems) + { + item.HighlightedFiles.Clear(); + if (transformedText is not null && transformedText != "") + { + foreach (var term in item.SearchTerms) + { + if (term.Term.ToLower().Contains(transformedText)) + { + if (term.IsInFile) + item.HighlightedFiles.Add(term.FileName); + } + } + } + } } private void ParseWorkshopSubmissionsFile(byte[] compressedFileContent) @@ -124,9 +134,8 @@ T getDataItem(dynamic item, T defaultValue) return; } - var submissionItem = new WorkshopSubmissionItem() + var submissionItem = new WorkshopSubmissionItem(id) { - Id = id, Name = getDataItem(submissionData.name, "No Name"), Author = getDataItem(submissionData.author, "No Author"), Description = getDataItem(submissionData.description, "No Description"), @@ -135,6 +144,8 @@ T getDataItem(dynamic item, T defaultValue) Sha256 = sha256, }; + submissionItem.UpdateSearchTerms(); + // Remote submissions are fetched before local ones so this submission only exists locally if (isLocal) { @@ -268,6 +279,10 @@ private async void OnRefreshClick(object sender, RoutedEventArgs eventArgs) var button = (Button)sender; button.IsEnabled = false; + foreach (var item in m_WorkshopSubmissionItems) + { + item.Refresh(); + } await ForceRefreshWorkshopContentFromRemote(); button.IsEnabled = true; } diff --git a/ConfigApp/WorkshopEditDialog.xaml b/ConfigApp/Workshop/WorkshopEditDialog.xaml similarity index 82% rename from ConfigApp/WorkshopEditDialog.xaml rename to ConfigApp/Workshop/WorkshopEditDialog.xaml index 6f8861d3e..c3617b07c 100644 --- a/ConfigApp/WorkshopEditDialog.xaml +++ b/ConfigApp/Workshop/WorkshopEditDialog.xaml @@ -16,6 +16,18 @@ + + + diff --git a/ConfigApp/WorkshopEditDialog.xaml.cs b/ConfigApp/Workshop/WorkshopEditDialog.xaml.cs similarity index 70% rename from ConfigApp/WorkshopEditDialog.xaml.cs rename to ConfigApp/Workshop/WorkshopEditDialog.xaml.cs index aa10b2c23..31ac2e0ba 100644 --- a/ConfigApp/WorkshopEditDialog.xaml.cs +++ b/ConfigApp/Workshop/WorkshopEditDialog.xaml.cs @@ -1,4 +1,6 @@ -using System.Windows; +using System.ComponentModel; +using System.Diagnostics; +using System.Windows; namespace ConfigApp { @@ -8,15 +10,39 @@ public enum WorkshopEditDialogMode Install } + public enum WorkshopSubmissionFileType + { + Script, + Sound, + Text, + Undefined + } + public class WorkshopSubmissionFile : IComparable { public string Name { get; private set; } public bool IsEnabled { get; private set; } + public WorkshopSubmissionFileType Type { get; private set; } public EffectData? EffectData { get; private set; } public WorkshopSubmissionFile(string name, bool enabled, EffectData? effectData = null) { Name = name; + switch (name[^4..]) + { + case ".lua": + Type = WorkshopSubmissionFileType.Script; + break; + case ".mp3": + Type = WorkshopSubmissionFileType.Sound; + break; + case ".txt": + Type = WorkshopSubmissionFileType.Text; + break; + default: + Type = WorkshopSubmissionFileType.Undefined; + break; + } IsEnabled = enabled; EffectData = effectData; } @@ -50,7 +76,7 @@ public partial class WorkshopEditDialog : Window private readonly WorkshopEditDialogMode m_DialogMode; - public WorkshopEditDialog(List files, WorkshopEditDialogMode dialogMode) + public WorkshopEditDialog(List files, WorkshopEditDialogMode dialogMode, string? path = null, List? highlightedFiles = null) { InitializeComponent(); @@ -66,17 +92,17 @@ public WorkshopEditDialog(List files, WorkshopEditDialog button_save_or_no.Content = "No"; } - TreeMenuItem generateItem(string text, TreeMenuItem? parent = null) + TreeMenuItem generateItem(string text, TreeMenuItem? parent = null, bool showCheckbox = true) { var item = new TreeMenuItem(text, parent); - if (m_DialogMode == WorkshopEditDialogMode.Install) + if (m_DialogMode == WorkshopEditDialogMode.Install || !showCheckbox) item.CheckBoxVisiblity = Visibility.Collapsed; return item; } var luaParentItem = generateItem("Scripts"); var mp3ParentItem = generateItem("Sounds"); - var txtParentItem = generateItem("Text Files"); + var txtParentItem = generateItem("Text Files", showCheckbox: false); var parentFolderItems = new Dictionary(); @@ -91,19 +117,15 @@ TreeMenuItem generateItem(string text, TreeMenuItem? parent = null) var pathFragments = (pathName.StartsWith("sounds\\") ? pathName[7..] : pathName).Split('\\'); TreeMenuItem targetItem; - bool isConfigurable = false; - switch (pathName[^4..]) + switch (file.Type) { - case ".lua": + case WorkshopSubmissionFileType.Script: targetItem = luaParentItem; - isConfigurable = true; break; - case ".mp3": + case WorkshopSubmissionFileType.Sound: targetItem = mp3ParentItem; break; - case ".txt": - if (m_DialogMode != WorkshopEditDialogMode.Install) - continue; + case WorkshopSubmissionFileType.Text: targetItem = txtParentItem; break; default: @@ -137,23 +159,43 @@ TreeMenuItem generateItem(string text, TreeMenuItem? parent = null) } } - var menuItem = generateItem(pathFragments.Last(), targetItem); + var menuItem = generateItem(pathFragments.Last(), targetItem, file.Type != WorkshopSubmissionFileType.Text); var fileState = new WorkshopSubmissionFileState(menuItem, pathName, file.EffectData); - menuItem.ForceConfigHidden = m_DialogMode != WorkshopEditDialogMode.Edit || !isConfigurable; + menuItem.ForceConfigHidden = m_DialogMode != WorkshopEditDialogMode.Edit; menuItem.OnConfigureClick = () => { - var effectConfig = new EffectConfig(null, fileState.EffectData, new Effects.EffectInfo() + if (file.Type == WorkshopSubmissionFileType.Script) { - Name = pathName, - IsTimed = true - }); - effectConfig.ShowDialog(); + var effectConfig = new EffectConfig(null, fileState.EffectData, new Effects.EffectInfo() + { + Name = pathName, + IsTimed = true + }); + effectConfig.ShowDialog(); - if (!effectConfig.IsSaved) - return; + if (!effectConfig.IsSaved) + return; - fileState.EffectData = effectConfig.GetNewData(); + fileState.EffectData = effectConfig.GetNewData(); + } + else + { + try + { + System.Diagnostics.Process.Start(new ProcessStartInfo(path is not null ? path.Replace('/', '\\') + pathName : pathName) { UseShellExecute = true }); + } + catch (Win32Exception) + { + MessageBox.Show("Error: File not found", "ChaosModV", MessageBoxButton.OK, MessageBoxImage.Error); + } + } }; + + if (highlightedFiles?.Contains(pathName) ?? false) + { + menuItem.IsColored = true; + } + targetItem.AddChild(menuItem); FileStates.Add(fileState); diff --git a/ConfigApp/WorkshopInfoHandler.cs b/ConfigApp/Workshop/WorkshopInfoHandler.cs similarity index 96% rename from ConfigApp/WorkshopInfoHandler.cs rename to ConfigApp/Workshop/WorkshopInfoHandler.cs index eb29721e5..38e7cdb8a 100644 --- a/ConfigApp/WorkshopInfoHandler.cs +++ b/ConfigApp/Workshop/WorkshopInfoHandler.cs @@ -1,7 +1,7 @@ using System.Windows; using System.Windows.Input; -namespace ConfigApp +namespace ConfigApp.Workshop { public class WorkshopInfoHandler : ICommand { diff --git a/ConfigApp/WorkshopInstallHandler.cs b/ConfigApp/Workshop/WorkshopInstallHandler.cs similarity index 99% rename from ConfigApp/WorkshopInstallHandler.cs rename to ConfigApp/Workshop/WorkshopInstallHandler.cs index 46bd325aa..404e4225a 100644 --- a/ConfigApp/WorkshopInstallHandler.cs +++ b/ConfigApp/Workshop/WorkshopInstallHandler.cs @@ -9,7 +9,7 @@ using Newtonsoft.Json.Linq; using ZstdSharp; -namespace ConfigApp +namespace ConfigApp.Workshop { public class WorkshopInstallHandler : ICommand { @@ -173,7 +173,6 @@ string getFileSha256(byte[] buffer) var fileStream = new MemoryStream(fileContent); if (isFileCompressed) - { try { var decompressor = new Decompressor(); @@ -185,7 +184,6 @@ string getFileSha256(byte[] buffer) // File content is not (zstd) compressed even though compressed = yes? // Skip decompression } - } try { diff --git a/ConfigApp/WorkshopSettingsDialog.xaml b/ConfigApp/Workshop/WorkshopSettingsDialog.xaml similarity index 100% rename from ConfigApp/WorkshopSettingsDialog.xaml rename to ConfigApp/Workshop/WorkshopSettingsDialog.xaml diff --git a/ConfigApp/WorkshopSettingsDialog.xaml.cs b/ConfigApp/Workshop/WorkshopSettingsDialog.xaml.cs similarity index 100% rename from ConfigApp/WorkshopSettingsDialog.xaml.cs rename to ConfigApp/Workshop/WorkshopSettingsDialog.xaml.cs diff --git a/ConfigApp/Workshop/WorkshopSettingsHandler.cs b/ConfigApp/Workshop/WorkshopSettingsHandler.cs new file mode 100644 index 000000000..75400a269 --- /dev/null +++ b/ConfigApp/Workshop/WorkshopSettingsHandler.cs @@ -0,0 +1,57 @@ +using System.IO; +using System.Windows; +using System.Windows.Input; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace ConfigApp.Workshop +{ + public class WorkshopSettingsHandler : ICommand + { + public event EventHandler? CanExecuteChanged = null; + + private readonly WorkshopSubmissionItem m_SubmissionItem; + private readonly WorkshopSubmissionFileHandler m_FileHandler; + + public WorkshopSettingsHandler(WorkshopSubmissionItem submissionItem, WorkshopSubmissionFileHandler fileHandler) + { + m_SubmissionItem = submissionItem; + m_FileHandler = fileHandler; + } + + public bool CanExecute(object? parameter) + { + return true; + } + + public void Execute(object? parameter) + { + List files; + + try + { + m_FileHandler.ReloadFiles(); + files = m_FileHandler.GetSubmissionFiles(); + m_SubmissionItem.UpdateSearchTerms(); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "ChaosModV", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + var editWindow = new WorkshopEditDialog(files, WorkshopEditDialogMode.Edit, m_FileHandler.SubmissionDirectory, m_SubmissionItem.HighlightedFiles); + editWindow.ShowDialog(); + + try + { + m_FileHandler.SetSettings(editWindow.FileStates); + m_SubmissionItem.UpdateSearchTerms(); + } + catch (Exception) + { + MessageBox.Show("Error while saving settings! Check that workshop folder has write permissions", "ChaosModV", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + } +} diff --git a/ConfigApp/Workshop/WorkshopSubmissionFileHandler.cs b/ConfigApp/Workshop/WorkshopSubmissionFileHandler.cs new file mode 100644 index 000000000..37d643181 --- /dev/null +++ b/ConfigApp/Workshop/WorkshopSubmissionFileHandler.cs @@ -0,0 +1,103 @@ +namespace ConfigApp.Workshop +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using Newtonsoft.Json.Linq; + using System.Windows; + using Newtonsoft.Json; + + public class WorkshopSubmissionFileHandler + { + private readonly WorkshopSubmissionItem m_SubmissionItem; + + public String SubmissionDirectory => $"workshop/{m_SubmissionItem.Id}/"; + private String SubmissionSettingsFile => $"workshop/{m_SubmissionItem.Id}.json"; + + private readonly List m_Files; + + public WorkshopSubmissionFileHandler(WorkshopSubmissionItem submissionItem) + { + m_SubmissionItem = submissionItem; + m_Files = new List(); + + ReloadFiles(); + } + + public List GetSubmissionFiles() + { + return m_Files; + } + + public void SetSettings(List states) + { + var disabledFilesArrayJson = new JArray(); + var scriptSettingsObjectJson = new JObject(); + foreach (var state in states) + { + if (!state.Item.IsChecked) + disabledFilesArrayJson.Add(state.FullPath); + + if (state.EffectData != null) + // Don't save settings if everything is set 1:1 as defaults + if (JsonConvert.SerializeObject(state.EffectData) != JsonConvert.SerializeObject(new EffectData())) + scriptSettingsObjectJson[state.FullPath] = JToken.FromObject(state.EffectData); + } + + var newJson = new JObject(); + if (disabledFilesArrayJson.Count > 0) + newJson.Add(new JProperty("disabled_files", disabledFilesArrayJson)); + if (scriptSettingsObjectJson.Count > 0) + newJson.Add(new JProperty("effect_settings", scriptSettingsObjectJson)); + + if (newJson.Count == 0) + File.Delete(SubmissionSettingsFile); + else + File.WriteAllText(SubmissionSettingsFile, $"{newJson}"); + } + + public void ReloadFiles() + { + m_Files.Clear(); + + if (Directory.Exists(SubmissionDirectory)) + { + var disabledFiles = new List(); + var scriptSettings = new Dictionary(); + if (File.Exists(SubmissionSettingsFile)) + try + { + var fileText = File.ReadAllText(SubmissionSettingsFile); + if (!string.IsNullOrWhiteSpace(fileText)) + { + var json = JObject.Parse(fileText); + + if (json.ContainsKey("disabled_files")) + disabledFiles.AddRange(json["disabled_files"]?.Select(file => file.Value() ?? string.Empty) ?? Array.Empty()); + + if (json.ContainsKey("effect_settings")) + { + var scriptSettingsJson = json["effect_settings"]?.ToObject>(); + if (scriptSettingsJson is not null) + scriptSettings = scriptSettingsJson; + } + } + } + catch (JsonException) + { + throw new Exception("Submission settings file is corrupt, assuming default settings!"); + } + + foreach (var file in Directory.EnumerateFiles(SubmissionDirectory, "*", SearchOption.AllDirectories)) + { + var pathName = file.Replace(SubmissionDirectory, ""); + m_Files.Add(new WorkshopSubmissionFile(pathName, !disabledFiles.Contains(pathName), + scriptSettings.ContainsKey(pathName) ? scriptSettings[pathName] : null)); + } + } + } + } +} diff --git a/ConfigApp/WorkshopSubmissionItem.cs b/ConfigApp/Workshop/WorkshopSubmissionItem.cs similarity index 54% rename from ConfigApp/WorkshopSubmissionItem.cs rename to ConfigApp/Workshop/WorkshopSubmissionItem.cs index 2d5e01231..c78b799af 100644 --- a/ConfigApp/WorkshopSubmissionItem.cs +++ b/ConfigApp/Workshop/WorkshopSubmissionItem.cs @@ -1,28 +1,59 @@ using System.ComponentModel; using System.Diagnostics; using System.Drawing; +using System.IO; +using System.Text.RegularExpressions; using System.Windows; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media.Imaging; +using System.Windows.Media.Media3D; -namespace ConfigApp +namespace ConfigApp.Workshop { + public class SearchTerm + { + public string Term { get; } + public bool IsInFile { get; } + public string FileName { get; } + + public SearchTerm(string term, string? filename = null) + { + Term = term; + if (filename is not null) + { + IsInFile = true; + FileName = filename; + } + else + { + IsInFile = false; + FileName = ""; + } + } + + public static implicit operator SearchTerm(string term) => new(term); + } + public class WorkshopSubmissionItem : INotifyPropertyChanged { private static BitmapSource? ms_DefaultIcon = null; + private readonly WorkshopSubmissionFileHandler m_FileHandler; + public event PropertyChangedEventHandler? PropertyChanged = null; - public string? Id { get; set; } = null; - public string? Name { get; set; } = null; - public string? Author { get; set; } = null; - public string? Description { get; set; } = null; - public string? Version { get; set; } = null; - public int? LastUpdated { get; set; } = null; - public string? Sha256 { get; set; } = null; - public BitmapSource? SubmissionIcon { get; set; } = null; + public string? Id { get; private init; } = null; + public string? Name { get; init; } = null; + public string? Author { get; init; } = null; + public string? Description { get; init; } = null; + public string? Version { get; init; } = null; + public int? LastUpdated { get; init; } = null; + public string? Sha256 { get; init; } = null; + public BitmapSource? SubmissionIcon { get; init; } = null; public bool IsAlien { get; set; } = false; + public List SearchTerms { get; } = new(); + public List HighlightedFiles { get; } = new List(); // In order for sorting public enum SubmissionInstallState @@ -90,11 +121,57 @@ public ICommand InfoButtonCommand public ICommand SettingsButtonCommand { - get => new WorkshopSettingsHandler(this); + get => new WorkshopSettingsHandler(this, m_FileHandler); } public Visibility SettingsButtonVisibility { get; private set; } = Visibility.Hidden; - public WorkshopSubmissionItem() + public void Refresh() + { + m_FileHandler.ReloadFiles(); + UpdateSearchTerms(); + } + + public void UpdateSearchTerms() + { + SearchTerms.Clear(); + + if (Name is not null) + SearchTerms.Add(Name); + if (Description is not null) + SearchTerms.Add(Description); + if (Author is not null) + SearchTerms.Add(Author); + + foreach (var file in m_FileHandler.GetSubmissionFiles()) + { + SearchTerms.Add(new(file.Name, file.Name)); + if (file.EffectData?.CustomName is not null) + { + SearchTerms.Add(new(file.EffectData.CustomName, file.Name)); + } + + if (file.Type == WorkshopSubmissionFileType.Script) + { + try + { + foreach (var line in File.ReadAllLines(m_FileHandler.SubmissionDirectory + file.Name)) + { + var match = Regex.Match(line, @"(?:Name|ScriptId)\s*=\s*""((?:\\""|[^""])+)"""); + if (match.Success) + { + SearchTerms.Add(new(match.Groups[1].Value, file.Name)); + } + } + } + catch (Exception exception) when (exception is IOException || exception is FileNotFoundException) + { + continue; + } + } + } + } + + public WorkshopSubmissionItem(string id) { if (ms_DefaultIcon == null) { @@ -115,6 +192,10 @@ public WorkshopSubmissionItem() } SubmissionIcon = ms_DefaultIcon; + + Id = id; + + m_FileHandler = new(this); } } } diff --git a/ConfigApp/WorkshopSettingsHandler.cs b/ConfigApp/WorkshopSettingsHandler.cs deleted file mode 100644 index 61f5a6356..000000000 --- a/ConfigApp/WorkshopSettingsHandler.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.IO; -using System.Windows; -using System.Windows.Input; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace ConfigApp -{ - public class WorkshopSettingsHandler : ICommand - { - public event EventHandler? CanExecuteChanged = null; - - private readonly WorkshopSubmissionItem m_SubmissionItem; - - public WorkshopSettingsHandler(WorkshopSubmissionItem submissionItem) - { - m_SubmissionItem = submissionItem; - } - - public bool CanExecute(object? parameter) - { - return true; - } - - public void Execute(object? parameter) - { - var id = m_SubmissionItem.Id; - var submissionDir = $"workshop/{id}/"; - if (!Directory.Exists(submissionDir)) - return; - - var submissionSettingsFile = $"workshop/{id}.json"; - var disabledFiles = new List(); - var scriptSettings = new Dictionary(); - if (File.Exists(submissionSettingsFile)) - { - try - { - var fileText = File.ReadAllText(submissionSettingsFile); - if (!string.IsNullOrWhiteSpace(fileText)) - { - var json = JObject.Parse(fileText); - - if (json.ContainsKey("disabled_files")) - disabledFiles.AddRange(json["disabled_files"]?.Select(file => file.Value() ?? string.Empty) ?? Array.Empty()); - - if (json.ContainsKey("effect_settings")) - { - var scriptSettingsJson = json["effect_settings"]?.ToObject>(); - if (scriptSettingsJson is not null) - scriptSettings = scriptSettingsJson; - } - } - } - catch (JsonException) - { - MessageBox.Show($"Submission settings file is corrupt, assuming default settings!", "ChaosModV", MessageBoxButton.OK, MessageBoxImage.Error); - } - } - - var files = new List(); - foreach (var file in Directory.EnumerateFiles(submissionDir, "*", SearchOption.AllDirectories)) - { - var pathName = file.Replace(submissionDir, ""); - files.Add(new WorkshopSubmissionFile(pathName, !disabledFiles.Contains(pathName), - scriptSettings.ContainsKey(pathName) ? scriptSettings[pathName] : null)); - } - - var editWindow = new WorkshopEditDialog(files, WorkshopEditDialogMode.Edit); - editWindow.ShowDialog(); - - var disabledFilesArrayJson = new JArray(); - var scriptSettingsObjectJson = new JObject(); - foreach (var state in editWindow.FileStates) - { - if (!state.Item.IsChecked) - disabledFilesArrayJson.Add(state.FullPath); - - if (state.EffectData != null) - { - // Don't save settings if everything is set 1:1 as defaults - if (JsonConvert.SerializeObject(state.EffectData) != JsonConvert.SerializeObject(new EffectData())) - scriptSettingsObjectJson[state.FullPath] = JToken.FromObject(state.EffectData); - } - } - - var newJson = new JObject(); - if (disabledFilesArrayJson.Count > 0) - newJson.Add(new JProperty("disabled_files", disabledFilesArrayJson)); - if (scriptSettingsObjectJson.Count > 0) - newJson.Add(new JProperty("effect_settings", scriptSettingsObjectJson)); - - if (newJson.Count == 0) - File.Delete(submissionSettingsFile); - else - File.WriteAllText(submissionSettingsFile, $"{newJson}"); - } - } -}