├── .editorconfig ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug-report.yaml │ └── config.yml ├── .gitignore ├── .gitmodules ├── BG3ModManager.sln ├── BuildRelease.py ├── LICENSE ├── README.md ├── Screenshots └── PreferencesWindow_GameDataPath.png ├── Update.xml └── src ├── Core ├── AppServices │ ├── FileWatcherService.cs │ ├── ModRegistryService.cs │ └── ScreenReaderService.cs ├── Attributes │ ├── MenuSettingsAttribute.cs │ ├── ScreenReaderAttribute.cs │ └── SettingsEntryAttribute.cs ├── Directory.build.props ├── DivinityApp.cs ├── DivinityInteractions.cs ├── DivinityModManagerCore.csproj ├── Enums │ ├── AlertType.cs │ ├── DivinityExtenderModStatus.cs │ ├── DivinityGameLaunchWindowAction.cs │ ├── DivinityOsirisModStatus.cs │ ├── Extender │ │ └── ExtenderUpdateChannel.cs │ ├── LaunchGameType.cs │ ├── ModSourceType.cs │ ├── ScriptExtenderIconType.cs │ └── Steam │ │ └── EPublishedFileQueryType.cs ├── Extensions │ ├── DictionaryExtensions.cs │ ├── EnumExtensions.cs │ ├── KeyExtensions.cs │ ├── ModelExtensions.cs │ ├── ReactiveExtensions.cs │ ├── ResourceExtensions.cs │ └── StringExtensions.cs ├── FodyWeavers.xml ├── FodyWeavers.xsd ├── ModUpdater │ ├── Cache │ │ ├── GithubModsCacheHandler.cs │ │ ├── IExternalModCacheHandler.cs │ │ ├── NexusModsCacheHandler.cs │ │ └── SteamWorkshopCacheHandler.cs │ └── ModUpdateHandler.cs ├── Models │ ├── App │ │ ├── AppSettings.cs │ │ ├── ConfirmationSettings.cs │ │ ├── DefaultPathwayData.cs │ │ ├── Hotkey.cs │ │ ├── IgnoredModsData.cs │ │ ├── ImportOperationResults.cs │ │ ├── MissingModResults.cs │ │ ├── ModLoadingResults.cs │ │ ├── ModSettingsParseResults.cs │ │ └── MultiReactiveCommandHotkey.cs │ ├── BaseHistoryObject.cs │ ├── Cache │ │ ├── BaseModCacheData.cs │ │ ├── GithubModsCachedData.cs │ │ ├── NexusModsCachedData.cs │ │ └── SteamWorkshopCachedData.cs │ ├── Conflicts │ │ ├── DivinityConflictEntryData.cs │ │ └── DivinityConflictGroup.cs │ ├── DivinityBaseModData.cs │ ├── DivinityGameMasterCampaign.cs │ ├── DivinityLoadOrder.cs │ ├── DivinityMissingModData.cs │ ├── DivinityModData.cs │ ├── DivinityModFilterData.cs │ ├── DivinityModManagerSettings.cs │ ├── DivinityModScriptExtenderConfig.cs │ ├── DivinityModVersion.cs │ ├── DivinityModVersion2.cs │ ├── DivinityModWorkshopCachedData.cs │ ├── DivinityModWorkshopData.cs │ ├── DivinityPathwayData.cs │ ├── DivinityProfileActiveModData.cs │ ├── DivinityProfileData.cs │ ├── DivinitySerializedModData.cs │ ├── Extender │ │ ├── ScriptExtenderSettings.cs │ │ ├── ScriptExtenderUpdateConfig.cs │ │ └── ScriptExtenderUpdateData.cs │ ├── Github │ │ └── GithubModData.cs │ ├── ISelectable.cs │ ├── ModFileDeletionData.cs │ ├── ModuleShortDesc.cs │ ├── NexusMods │ │ ├── NexusModFileVersionData.cs │ │ ├── NexusModsModData.cs │ │ └── NexusModsModDownloadLink.cs │ ├── Steam │ │ ├── IWorkshopPublishFileDetails.cs │ │ ├── PublishedFileDetails.cs │ │ └── QueryFilesResponseData.cs │ ├── Updates │ │ ├── DivinityModUpdateData.cs │ │ └── UpdateResult.cs │ ├── View │ │ └── EnumEntry.cs │ └── WindowSettings.cs ├── Services.cs ├── Usings.cs ├── Util │ ├── DateUtils.cs │ ├── DivinityFileUtils.cs │ ├── DivinityGlobalCommands.cs │ ├── DivinityGlobalEvents.cs │ ├── DivinityJsonUtils.cs │ ├── DivinityModDataLoader.cs │ ├── DivinityModSorter.cs │ ├── DivinityRegistryHelper.cs │ ├── DivinitySaveTools.cs │ ├── DivinityStreamUtils.cs │ ├── GithubHelper.cs │ ├── JunctionPoint.cs │ ├── NativeLibraryHelper.cs │ ├── NexusModsDataLoader.cs │ ├── ProcessHelper.cs │ ├── PropertyConverters.cs │ ├── RecycleBinHelper.cs │ ├── TempFile.cs │ └── WebHelper.cs └── ViewModels │ ├── BaseHistoryViewModel.cs │ ├── BaseViewModel.cs │ └── IDivinityAppViewModel.cs ├── GUI ├── App.xaml ├── App.xaml.cs ├── AppCommands.cs ├── BG3ModManager.ico ├── Controls │ ├── AlertBar.xaml │ ├── AlertBar.xaml.cs │ ├── AutoGrayableImage.cs │ ├── AutoGrid.cs │ ├── AutomationTooltip.cs │ ├── Behavior │ │ ├── GridViewAutosizeColumnsBehavior.cs │ │ ├── ScreenReaderHelperBehavior.cs │ │ ├── TextBlockSettingsEntryAttributeBehavior.cs │ │ └── ToolTipHelperBehavior.cs │ ├── BusyIndicator.xaml │ ├── BusyIndicator.xaml.cs │ ├── CircleDecorator.cs │ ├── Extensions │ │ ├── DependencyExtensions.cs │ │ ├── EnumExtension.cs │ │ └── HyperlinkExtensions.cs │ ├── HotkeyEditorControl.xaml │ ├── HotkeyEditorControl.xaml.cs │ ├── HyperlinkText.xaml │ ├── HyperlinkText.xaml.cs │ ├── Markdown.cs │ ├── ModEntryGrid.cs │ ├── ModListView.cs │ ├── SelectableTextBlock.cs │ ├── TemplateSelectors │ │ └── TagTemplateSelector.cs │ └── UnfocusableTextBox.cs ├── Converters │ ├── BoolToVisibilityConverter.cs │ ├── EnumToStringConverter.cs │ ├── IntToVisibilityConverter.cs │ ├── ModExistsConverter.cs │ ├── ModIsActiveConverter.cs │ ├── ModToDisplayNameConverter.cs │ ├── StringNotEmptyToVisibilityConverter.cs │ ├── StringToLinearBrushConverter.cs │ ├── StringToSolidBrushConverter.cs │ ├── StringToUriConverter.cs │ └── UriToBitmapImageConverter.cs ├── Directory.build.props ├── Extensions │ └── DependencyObjectExtensions.cs ├── FodyWeavers.xml ├── FodyWeavers.xsd ├── GUI.csproj ├── Program.cs ├── Resources │ ├── AppFeatures.json │ ├── BG3MMSplashScreen.png │ ├── DefaultPathways.json │ ├── Icons │ │ ├── AddItem_16x.png │ │ ├── AlertBar_Close.png │ │ ├── AlertBar_Close_Hover.png │ │ ├── AlertBar_Danger_16x.png │ │ ├── AlertBar_Information_16x.png │ │ ├── AlertBar_Success_16x.png │ │ ├── AlertBar_Warning_16x.png │ │ ├── BG3_64x.png │ │ ├── BrowserLink_16x.png │ │ ├── Builder_16x.png │ │ ├── CopyToClipboard_16x.png │ │ ├── DefaultIcon_16x.png │ │ ├── DeleteAzureResource_16x.png │ │ ├── DivinityEngine2_64x.png │ │ ├── DivinityEngineGlasses_64x.png │ │ ├── ExclamationPoint_16x.png │ │ ├── ExpandChevronDown_16x.png │ │ ├── ExpandChevronDown_lightGray_16x.png │ │ ├── ExpandChevronUp_16x.png │ │ ├── ExpandChevronUp_lightGrey_16x.png │ │ ├── ExportData_16x.png │ │ ├── ExportScript_16x.png │ │ ├── FileMissing_16x.png │ │ ├── Folder_16x.png │ │ ├── Github_16x.png │ │ ├── HighPriority_16x.png │ │ ├── Kofi_16x.png │ │ ├── LoadCampaignOrder_16x.png │ │ ├── Log_Normal_32x.png │ │ ├── OpenFile_16x.png │ │ ├── OpenFolder_16x.png │ │ ├── Osiris_16x.png │ │ ├── Osiris_ModFixer_16x.png │ │ ├── Refresh_16x.png │ │ ├── Rename_16x.png │ │ ├── SaveAs_16x.png │ │ ├── SaveGrey_16x.png │ │ ├── Save_16x.png │ │ ├── StatusCriticalError_16x.png │ │ ├── StatusWarning_16x.png │ │ ├── Steam_16x.png │ │ ├── XMLSchemaError_16x.png │ │ ├── ZipFileAs_16x.png │ │ └── ZipFile_16x.png │ └── IgnoredMods.json ├── Themes │ ├── Dark.xaml │ ├── DefaultStyles │ │ ├── ComboBox.xaml │ │ └── MarkdownStyle.xaml │ ├── DivinityColors.cs │ ├── Light.xaml │ ├── MainResourceDictionary.xaml │ └── ResourceAliasHelper.cs ├── Usings.cs ├── Util │ ├── AutomationTooltipPeer.cs │ ├── BindingHelper.cs │ ├── CustomPropertyResolver.cs │ ├── ElementHelper.cs │ ├── FocusHelper.cs │ ├── LogTraceListener.cs │ ├── RxExceptionHandler.cs │ └── ScreenReader │ │ ├── AlertBarAutomationPeer.cs │ │ ├── CachedAutomationPeer.cs │ │ ├── MainWindowAutomationPeer.cs │ │ ├── ModEntryGridAutomationPeer.cs │ │ └── ModListViewAutomationPeer.cs ├── ViewModels │ ├── AppKeys.cs │ ├── AppUpdateWindowViewModel.cs │ ├── BaseProgressViewModel.cs │ ├── DeleteFilesViewData.cs │ ├── ExportOrderToArchiveViewModel.cs │ ├── MainWindowExceptionHandler.cs │ ├── MainWindowViewModel.cs │ ├── ModListDragHandler.cs │ ├── ModListDropHandler.cs │ ├── ModUpdatesViewData.cs │ └── SettingsWindowViewModel.cs ├── Views │ ├── AboutWindow.xaml │ ├── AboutWindow.xaml.cs │ ├── AppUpdateWindow.xaml │ ├── AppUpdateWindow.xaml.cs │ ├── DeleteFilesConfirmationView.xaml │ ├── DeleteFilesConfirmationView.xaml.cs │ ├── ExportOrderToArchiveView.xaml │ ├── ExportOrderToArchiveView.xaml.cs │ ├── HelpWindow.xaml │ ├── HelpWindow.xaml.cs │ ├── HideWindowBase.cs │ ├── HorizontalModLayout.xaml │ ├── HorizontalModLayout.xaml.cs │ ├── LeaderLibSettingsWindow.xaml │ ├── LeaderLibSettingsWindow.xaml.cs │ ├── MainViewControl.xaml │ ├── MainViewControl.xaml.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── ModUpdatesLayout.xaml │ ├── ModUpdatesLayout.xaml.cs │ ├── SettingsWindow.xaml │ ├── SettingsWindow.xaml.cs │ ├── VersionGeneratorWindow.xaml │ ├── VersionGeneratorWindow.xaml.cs │ ├── VerticalModLayout.xaml │ └── VerticalModLayout.xaml.cs └── app.manifest └── Toolbox ├── Args ├── MainArgs.cs └── ScriptExtenderUpdaterArgs.cs ├── Directory.build.props ├── NativeMethods.cs ├── Program.cs ├── ScriptExtender └── Updater.cs └── Toolbox.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | .gitignore merge=ours 2 | AppFeatures.json merge=ours 3 | AssemblyInfo.cs merge=ours 4 | BuildRelease.py merge=ours 5 | DefaultPathways.json merge=ours 6 | DivinityApp.cs merge=ours 7 | DivinityBaseModData.cs merge=ours 8 | DivinityModDataLoader.cs merge=ours 9 | DivinityModManagerSettings.cs merge=ours 10 | DivinityModOsiExtenderConfig.cs merge=ours 11 | GUI.csproj merge=ours 12 | IgnoredMods.json merge=ours 13 | MainWindow.xaml merge=ours 14 | MainWindow.xaml.cs merge=ours 15 | MainWindowViewModel.cs merge=ours 16 | ScriptExtenderSettings.cs merge=ours 17 | SettingsWindow.xaml merge=ours 18 | SettingsWindow.xaml.cs merge=ours 19 | Update.xml merge=ours 20 | VersionGeneratorWindow.xaml merge=ours 21 | packages.config merge=ours -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: input 11 | id: os 12 | attributes: 13 | label: Operating System 14 | description: The operating system you're using (Windows 10 / 11, Linux with Wine, Mac etc). Note that only Windows is officially supported. 15 | placeholder: Windows 10 16 | validations: 17 | required: true 18 | - type: input 19 | id: version 20 | attributes: 21 | label: BG3 Mod Manager Version 22 | description: The version of the mod manager when you encountered this issue (visible on the window title bar). 23 | placeholder: 1.0.9.10 24 | validations: 25 | required: true 26 | - type: input 27 | id: game-version 28 | attributes: 29 | label: BG3 Game Version 30 | description: The version of Baldur's Gate 3 when you encountered this issue. 31 | placeholder: Full Release Patch 3 Hotfix 2 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: what-happened 36 | attributes: 37 | label: Bug Summary 38 | description: Describe what the bug is, and how to reproduce it. 39 | placeholder: Describe the bug here. 40 | value: 41 | validations: 42 | required: true 43 | - type: textarea 44 | id: links 45 | attributes: 46 | label: Links 47 | description: Links to any screenshots, pastes for files etc that are too big to be attached. 48 | placeholder: Debug log https://gist.github.com/MyUserName/SomeLink 49 | value: 50 | validations: 51 | required: false 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Ask a Question 4 | url: https://github.com/LaughingLeader/BG3ModManager/discussions/new?category=q-a 5 | about: Ask a question in the Discussions tab. 6 | - name: Request a Feature 7 | url: https://github.com/LaughingLeader/BG3ModManager/discussions/new?category=feature-requests 8 | about: Request a new feature in the Discussions tab. 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "External/CrossSpeak"] 2 | path = External/CrossSpeak 3 | url = https://github.com/LaughingLeader/CrossSpeak 4 | [submodule "External/lslib"] 5 | path = External/lslib 6 | url = https://github.com/LaughingLeader/lslib 7 | -------------------------------------------------------------------------------- /BuildRelease.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from pathlib import Path 4 | import shutil 5 | import zipfile 6 | from zipfile import ZipFile 7 | 8 | import contextlib 9 | 10 | def get_arg(arg, fallback): 11 | if len(sys.argv) > arg: 12 | val = sys.argv[arg] 13 | if val != None: 14 | return val 15 | return fallback 16 | 17 | script_dir = Path(os.path.dirname(os.path.abspath(__file__))) 18 | os.chdir(script_dir) 19 | 20 | version = get_arg(1, None) 21 | 22 | file_name = f"BG3ModManager_v{version}.zip" 23 | export_file = "BG3ModManager_Latest.zip" 24 | print(f"Writing release zip:{file_name}") 25 | 26 | def zipdir(src, zip_name): 27 | ziph = zipfile.ZipFile(zip_name, 'w') 28 | for root, dirs, files in os.walk(src): 29 | for file in files: 30 | ziph.write(os.path.join(root, file), arcname=os.path.join(root.replace(src, ""), file), compress_type=zipfile.ZIP_DEFLATED) 31 | ziph.close() 32 | 33 | def SilentRemove(f): 34 | try: 35 | if Path(f).is_dir(): 36 | shutil.rmtree(f, ignore_errors=True) 37 | else: 38 | os.remove(f) 39 | print(f"Removed {f}") 40 | except Exception: 41 | pass 42 | 43 | def SilentCopyAndRemove(source, dest): 44 | with contextlib.suppress(FileNotFoundError, PermissionError): 45 | shutil.copy(source, dest) 46 | if Path(source).is_dir(): 47 | shutil.rmtree(source, ignore_errors=True) 48 | else: 49 | os.remove(source) 50 | print(f"Removed {source}") 51 | 52 | import time 53 | time.sleep(3) 54 | 55 | SilentRemove("bin/Publish/Data") 56 | SilentRemove("bin/Publish/Orders") 57 | SilentRemove("bin/Publish/_Logs") 58 | SilentRemove(file_name) 59 | zipdir("bin/Publish", file_name) 60 | shutil.copy(file_name, export_file) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 LaughingLeader 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Screenshots/PreferencesWindow_GameDataPath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/Screenshots/PreferencesWindow_GameDataPath.png -------------------------------------------------------------------------------- /Update.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.0.12.9 4 | https://github.com/LaughingLeader/BG3ModManager/releases/latest/download/BG3ModManager_Latest.zip 5 | https://raw.githubusercontent.com/wiki/LaughingLeader/BG3ModManager/Changelog.md 6 | false 7 | -------------------------------------------------------------------------------- /src/Core/AppServices/ModRegistryService.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Models; 2 | 3 | using DynamicData; 4 | 5 | namespace DivinityModManager 6 | { 7 | public interface IModRegistryService 8 | { 9 | bool TryGetDisplayName(string uuid, out string name); 10 | bool ModExists(string uuid); 11 | bool ModIsActive(string uuid); 12 | } 13 | } 14 | 15 | namespace DivinityModManager.AppServices 16 | { 17 | public class ModRegistryService : IModRegistryService 18 | { 19 | private readonly SourceCache _mods; 20 | 21 | public bool TryGetDisplayName(string uuid, out string name) 22 | { 23 | name = ""; 24 | var mod = _mods.Lookup(uuid); 25 | if (mod.HasValue) 26 | { 27 | name = mod.Value.DisplayName; 28 | return true; 29 | } 30 | return false; 31 | } 32 | 33 | public bool ModExists(string uuid) 34 | { 35 | var mod = _mods.Lookup(uuid); 36 | if (mod.HasValue) 37 | { 38 | return true; 39 | } 40 | return false; 41 | } 42 | 43 | public bool ModIsActive(string uuid) 44 | { 45 | var mod = _mods.Lookup(uuid); 46 | if (mod.HasValue && mod.Value.IsActive) 47 | { 48 | return true; 49 | } 50 | return false; 51 | } 52 | 53 | public ModRegistryService(SourceCache mods) 54 | { 55 | _mods = mods; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Core/AppServices/ScreenReaderService.cs: -------------------------------------------------------------------------------- 1 | using CrossSpeak; 2 | 3 | using DivinityModManager.Util; 4 | 5 | namespace DivinityModManager 6 | { 7 | public interface IScreenReaderService 8 | { 9 | bool IsScreenReaderActive(); 10 | void Output(string text, bool interrupt = false); 11 | void Speak(string text, bool interrupt = false); 12 | void Close(); 13 | void Silence(); 14 | } 15 | } 16 | 17 | namespace DivinityModManager.AppServices 18 | { 19 | public class ScreenReaderService : IScreenReaderService 20 | { 21 | private static readonly string[] _dlls = ["nvdaControllerClient64.dll", "SAAPI64.dll", "Tolk.dll"]; 22 | private static bool _loadedDlls = false; 23 | 24 | public bool IsScreenReaderActive() 25 | { 26 | if (EnsureInit(false)) 27 | { 28 | return !String.IsNullOrWhiteSpace(CrossSpeakManager.Instance.DetectScreenReader()); 29 | } 30 | return false; 31 | } 32 | 33 | public void Close() 34 | { 35 | if (CrossSpeakManager.Instance.IsLoaded()) 36 | { 37 | CrossSpeakManager.Instance.Close(); 38 | } 39 | } 40 | 41 | public void Silence() 42 | { 43 | if (CrossSpeakManager.Instance.IsLoaded()) 44 | { 45 | CrossSpeakManager.Instance.Silence(); 46 | } 47 | } 48 | 49 | private bool EnsureInit(bool trySAPI = false) 50 | { 51 | if (!_loadedDlls) 52 | { 53 | var libPath = Path.Combine(DivinityApp.GetAppDirectory(), "_Lib"); 54 | foreach (var dll in _dlls) 55 | { 56 | var filePath = Path.Combine(libPath, dll); 57 | try 58 | { 59 | if (File.Exists(filePath)) 60 | { 61 | NativeLibraryHelper.LoadLibrary(filePath); 62 | } 63 | } 64 | catch (Exception ex) 65 | { 66 | DivinityApp.Log($"Error loading '{dll}':\n{ex}"); 67 | } 68 | } 69 | _loadedDlls = true; 70 | } 71 | if (!CrossSpeakManager.Instance.IsLoaded()) 72 | { 73 | CrossSpeakManager.Instance.Initialize(); 74 | if (trySAPI && !CrossSpeakManager.Instance.HasSpeech()) 75 | { 76 | CrossSpeakManager.Instance.TrySAPI(true); 77 | } 78 | } 79 | return CrossSpeakManager.Instance.IsLoaded(); 80 | } 81 | 82 | public void Output(string text, bool interrupt = true) 83 | { 84 | if (EnsureInit(true)) 85 | { 86 | CrossSpeakManager.Instance.Output(text, interrupt); 87 | } 88 | } 89 | 90 | public void Speak(string text, bool interrupt = true) 91 | { 92 | if (EnsureInit(true)) 93 | { 94 | CrossSpeakManager.Instance.Output(text, interrupt); 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Core/Attributes/MenuSettingsAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager; 2 | 3 | public class MenuSettingsAttribute : Attribute 4 | { 5 | public string DisplayName { get; set; } 6 | public string Parent { get; set; } 7 | public bool AddSeparator { get; set; } 8 | public string Tooltip { get; set; } 9 | public string Style { get; set; } 10 | public MenuSettingsAttribute(string parent = "", string displayName = "", bool addSeparatorAfter = false, string tooltip = "") 11 | { 12 | DisplayName = displayName; 13 | Parent = parent; 14 | AddSeparator = addSeparatorAfter; 15 | Tooltip = tooltip; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Core/Attributes/ScreenReaderAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager; 2 | 3 | public class ScreenReaderHelperAttribute : Attribute 4 | { 5 | public string Name { get; set; } 6 | public string HelpText { get; set; } 7 | 8 | public ScreenReaderHelperAttribute(string name = "", string helpText = "") 9 | { 10 | Name = name; 11 | HelpText = HelpText; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Core/Attributes/SettingsEntryAttribute.cs: -------------------------------------------------------------------------------- 1 | using DynamicData.Binding; 2 | 3 | using System.Reflection; 4 | 5 | namespace DivinityModManager; 6 | 7 | public class SettingsEntryAttribute : Attribute 8 | { 9 | public string DisplayName { get; set; } 10 | public string Tooltip { get; set; } 11 | public bool IsDebug { get; set; } 12 | public bool HideFromUI { get; set; } 13 | public SettingsEntryAttribute(string displayName = "", string tooltip = "", bool isDebug = false) 14 | { 15 | DisplayName = displayName; 16 | Tooltip = tooltip; 17 | IsDebug = isDebug; 18 | } 19 | } 20 | 21 | public static class SettingsEntryAttributeExtensions 22 | { 23 | public static List GetSettingsAttributes(this ReactiveObject model) 24 | { 25 | var props = model.GetType().GetProperties() 26 | .Select(x => SettingsAttributeProperty.FromProperty(x)) 27 | .Where(x => x.Attribute != null).ToList(); 28 | return props; 29 | } 30 | 31 | public static IObservable WhenAnySettingsChange(this ReactiveObject model) 32 | { 33 | var props = model.GetType().GetProperties() 34 | .Select(x => SettingsAttributeProperty.FromProperty(x)) 35 | .Where(x => x.Attribute != null).Select(x => x.Property.Name).ToArray(); 36 | return model.WhenAnyPropertyChanged(props); 37 | } 38 | } 39 | 40 | public struct SettingsAttributeProperty 41 | { 42 | public PropertyInfo Property { get; set; } 43 | public SettingsEntryAttribute Attribute { get; set; } 44 | 45 | public static SettingsAttributeProperty FromProperty(PropertyInfo property) 46 | { 47 | return new SettingsAttributeProperty 48 | { 49 | Property = property, 50 | Attribute = property.GetCustomAttribute() 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Core/Directory.build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(SolutionDir)obj\Core\ 4 | 5 | -------------------------------------------------------------------------------- /src/Core/DivinityInteractions.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager; 2 | 3 | public struct DeleteFilesViewConfirmationData 4 | { 5 | public int Total; 6 | public bool PermanentlyDelete; 7 | public CancellationToken Token; 8 | } 9 | 10 | public static class DivinityInteractions 11 | { 12 | public static readonly Interaction ConfirmModDeletion = new(); 13 | } 14 | -------------------------------------------------------------------------------- /src/Core/DivinityModManagerCore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0-windows 4 | true 5 | true 6 | DivinityModManager 7 | x64 8 | x64 9 | Debug;Release;Publish;PublishTest 10 | false 11 | en-US 12 | none 13 | True 14 | 15 | 16 | full 17 | False 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Core/Enums/AlertType.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager; 2 | 3 | public enum AlertType 4 | { 5 | Info, 6 | Success, 7 | Warning, 8 | Danger 9 | } 10 | -------------------------------------------------------------------------------- /src/Core/Enums/DivinityExtenderModStatus.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager; 2 | 3 | [Flags] 4 | public enum DivinityExtenderModStatus 5 | { 6 | None, 7 | Supports, 8 | Fulfilled, 9 | DisabledFromConfig, 10 | MissingRequiredVersion, 11 | MissingAppData, 12 | MissingUpdater, 13 | } 14 | -------------------------------------------------------------------------------- /src/Core/Enums/DivinityGameLaunchWindowAction.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Converters; 2 | 3 | using System.ComponentModel; 4 | 5 | namespace DivinityModManager; 6 | 7 | [JsonConverter(typeof(StringEnumConverter))] 8 | public enum DivinityGameLaunchWindowAction 9 | { 10 | [Description("None")] 11 | None, 12 | [Description("Minimize")] 13 | Minimize, 14 | [Description("Close")] 15 | Close 16 | } 17 | -------------------------------------------------------------------------------- /src/Core/Enums/DivinityOsirisModStatus.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager; 2 | 3 | public enum DivinityOsirisModStatus 4 | { 5 | NONE, 6 | SCRIPTS, 7 | MODFIXER 8 | } 9 | -------------------------------------------------------------------------------- /src/Core/Enums/Extender/ExtenderUpdateChannel.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Converters; 2 | 3 | using System.ComponentModel; 4 | 5 | namespace DivinityModManager.Enums.Extender; 6 | 7 | [JsonConverter(typeof(StringEnumConverter))] 8 | public enum ExtenderUpdateChannel 9 | { 10 | [Description("Release")] 11 | Release, 12 | [Description("Devel")] 13 | Devel, 14 | [Description("Nightly")] 15 | Nightly 16 | } 17 | -------------------------------------------------------------------------------- /src/Core/Enums/LaunchGameType.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace DivinityModManager; 4 | public enum LaunchGameType 5 | { 6 | [Display(Name = "Default (Exe)", Description = "Open the game exe directly,\nand create a steam_appid.txt in the bin folder if it doesn't exist,\nallowing you to bypassing the launcher")] 7 | Exe, 8 | [Display(Name = "Steam", Description = "Open the game by running the Steam launch protocol ('steam://run/1086940')")] 9 | Steam, 10 | [Display(Name = "Custom", Description = "Open the game by opening a different file or protocol (ex. a batch file or protocol handler like playnite://playnite/start/id)")] 11 | Custom 12 | } -------------------------------------------------------------------------------- /src/Core/Enums/ModSourceType.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace DivinityModManager; 4 | 5 | public enum ModSourceType 6 | { 7 | [Description("None")] 8 | NONE, 9 | [Description("Steam Workshop")] 10 | STEAM, 11 | [Description("Nexus Mods")] 12 | NEXUSMODS, 13 | [Description("Github")] 14 | GITHUB 15 | } 16 | -------------------------------------------------------------------------------- /src/Core/Enums/ScriptExtenderIconType.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager; 2 | public enum ScriptExtenderIconType 3 | { 4 | None, 5 | Missing, 6 | Warning, 7 | FulfilledSupports, 8 | FulfilledRequired 9 | } 10 | -------------------------------------------------------------------------------- /src/Core/Enums/Steam/EPublishedFileQueryType.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Enums.Steam; 2 | 3 | public enum EPublishedFileQueryType 4 | { 5 | RankedByVote = 0, 6 | RankedByPublicationDate = 1, 7 | AcceptedForGameRankedByAcceptanceDate = 2, 8 | RankedByTrend = 3, 9 | FavoritedByFriendsRankedByPublicationDate = 4, 10 | CreatedByFriendsRankedByPublicationDate = 5, 11 | RankedByNumTimesReported = 6, 12 | CreatedByFollowedUsersRankedByPublicationDate = 7, 13 | NotYetRated = 8, 14 | RankedByTotalUniqueSubscriptions = 9, 15 | RankedByTotalVotesAsc = 10, 16 | RankedByVotesUp = 11, 17 | RankedByTextSearch = 12, 18 | RankedByPlaytimeTrend = 13, 19 | RankedByTotalPlaytime = 14, 20 | RankedByAveragePlaytimeTrend = 15, 21 | RankedByLifetimeAveragePlaytime = 16, 22 | RankedByPlaytimeSessionsTrend = 17, 23 | RankedByLifetimePlaytimeSessions = 18, 24 | RankedByInappropriateContentRating = 19, 25 | } -------------------------------------------------------------------------------- /src/Core/Extensions/DictionaryExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Extensions; 2 | 3 | public static class DictionaryExtensions 4 | { 5 | public static object FindKeyValue(this Dictionary dict, string key, StringComparison stringComparison = StringComparison.OrdinalIgnoreCase) 6 | { 7 | foreach (KeyValuePair kvp in dict) 8 | { 9 | if (kvp.Key.Equals(key, stringComparison)) 10 | { 11 | return kvp.Value; 12 | } 13 | else if (kvp.Value.GetType() == typeof(Dictionary)) 14 | { 15 | var subDict = (Dictionary)kvp.Value; 16 | var val = subDict.FindKeyValue(key, stringComparison); 17 | if (val != null) return val; 18 | } 19 | } 20 | return null; 21 | } 22 | 23 | private static object FindKeyValue_Recursive(object obj, string key, StringComparison stringComparison = StringComparison.OrdinalIgnoreCase) 24 | { 25 | if (obj.GetType() == typeof(Dictionary)) 26 | { 27 | var subDict = (Dictionary)obj; 28 | var val = subDict.FindKeyValue(key, stringComparison); 29 | if (val != null) return val; 30 | } 31 | else if (obj is IList list) 32 | { 33 | foreach (var childobj in list) 34 | { 35 | var val = FindKeyValue_Recursive(childobj, key, stringComparison); 36 | if (val != null) return val; 37 | } 38 | } 39 | else if (obj is IEnumerable enumerable) 40 | { 41 | foreach (var childobj in enumerable) 42 | { 43 | var val = FindKeyValue_Recursive(childobj, key, stringComparison); 44 | if (val != null) return val; 45 | } 46 | } 47 | return null; 48 | } 49 | 50 | public static bool TryFindKeyValue(this Dictionary dict, string key, out object valObj, StringComparison stringComparison = StringComparison.OrdinalIgnoreCase) 51 | { 52 | foreach (KeyValuePair kvp in dict) 53 | { 54 | if (kvp.Key.Equals(key, stringComparison)) 55 | { 56 | valObj = kvp.Value; 57 | return true; 58 | } 59 | else 60 | { 61 | var val = FindKeyValue_Recursive(kvp.Value, key, stringComparison); 62 | if (val != null) 63 | { 64 | valObj = val; 65 | return true; 66 | } 67 | } 68 | } 69 | valObj = null; 70 | return false; 71 | } 72 | 73 | /// 74 | /// ToDictionary that allows duplicate key entries. 75 | /// Source: https://stackoverflow.com/a/22508992/2290477 76 | /// 77 | /// 78 | /// 79 | /// 80 | /// 81 | /// 82 | /// 83 | /// 84 | /// 85 | public static Dictionary SafeToDictionary( 86 | this IEnumerable source, 87 | Func keySelector, 88 | Func elementSelector, 89 | IEqualityComparer comparer = null) 90 | { 91 | var dictionary = new Dictionary(comparer); 92 | 93 | if (source == null) 94 | { 95 | return dictionary; 96 | } 97 | 98 | foreach (TSource element in source) 99 | { 100 | dictionary[keySelector(element)] = elementSelector(element); 101 | } 102 | 103 | return dictionary; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Core/Extensions/EnumExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Reflection; 4 | 5 | namespace DivinityModManager; 6 | 7 | public static class EnumExtensions 8 | { 9 | /// 10 | /// Get an enum's Description attribute value. 11 | /// 12 | public static string GetDescription(this Enum enumValue) 13 | { 14 | var member = enumValue.GetType().GetMember(enumValue.ToString()).FirstOrDefault(); 15 | if (member != null) 16 | { 17 | var descriptionAttribute = member.GetCustomAttribute(false); 18 | 19 | if (descriptionAttribute == null) 20 | { 21 | var displayAttribute = member.GetCustomAttribute(false); 22 | if (displayAttribute != null) 23 | { 24 | return displayAttribute.Name; 25 | } 26 | } 27 | else 28 | { 29 | return descriptionAttribute.Description; 30 | } 31 | return enumValue.ToString(); 32 | } 33 | return ""; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Core/Extensions/KeyExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Input; 2 | 3 | namespace DivinityModManager; 4 | 5 | public static class KeyExtensions 6 | { 7 | private static readonly Dictionary KeyToName = new() 8 | { 9 | {Key.Add, "+"}, 10 | {Key.D0, "0"}, 11 | {Key.D1, "1"}, 12 | {Key.D2, "2"}, 13 | {Key.D3, "3"}, 14 | {Key.D4, "4"}, 15 | {Key.D5, "5"}, 16 | {Key.D6, "6"}, 17 | {Key.D7, "7"}, 18 | {Key.D8, "8"}, 19 | {Key.D9, "9"}, 20 | {Key.Decimal, "."}, 21 | {Key.Divide, " / "}, 22 | {Key.Multiply, "*"}, 23 | {Key.Oem1, ";"}, 24 | {Key.Oem5, "\\"}, 25 | {Key.Oem6, "]"}, 26 | {Key.Oem7, "'"}, 27 | {Key.OemBackslash, "\\"}, 28 | {Key.OemComma, ","}, 29 | {Key.OemMinus, "-"}, 30 | {Key.OemOpenBrackets, "["}, 31 | {Key.OemPeriod, "."}, 32 | {Key.OemPlus, "="}, 33 | {Key.OemQuestion, "/"}, 34 | {Key.OemTilde, "`"}, 35 | {Key.Subtract, "-"} 36 | }; 37 | 38 | public static string GetKeyName(this Key key) 39 | { 40 | if (KeyToName.TryGetValue(key, out string name)) 41 | { 42 | return name; 43 | } 44 | return key.ToString(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Core/Extensions/ModelExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace DivinityModManager.Extensions; 4 | 5 | public static class ModelExtensions 6 | { 7 | public static void SetToDefault(this ReactiveObject model) 8 | { 9 | /*PropertyInfo[] props = model.GetType().GetProperties(); 10 | foreach (PropertyInfo prop in props) 11 | { 12 | var d = prop.GetCustomAttribute(); 13 | if (d != null && prop.GetValue(model) != d.Value) 14 | { 15 | prop.SetValue(model, d.Value); 16 | } 17 | }*/ 18 | var props = TypeDescriptor.GetProperties(model.GetType()); 19 | foreach (PropertyDescriptor pr in props) 20 | { 21 | if (pr.CanResetValue(model)) 22 | { 23 | pr.ResetValue(model); 24 | } 25 | } 26 | } 27 | 28 | public static void SetFrom(this T target, T from) where T : ReactiveObject 29 | { 30 | var props = TypeDescriptor.GetProperties(target.GetType()); 31 | foreach (PropertyDescriptor pr in props) 32 | { 33 | var value = pr.GetValue(from); 34 | if (value != null) 35 | { 36 | pr.SetValue(target, value); 37 | target.RaisePropertyChanged(pr.Name); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Core/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Extensions; 2 | 3 | public static class StringExtensions 4 | { 5 | public static bool IsExistingDirectory(this string path) 6 | { 7 | return !String.IsNullOrWhiteSpace(path) && Directory.Exists(path); 8 | } 9 | 10 | public static bool IsExistingFile(this string path) 11 | { 12 | return !String.IsNullOrWhiteSpace(path) && File.Exists(path); 13 | } 14 | 15 | /* 16 | * MaybeAddReplacement("%LOCALAPPDATA%", Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)); 17 | MaybeAddReplacement("%APPDATA%", Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); 18 | MaybeAddReplacement("%USERPROFILE%", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); 19 | * */ 20 | 21 | private static Dictionary _specialPaths = new Dictionary() 22 | { 23 | { "%LOCALAPPDATA%", Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) }, 24 | { "%APPDATA%", Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) }, 25 | { "%USERPROFILE%", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) }, 26 | }; 27 | 28 | public static string ReplaceSpecialPaths(this string path) 29 | { 30 | if (string.IsNullOrEmpty(path)) return path; 31 | 32 | foreach (var kvp in _specialPaths) 33 | { 34 | if(!string.IsNullOrEmpty(kvp.Value)) 35 | { 36 | path = path.Replace(kvp.Value, kvp.Key); 37 | } 38 | } 39 | return path; 40 | } 41 | 42 | /// 43 | /// Checks File.Exists after expanding environment variables. 44 | /// 45 | public static bool FileExists(this string path) 46 | { 47 | if (string.IsNullOrEmpty(path)) return false; 48 | 49 | return File.Exists(Environment.ExpandEnvironmentVariables(path)); 50 | } 51 | 52 | /// 53 | /// Checks Directory.Exists after expanding environment variables. 54 | /// 55 | public static bool DirectoryExists(this string path) 56 | { 57 | if (string.IsNullOrEmpty(path)) return false; 58 | 59 | return Directory.Exists(Environment.ExpandEnvironmentVariables(path)); 60 | } 61 | 62 | /// 63 | /// Expands environment variables and makes the path relative to the app directory if not rooted. 64 | /// 65 | public static string ToRealPath(this string path) 66 | { 67 | if (string.IsNullOrEmpty(path)) return path; 68 | 69 | var finalPath = Environment.ExpandEnvironmentVariables(path); 70 | if(!Path.IsPathRooted(finalPath)) 71 | { 72 | finalPath = DivinityApp.GetAppDirectory(finalPath); 73 | } 74 | return finalPath; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Core/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /src/Core/FodyWeavers.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. 12 | 13 | 14 | 15 | 16 | A comma-separated list of error codes that can be safely ignored in assembly verification. 17 | 18 | 19 | 20 | 21 | 'false' to turn off automatic generation of the XML Schema file. 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Core/ModUpdater/Cache/GithubModsCacheHandler.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Models; 2 | using DivinityModManager.Models.Cache; 3 | 4 | using Newtonsoft.Json; 5 | 6 | namespace DivinityModManager.ModUpdater.Cache; 7 | 8 | public class GithubModsCacheHandler : IExternalModCacheHandler 9 | { 10 | public ModSourceType SourceType => ModSourceType.GITHUB; 11 | public string FileName => "githubdata.json"; 12 | 13 | //Format Github data so people can more easily edit/add mods manually. 14 | public JsonSerializerSettings SerializerSettings => new() 15 | { 16 | NullValueHandling = NullValueHandling.Ignore, 17 | Formatting = Formatting.Indented, 18 | }; 19 | 20 | public bool IsEnabled { get; set; } 21 | public GithubModsCachedData CacheData { get; set; } 22 | 23 | public GithubModsCacheHandler() : base() 24 | { 25 | CacheData = new GithubModsCachedData(); 26 | } 27 | 28 | public async Task Update(IEnumerable mods, CancellationToken cts) 29 | { 30 | return false; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Core/ModUpdater/Cache/IExternalModCacheHandler.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using DivinityModManager.Models; 4 | using DivinityModManager.Models.Cache; 5 | using DivinityModManager.Util; 6 | 7 | using Newtonsoft.Json; 8 | 9 | using System.Text; 10 | 11 | namespace DivinityModManager.ModUpdater.Cache; 12 | 13 | public interface IExternalModCacheHandler where T : IModCacheData 14 | { 15 | ModSourceType SourceType { get; } 16 | string FileName { get; } 17 | JsonSerializerSettings SerializerSettings { get; } 18 | 19 | bool IsEnabled { get; set; } 20 | T CacheData { get; set; } 21 | Task Update(IEnumerable mods, CancellationToken cts); 22 | } 23 | 24 | public static class IExternalModCacheDataExtensions 25 | { 26 | public static async Task LoadCacheAsync(this IExternalModCacheHandler cacheData, string currentAppVersion, CancellationToken cts) where T : IModCacheData 27 | { 28 | var filePath = DivinityApp.GetAppDirectory("Data", cacheData.FileName); 29 | 30 | if (File.Exists(filePath)) 31 | { 32 | var cachedData = await DivinityJsonUtils.DeserializeFromPathAsync(filePath, cts); 33 | if (cachedData != null) 34 | { 35 | if (string.IsNullOrEmpty(cachedData.LastVersion) || cachedData.LastVersion != currentAppVersion) 36 | { 37 | cachedData.LastUpdated = -1; 38 | } 39 | cachedData.CacheUpdated = true; 40 | return cachedData; 41 | } 42 | } 43 | return default; 44 | } 45 | 46 | public static async Task SaveCacheAsync(this IExternalModCacheHandler handler, bool updateLastTimestamp, string currentAppVersion, CancellationToken cts) where T : IModCacheData 47 | { 48 | try 49 | { 50 | var parentDir = DivinityApp.GetAppDirectory("Data"); 51 | var filePath = Path.Combine(parentDir, handler.FileName); 52 | if (!Directory.Exists(parentDir)) Directory.CreateDirectory(parentDir); 53 | 54 | if (updateLastTimestamp) 55 | { 56 | handler.CacheData.LastUpdated = DateTimeOffset.Now.ToUnixTimeSeconds(); 57 | } 58 | handler.CacheData.LastVersion = currentAppVersion; 59 | 60 | string contents = JsonConvert.SerializeObject(handler.CacheData, handler.SerializerSettings); 61 | 62 | var buffer = Encoding.UTF8.GetBytes(contents); 63 | using var fs = new System.IO.FileStream(filePath, System.IO.FileMode.Create, 64 | System.IO.FileAccess.Write, System.IO.FileShare.None, buffer.Length, true); 65 | await fs.WriteAsync(buffer, 0, buffer.Length, cts); 66 | 67 | return true; 68 | } 69 | catch (Exception ex) 70 | { 71 | DivinityApp.Log($"Error saving cache:\n{ex}"); 72 | } 73 | return false; 74 | } 75 | 76 | public static bool DeleteCache(this IExternalModCacheHandler handler, bool permanent = false) where T : IModCacheData 77 | { 78 | try 79 | { 80 | var parentDir = DivinityApp.GetAppDirectory("Data"); 81 | var filePath = Path.Combine(parentDir, handler.FileName); 82 | if (File.Exists(filePath)) 83 | { 84 | RecycleBinHelper.DeleteFile(filePath, false, permanent); 85 | return true; 86 | } 87 | } 88 | catch (Exception ex) 89 | { 90 | DivinityApp.Log($"Error saving cache:\n{ex}"); 91 | } 92 | return false; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Core/ModUpdater/Cache/NexusModsCacheHandler.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Models; 2 | using DivinityModManager.Models.Cache; 3 | using DivinityModManager.Util; 4 | 5 | using Newtonsoft.Json; 6 | 7 | namespace DivinityModManager.ModUpdater.Cache; 8 | 9 | public class NexusModsCacheHandler : IExternalModCacheHandler 10 | { 11 | public ModSourceType SourceType => ModSourceType.NEXUSMODS; 12 | public string FileName => "nexusmodsdata.json"; 13 | public JsonSerializerSettings SerializerSettings => ModUpdateHandler.DefaultSerializerSettings; 14 | public bool IsEnabled { get; set; } = false; 15 | public NexusModsCachedData CacheData { get; set; } 16 | 17 | public string APIKey { get; set; } 18 | public string AppName { get; set; } 19 | public string AppVersion { get; set; } 20 | 21 | public NexusModsCacheHandler() : base() 22 | { 23 | CacheData = new NexusModsCachedData(); 24 | } 25 | 26 | public async Task Update(IEnumerable mods, CancellationToken cts) 27 | { 28 | if (!NexusModsDataLoader.IsInitialized && !string.IsNullOrEmpty(APIKey)) 29 | { 30 | NexusModsDataLoader.Init(APIKey, AppName, AppVersion); 31 | } 32 | 33 | if (NexusModsDataLoader.CanFetchData) 34 | { 35 | var result = await NexusModsDataLoader.LoadAllModsDataAsync(mods, cts); 36 | 37 | if (result.Success) 38 | { 39 | DivinityApp.Log($"Fetched NexusMods mod info for {result.UpdatedMods.Count} mod(s)."); 40 | 41 | foreach (var mod in mods.Where(x => x.NexusModsData.ModId >= DivinityApp.NEXUSMODS_MOD_ID_START).Select(x => x.NexusModsData)) 42 | { 43 | CacheData.Mods[mod.UUID] = mod; 44 | } 45 | 46 | return true; 47 | } 48 | else 49 | { 50 | DivinityApp.Log($"Failed to update NexusMods mod info:\n{result.FailureMessage}"); 51 | } 52 | } 53 | else 54 | { 55 | DivinityApp.Log("NexusModsAPIKey not set, or daily/hourly limit reached. Skipping."); 56 | } 57 | return false; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Core/ModUpdater/Cache/SteamWorkshopCacheHandler.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Models; 2 | using DivinityModManager.Models.Cache; 3 | using DivinityModManager.Util; 4 | 5 | using Newtonsoft.Json; 6 | 7 | namespace DivinityModManager.ModUpdater.Cache; 8 | 9 | public class SteamWorkshopCacheHandler : IExternalModCacheHandler 10 | { 11 | public ModSourceType SourceType => ModSourceType.STEAM; 12 | public string FileName => "workshopdata.json"; 13 | public JsonSerializerSettings SerializerSettings => ModUpdateHandler.DefaultSerializerSettings; 14 | public SteamWorkshopCachedData CacheData { get; set; } 15 | public bool IsEnabled { get; set; } = false; 16 | 17 | public string SteamAppID { get; set; } 18 | 19 | public SteamWorkshopCacheHandler() : base() 20 | { 21 | CacheData = new SteamWorkshopCachedData(); 22 | } 23 | 24 | public async Task Update(IEnumerable mods, CancellationToken cts) 25 | { 26 | return false; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Core/Models/App/AppSettings.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Models.App; 2 | 3 | public class AppSettings 4 | { 5 | public DefaultPathwayData DefaultPathways { get; set; } = new DefaultPathwayData(); 6 | 7 | public Dictionary Features { get; set; } = new Dictionary(); 8 | 9 | public bool FeatureEnabled(string id) 10 | { 11 | if (Features.TryGetValue(id.ToLower(), out bool v)) 12 | { 13 | return v == true; 14 | } 15 | return false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Core/Models/App/ConfirmationSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace DivinityModManager.Models.App; 9 | 10 | [DataContract] 11 | public class ConfirmationSettings : ReactiveObject 12 | { 13 | [Reactive, DataMember] public bool DisableAdminModeWarning { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Core/Models/App/DefaultPathwayData.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Models.App; 2 | 3 | public class AppPathwayData 4 | { 5 | public string Registry_32 { get; set; } 6 | public string Registry_64 { get; set; } 7 | public string AppID { get; set; } 8 | public string RootFolderName { get; set; } 9 | public string ExePath { get; set; } 10 | } 11 | public class DefaultPathwayData 12 | { 13 | public AppPathwayData Steam { get; set; } = new AppPathwayData(); 14 | public AppPathwayData GOG { get; set; } = new AppPathwayData(); 15 | 16 | public string DocumentsGameFolder { get; set; } 17 | public string GameDataFolder { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /src/Core/Models/App/IgnoredModsData.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Models; 2 | 3 | public class IgnoredModsData 4 | { 5 | public List IgnoreDependencies { get; set; } = new List(); 6 | public List> Mods { get; set; } = new List>(); 7 | public List IgnoreBuiltinPath { get; set; } = new List(); 8 | } 9 | -------------------------------------------------------------------------------- /src/Core/Models/App/ImportOperationResults.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Models.App; 2 | 3 | public struct ImportOperationError 4 | { 5 | public Exception Exception { get; set; } 6 | public string File { get; set; } 7 | } 8 | 9 | public class ImportOperationResults 10 | { 11 | public bool Success => Mods.Count >= TotalPaks; 12 | public int TotalFiles { get; set; } 13 | public int TotalPaks { get; set; } 14 | public List Mods { get; set; } = new List(); 15 | public List Orders { get; set; } = new List(); 16 | public List Errors { get; set; } = new List(); 17 | 18 | public void AddError(string path, Exception ex) => Errors.Add(new ImportOperationError { Exception = ex, File = path }); 19 | } 20 | -------------------------------------------------------------------------------- /src/Core/Models/App/ModLoadingResults.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Models.App; 2 | 3 | public class ModLoadingResults 4 | { 5 | public string DirectoryPath { get; set; } 6 | public List Mods { get; set; } 7 | public List Duplicates { get; set; } 8 | 9 | public ModLoadingResults() 10 | { 11 | Mods = new List(); 12 | Duplicates = new List(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Core/Models/App/ModSettingsParseResults.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Models.App; 2 | 3 | public class ModSettingsParseResults 4 | { 5 | public List ActiveMods { get; set; } 6 | 7 | public int CountActive(bool includeIgnoredMods = false) 8 | { 9 | var i = 0; 10 | foreach (var mod in ActiveMods) 11 | { 12 | if (includeIgnoredMods || !DivinityApp.IgnoredMods.Lookup( mod.UUID).HasValue) 13 | { 14 | i++; 15 | } 16 | } 17 | return i; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Core/Models/App/MultiReactiveCommandHotkey.cs: -------------------------------------------------------------------------------- 1 | using DynamicData; 2 | using DynamicData.Binding; 3 | 4 | using System.Text; 5 | using System.Windows.Input; 6 | 7 | namespace DivinityModManager.Models.App; 8 | 9 | public class MultiReactiveCommandHotkey : ReactiveObject, IHotkey 10 | { 11 | [Reactive] public string DisplayName { get; set; } 12 | [Reactive] public Key Key { get; set; } 13 | 14 | [Reactive] public ModifierKeys Modifiers { get; set; } 15 | 16 | [Reactive] public ICommand Command { get; set; } 17 | 18 | [Reactive] public bool Enabled { get; set; } 19 | 20 | [Reactive] public IObservable CanExecute { get; private set; } 21 | 22 | private ObservableCollection commands = new(); 23 | 24 | public ObservableCollection Commands 25 | { 26 | get => commands; 27 | } 28 | 29 | public void Add(RxCommandUnit command) 30 | { 31 | Commands.Add(command); 32 | } 33 | 34 | public void Invoke() 35 | { 36 | foreach (var cmd in Commands) 37 | { 38 | Observable.Start(() => { }).InvokeCommand(cmd); 39 | } 40 | } 41 | 42 | private void Init(Key key, ModifierKeys modifiers) 43 | { 44 | Key = key; 45 | Modifiers = modifiers; 46 | 47 | var canExecuteInitial = this.WhenAnyValue(x => x.Enabled, (b) => b == true); 48 | var anyCommandsCanExecute = Commands.ToObservableChangeSet().AutoRefreshOnObservable(c => c.CanExecute).ToCollection().Select(x => x.Any()); 49 | CanExecute = Observable.Merge(new IObservable[2] { canExecuteInitial, anyCommandsCanExecute }); 50 | Command = ReactiveCommand.Create(Invoke, CanExecute); 51 | } 52 | 53 | 54 | public MultiReactiveCommandHotkey(Key key) 55 | { 56 | Init(key, ModifierKeys.None); 57 | } 58 | 59 | public MultiReactiveCommandHotkey(Key key, ModifierKeys modifiers) 60 | { 61 | Init(key, modifiers); 62 | } 63 | 64 | public override string ToString() 65 | { 66 | var str = new StringBuilder(); 67 | 68 | if (Modifiers.HasFlag(ModifierKeys.Control)) 69 | str.Append("Ctrl + "); 70 | if (Modifiers.HasFlag(ModifierKeys.Shift)) 71 | str.Append("Shift + "); 72 | if (Modifiers.HasFlag(ModifierKeys.Alt)) 73 | str.Append("Alt + "); 74 | if (Modifiers.HasFlag(ModifierKeys.Windows)) 75 | str.Append("Win + "); 76 | 77 | str.Append(Key); 78 | 79 | return str.ToString(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Core/Models/BaseHistoryObject.cs: -------------------------------------------------------------------------------- 1 | using ReactiveHistory; 2 | 3 | using System.Reflection; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace DivinityModManager.Models; 7 | 8 | public class BaseHistoryObject : ReactiveObject 9 | { 10 | public IHistory History { get; set; } 11 | 12 | public virtual void Snapshot(Action undo, Action redo) 13 | { 14 | History.Snapshot(undo, redo); 15 | } 16 | 17 | public bool UpdateWithHistory(ref T field, T value, [CallerMemberName] string propertyName = null) 18 | { 19 | if (!Equals(field, value)) 20 | { 21 | if (History != null) 22 | { 23 | var undoValue = field; 24 | var redoValue = value; 25 | 26 | History.Snapshot(() => 27 | { 28 | this.SetProperty(this, propertyName, undoValue); 29 | }, () => 30 | { 31 | this.SetProperty(this, propertyName, redoValue); 32 | }); 33 | } 34 | 35 | this.RaiseAndSetIfChanged(ref field, value, propertyName); 36 | return true; 37 | } 38 | return false; 39 | } 40 | 41 | public bool UpdateWithHistory(ref T field, T value, Action undo, Action redo, [CallerMemberName] string propertyName = null) 42 | { 43 | if (!Equals(field, value)) 44 | { 45 | if (History != null) 46 | { 47 | History.Snapshot(undo, redo); 48 | } 49 | 50 | this.RaiseAndSetIfChanged(ref field, value, propertyName); 51 | return true; 52 | } 53 | return false; 54 | } 55 | 56 | private bool SetProperty(object targetObject, string propertyName, T value) 57 | { 58 | var prop = this.GetType().GetProperty(propertyName, BindingFlags.Public | BindingFlags.SetProperty | BindingFlags.Instance); 59 | if (prop != null && prop.CanWrite) 60 | { 61 | prop.SetValue(this, value); 62 | return true; 63 | } 64 | return false; 65 | } 66 | 67 | private bool SetField(string fieldName, T value, string propertyName = null) 68 | { 69 | var field = this.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); 70 | if (field != null) 71 | { 72 | field.SetValue(this, value); 73 | return true; 74 | } 75 | return false; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Core/Models/Cache/BaseModCacheData.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace DivinityModManager.Models.Cache; 4 | 5 | public interface IModCacheData 6 | { 7 | long LastUpdated { get; set; } 8 | string LastVersion { get; set; } 9 | bool CacheUpdated { get; set; } 10 | } 11 | 12 | [DataContract] 13 | public class BaseModCacheData : IModCacheData 14 | { 15 | [DataMember] public long LastUpdated { get; set; } 16 | [DataMember] public string LastVersion { get; set; } 17 | [DataMember] public Dictionary Mods { get; set; } = new Dictionary(); 18 | 19 | public bool CacheUpdated { get; set; } 20 | 21 | public BaseModCacheData() 22 | { 23 | LastUpdated = -1; 24 | LastVersion = ""; 25 | 26 | Mods = new Dictionary(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Core/Models/Cache/GithubModsCachedData.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Models.Github; 2 | 3 | namespace DivinityModManager.Models.Cache; 4 | 5 | public class GithubModsCachedData : BaseModCacheData 6 | { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/Core/Models/Cache/NexusModsCachedData.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Models.NexusMods; 2 | 3 | namespace DivinityModManager.Models.Cache; 4 | 5 | public class NexusModsCachedData : BaseModCacheData 6 | { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/Core/Models/Conflicts/DivinityConflictEntryData.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Models.Conflicts; 2 | 3 | public class DivinityConflictEntryData : ReactiveObject 4 | { 5 | private string target; 6 | 7 | public string Target 8 | { 9 | get => target; 10 | set { this.RaiseAndSetIfChanged(ref target, value); } 11 | } 12 | 13 | private string name; 14 | 15 | public string Name 16 | { 17 | get => name; 18 | set { this.RaiseAndSetIfChanged(ref name, value); } 19 | } 20 | 21 | public List ConflictModDataList { get; set; } = new List(); 22 | } 23 | 24 | public class DivinityConflictModData : ReactiveObject 25 | { 26 | private readonly DivinityModData modData; 27 | public DivinityModData Mod => modData; 28 | 29 | public string Value { get; set; } 30 | 31 | public DivinityConflictModData(DivinityModData mod, string val = "") 32 | { 33 | modData = mod; 34 | Value = val; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Core/Models/Conflicts/DivinityConflictGroup.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Models.Conflicts; 2 | 3 | public class DivinityConflictGroup : ReactiveObject 4 | { 5 | private string header; 6 | 7 | public string Header 8 | { 9 | get => header; 10 | set { this.RaiseAndSetIfChanged(ref header, value); } 11 | } 12 | 13 | private int totalConflicts = 0; 14 | 15 | public int TotalConflicts 16 | { 17 | get => totalConflicts; 18 | set { this.RaiseAndSetIfChanged(ref totalConflicts, value); } 19 | } 20 | 21 | public List Conflicts { get; set; } = new List(); 22 | 23 | private int selectedConflictIndex = 0; 24 | 25 | public int SelectedConflictIndex 26 | { 27 | get => selectedConflictIndex; 28 | set { this.RaiseAndSetIfChanged(ref selectedConflictIndex, value); } 29 | } 30 | 31 | public void OnActivated(CompositeDisposable disposables) 32 | { 33 | this.WhenAnyValue(x => x.Conflicts.Count).Subscribe(c => TotalConflicts = c).DisposeWith(disposables); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Core/Models/DivinityGameMasterCampaign.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using DivinityModManager.Extensions; 4 | 5 | using LSLib.LS; 6 | 7 | namespace DivinityModManager.Models; 8 | 9 | [ScreenReaderHelper(Name = "DisplayName", HelpText = "HelpText")] 10 | public class DivinityGameMasterCampaign : DivinityBaseModData 11 | { 12 | public Resource MetaResource { get; set; } 13 | 14 | public List Dependencies = new(); 15 | 16 | public bool Export(IEnumerable order) 17 | { 18 | try 19 | { 20 | var conversionParams = ResourceConversionParameters.FromGameVersion(DivinityApp.GAME); 21 | if (File.Exists(FilePath) && new FileInfo(FilePath).Length > 0) 22 | { 23 | var backupName = Path.Combine(Path.GetDirectoryName(FilePath), FileName + ".backup"); 24 | File.Copy(FilePath, backupName, true); 25 | } 26 | 27 | if (MetaResource.TryFindNode("Dependencies", out var dependenciesNode)) 28 | { 29 | if (dependenciesNode.Children.TryGetValue("ModuleShortDesc", out var nodeList)) 30 | { 31 | nodeList.Clear(); 32 | foreach (var m in order) 33 | { 34 | var attributes = new Dictionary() 35 | { 36 | { "UUID", new NodeAttribute(AttributeType.FixedString) {Value = m.UUID}}, 37 | { "Name", new NodeAttribute(AttributeType.LSString) {Value = m.Name}}, 38 | { "Version", new NodeAttribute(AttributeType.Int) {Value = m.Version.VersionInt}}, 39 | { "MD5", new NodeAttribute(AttributeType.LSString) {Value = m.MD5}}, 40 | { "Folder", new NodeAttribute(AttributeType.LSString) {Value = m.Folder}}, 41 | }; 42 | var modNode = new Node() 43 | { 44 | Name = "ModuleShortDesc", 45 | Parent = dependenciesNode, 46 | Attributes = attributes, 47 | Children = new Dictionary>() 48 | }; 49 | dependenciesNode.AppendChild(modNode); 50 | //nodeList.Add(modNode); 51 | } 52 | } 53 | } 54 | ResourceUtils.SaveResource(MetaResource, FilePath, LSLib.LS.Enums.ResourceFormat.LSF, conversionParams); 55 | if (File.Exists(FilePath)) 56 | { 57 | File.SetLastWriteTime(FilePath, DateTime.Now); 58 | File.SetLastAccessTime(FilePath, DateTime.Now); 59 | LastModified = DateTime.Now; 60 | DivinityApp.Log($"Wrote GM campaign metadata to {FilePath}"); 61 | } 62 | return true; 63 | } 64 | catch (Exception ex) 65 | { 66 | DivinityApp.Log($"Error saving GM Campaign meta.lsf:\n{ex}"); 67 | } 68 | return false; 69 | } 70 | 71 | public DivinityGameMasterCampaign() : base() 72 | { 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Core/Models/DivinityMissingModData.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Models; 2 | 3 | public class DivinityMissingModData 4 | { 5 | public string Name { get; set; } 6 | public int Index { get; set; } 7 | public string UUID { get; set; } 8 | public string Author { get; set; } 9 | public bool IsDependency { get; set; } 10 | public List RequiredBy { get; } = []; 11 | 12 | public override string ToString() 13 | { 14 | List text = []; 15 | if (Index > 0) 16 | { 17 | text.Add($"{Index}. "); 18 | } 19 | if(!string.IsNullOrWhiteSpace(Name)) 20 | { 21 | text.Add(Name); 22 | } 23 | else 24 | { 25 | text.Add(UUID); 26 | } 27 | if (!string.IsNullOrEmpty(Author)) 28 | { 29 | text.Add(" by " + Author); 30 | } 31 | if (RequiredBy.Count > 0) 32 | { 33 | text.Add(", Required By " + string.Join(';', RequiredBy.Order().Distinct())); 34 | } 35 | return string.Join("", text); 36 | } 37 | 38 | public static DivinityMissingModData FromData(DivinityModData modData, bool isDependency = true, string[] requiredBy = null) 39 | { 40 | var data = new DivinityMissingModData 41 | { 42 | Name = modData.Name, 43 | UUID = modData.UUID, 44 | Author = modData.Author, 45 | Index = modData.Index, 46 | IsDependency = isDependency 47 | }; 48 | if (requiredBy != null) 49 | { 50 | data.RequiredBy.AddRange(requiredBy); 51 | } 52 | return data; 53 | } 54 | 55 | public static DivinityMissingModData FromData(DivinityLoadOrderEntry modData, int index, bool isDependency = true, string[] requiredBy = null) 56 | { 57 | var data = new DivinityMissingModData 58 | { 59 | Name = modData.Name, 60 | UUID = modData.UUID, 61 | Index = index, 62 | IsDependency = isDependency 63 | }; 64 | if (requiredBy != null) 65 | { 66 | data.RequiredBy.AddRange(requiredBy); 67 | } 68 | return data; 69 | } 70 | 71 | public static DivinityMissingModData FromData(ModuleShortDesc modData, int index, bool isDependency = true, string[] requiredBy = null) 72 | { 73 | var data = new DivinityMissingModData 74 | { 75 | Name = modData.Name, 76 | UUID = modData.UUID, 77 | Index = index, 78 | IsDependency = isDependency 79 | }; 80 | if (requiredBy != null) 81 | { 82 | data.RequiredBy.AddRange(requiredBy); 83 | } 84 | return data; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Core/Models/DivinityModFilterData.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace DivinityModManager.Models; 4 | 5 | public struct DivinityModFilterData 6 | { 7 | public string FilterProperty { get; set; } 8 | public string FilterValue { get; set; } 9 | 10 | private static readonly char[] separators = new char[1] { ' ' }; 11 | 12 | public bool ValueContains(string val, bool separateWhitespace = false) 13 | { 14 | if (separateWhitespace && val.IndexOf(" ") > 1) 15 | { 16 | var vals = val.Split(separators, StringSplitOptions.RemoveEmptyEntries); 17 | var findVals = FilterValue.Split(separators, StringSplitOptions.RemoveEmptyEntries); 18 | DivinityApp.Log($"Searching for '{String.Join("; ", findVals)}' in ({String.Join("; ", vals)}"); 19 | return vals.Any(x => findVals.Any(x2 => CultureInfo.CurrentCulture.CompareInfo.IndexOf(x, x2, CompareOptions.IgnoreCase) >= 0)); 20 | } 21 | else 22 | { 23 | return CultureInfo.CurrentCulture.CompareInfo.IndexOf(val, FilterValue, CompareOptions.IgnoreCase) >= 0; 24 | } 25 | } 26 | 27 | public bool PropertyContains(string val) 28 | { 29 | return CultureInfo.CurrentCulture.CompareInfo.IndexOf(FilterProperty, val, CompareOptions.IgnoreCase) >= 0; 30 | } 31 | 32 | public bool Match(DivinityModData mod) 33 | { 34 | if (String.IsNullOrWhiteSpace(FilterValue)) return true; 35 | 36 | if (PropertyContains("Author")) 37 | { 38 | if (ValueContains(mod.Author)) return true; 39 | } 40 | 41 | if (PropertyContains("Version")) 42 | { 43 | if (ValueContains(mod.Version.Version)) return true; 44 | } 45 | 46 | if (PropertyContains("Depend")) 47 | { 48 | foreach (var dependency in mod.Dependencies.Items) 49 | { 50 | if (ValueContains(dependency.Name) || FilterValue == dependency.UUID || ValueContains(dependency.Folder)) 51 | { 52 | return true; 53 | } 54 | } 55 | } 56 | 57 | if (PropertyContains("Name")) 58 | { 59 | //DivinityApp.LogMessage($"Searching for '{FilterValue}' in '{mod.Name}' | {mod.Name.IndexOf(FilterValue)}"); 60 | if (ValueContains(mod.Name)) return true; 61 | } 62 | 63 | if (PropertyContains("File")) 64 | { 65 | if (ValueContains(mod.FileName)) return true; 66 | } 67 | 68 | if (PropertyContains("Desc")) 69 | { 70 | if (ValueContains(mod.Description)) return true; 71 | } 72 | 73 | if (PropertyContains("Type")) 74 | { 75 | if (ValueContains(mod.ModType)) return true; 76 | } 77 | 78 | if (PropertyContains("UUID")) 79 | { 80 | if (ValueContains(mod.UUID)) return true; 81 | } 82 | 83 | if (PropertyContains("Selected")) 84 | { 85 | if (mod.IsSelected) return true; 86 | } 87 | 88 | if (PropertyContains("Editor")) 89 | { 90 | if (mod.IsEditorMod) return true; 91 | } 92 | 93 | if (PropertyContains("Modified") || PropertyContains("Updated")) 94 | { 95 | DateTime date = DateTime.Now; 96 | if (DateTime.TryParse(FilterValue, out date)) 97 | { 98 | if (mod.LastModified >= date) return true; 99 | } 100 | } 101 | 102 | if (PropertyContains("Tag")) 103 | { 104 | if (mod.Tags != null && mod.Tags.Count > 0) 105 | { 106 | var f = this; 107 | if (mod.Tags.Any(x => f.ValueContains(x))) return true; 108 | } 109 | } 110 | 111 | return false; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Core/Models/DivinityModScriptExtenderConfig.cs: -------------------------------------------------------------------------------- 1 | using DynamicData; 2 | using DynamicData.Binding; 3 | 4 | using System.Runtime.Serialization; 5 | 6 | namespace DivinityModManager.Models; 7 | 8 | [DataContract] 9 | public class DivinityModScriptExtenderConfig : ReactiveObject 10 | { 11 | [DataMember][Reactive] public int RequiredVersion { get; set; } 12 | [DataMember][Reactive] public string ModTable { get; set; } 13 | 14 | [DataMember] public ObservableCollectionExtended FeatureFlags { get; set; } 15 | 16 | private ObservableAsPropertyHelper _totalFeatureFlags; 17 | public int TotalFeatureFlags => _totalFeatureFlags.Value; 18 | 19 | private ObservableAsPropertyHelper _hasAnySettings; 20 | public bool HasAnySettings => _hasAnySettings.Value; 21 | 22 | public bool Lua => FeatureFlags.Contains("Lua"); 23 | 24 | public DivinityModScriptExtenderConfig() 25 | { 26 | RequiredVersion = -1; 27 | FeatureFlags = new ObservableCollectionExtended(); 28 | var featureFlagsConnection = FeatureFlags.ToObservableChangeSet(); 29 | _totalFeatureFlags = featureFlagsConnection.Count().ToProperty(this, nameof(TotalFeatureFlags), 0); 30 | _hasAnySettings = this.WhenAnyValue(x => x.RequiredVersion, x => x.TotalFeatureFlags, x => x.ModTable) 31 | .Select(x => x.Item1 > -1 || x.Item2 > 0 || !String.IsNullOrEmpty(x.Item3)).ToProperty(this, nameof(HasAnySettings)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Core/Models/DivinityModVersion.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace DivinityModManager.Models; 4 | 5 | [JsonObject(MemberSerialization.OptIn)] 6 | public class DivinityModVersion : ReactiveObject 7 | { 8 | [Reactive] public int Major { get; set; } 9 | [Reactive] public int Minor { get; set; } 10 | [Reactive] public int Revision { get; set; } 11 | [Reactive] public int Build { get; set; } 12 | 13 | private readonly ObservableAsPropertyHelper _version; 14 | public string Version => _version.Value; 15 | 16 | private readonly ObservableAsPropertyHelper _versionInt; 17 | public int VersionInt => _versionInt.Value; 18 | 19 | public int ToInt() 20 | { 21 | return (Major << 28) + (Minor << 24) + (Revision << 16) + (Build); 22 | } 23 | 24 | public override string ToString() 25 | { 26 | return Version; 27 | } 28 | 29 | public void ParseInt(int vInt) 30 | { 31 | Major = (vInt >> 28); 32 | Minor = (vInt >> 24) & 0x0F; 33 | Revision = (vInt >> 16) & 0xFF; 34 | Build = (vInt & 0xFFFF); 35 | } 36 | 37 | public static string StringFromIndividual(int major, int minor, int revision, int build) 38 | { 39 | return $"{major}.{minor}.{revision}.{build}"; 40 | } 41 | 42 | public static int IntFromIndividual(int major, int minor, int revision, int build) 43 | { 44 | return (major << 28) + (minor << 24) + (revision << 16) + (build); 45 | } 46 | 47 | public static DivinityModVersion FromInt(int vInt) 48 | { 49 | return new DivinityModVersion(vInt); 50 | } 51 | 52 | public DivinityModVersion() 53 | { 54 | var whenAnyNum = this.WhenAnyValue(x => x.Major, x => x.Minor, x => x.Revision, x => x.Build); 55 | _version = whenAnyNum.Select(v => StringFromIndividual(v.Item1, v.Item2, v.Item3, v.Item4)).ToProperty(this, nameof(Version)); 56 | _versionInt = whenAnyNum.Select(v => IntFromIndividual(v.Item1, v.Item2, v.Item3, v.Item4)).ToProperty(this, nameof(VersionInt)); 57 | } 58 | 59 | public DivinityModVersion(int vInt) : this() 60 | { 61 | ParseInt(vInt); 62 | } 63 | 64 | public static bool operator >(DivinityModVersion a, DivinityModVersion b) 65 | { 66 | return a.VersionInt > b.VersionInt; 67 | } 68 | 69 | public static bool operator <(DivinityModVersion a, DivinityModVersion b) 70 | { 71 | return a.VersionInt < b.VersionInt; 72 | } 73 | 74 | public static bool operator >=(DivinityModVersion a, DivinityModVersion b) 75 | { 76 | return a.VersionInt >= b.VersionInt; 77 | } 78 | 79 | public static bool operator <=(DivinityModVersion a, DivinityModVersion b) 80 | { 81 | return a.VersionInt <= b.VersionInt; 82 | } 83 | 84 | public DivinityModVersion(int headerMajor, int headerMinor, int headerRevision, int headerBuild) : this() 85 | { 86 | Major = headerMajor; 87 | Minor = headerMinor; 88 | Revision = headerRevision; 89 | Build = headerBuild; 90 | } 91 | } -------------------------------------------------------------------------------- /src/Core/Models/DivinityModVersion2.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace DivinityModManager.Models; 4 | 5 | [JsonObject(MemberSerialization.OptIn)] 6 | public class DivinityModVersion2 : ReactiveObject 7 | { 8 | [Reactive] public ulong Major { get; set; } 9 | [Reactive] public ulong Minor { get; set; } 10 | [Reactive] public ulong Revision { get; set; } 11 | [Reactive] public ulong Build { get; set; } 12 | 13 | [JsonProperty][Reactive] public string Version { get; set; } 14 | 15 | private ulong versionInt = 0; 16 | 17 | [JsonProperty] 18 | public ulong VersionInt 19 | { 20 | get { return versionInt; } 21 | set => ParseInt(value); 22 | } 23 | 24 | private void UpdateVersion() 25 | { 26 | Version = $"{Major}.{Minor}.{Revision}.{Build}"; 27 | } 28 | 29 | public ulong ToInt() 30 | { 31 | return (Major << 55) + (Minor << 47) + (Revision << 31) + Build; 32 | } 33 | 34 | public override string ToString() 35 | { 36 | return String.Format("{0}.{1}.{2}.{3}", Major, Minor, Revision, Build); 37 | } 38 | 39 | public void ParseInt(ulong nextVersionInt) 40 | { 41 | nextVersionInt = Math.Max(ulong.MinValue, Math.Min(nextVersionInt, ulong.MaxValue)); 42 | if (versionInt != nextVersionInt) 43 | { 44 | versionInt = nextVersionInt; 45 | if (versionInt != 0) 46 | { 47 | Major = versionInt >> 55; 48 | Minor = (versionInt >> 47) & 0xFF; 49 | Revision = (versionInt >> 31) & 0xFFFF; 50 | Build = versionInt & 0x7FFFFFFFUL; 51 | } 52 | else 53 | { 54 | Major = Minor = Revision = Build = 0; 55 | } 56 | this.RaisePropertyChanged("VersionInt"); 57 | } 58 | } 59 | 60 | public static DivinityModVersion2 FromInt(ulong vInt) 61 | { 62 | if (vInt == 1 || vInt == 268435456) 63 | { 64 | // 1.0.0.0 65 | vInt = 36028797018963968; 66 | } 67 | return new DivinityModVersion2(vInt); 68 | } 69 | 70 | public static bool operator >(DivinityModVersion2 a, DivinityModVersion2 b) 71 | { 72 | return a.VersionInt > b.VersionInt; 73 | } 74 | 75 | public static bool operator <(DivinityModVersion2 a, DivinityModVersion2 b) 76 | { 77 | return a.VersionInt < b.VersionInt; 78 | } 79 | 80 | public static bool operator >=(DivinityModVersion2 a, DivinityModVersion2 b) 81 | { 82 | return a.VersionInt >= b.VersionInt; 83 | } 84 | 85 | public static bool operator <=(DivinityModVersion2 a, DivinityModVersion2 b) 86 | { 87 | return a.VersionInt <= b.VersionInt; 88 | } 89 | 90 | public DivinityModVersion2() 91 | { 92 | this.WhenAnyValue(x => x.VersionInt).Subscribe((x) => 93 | { 94 | UpdateVersion(); 95 | }); 96 | } 97 | 98 | public DivinityModVersion2(ulong vInt) : this() 99 | { 100 | ParseInt(vInt); 101 | } 102 | 103 | public DivinityModVersion2(ulong headerMajor, ulong headerMinor, ulong headerRevision, ulong headerBuild) : this() 104 | { 105 | Major = headerMajor; 106 | Minor = headerMinor; 107 | Revision = headerRevision; 108 | Build = headerBuild; 109 | versionInt = ToInt(); 110 | UpdateVersion(); 111 | } 112 | 113 | public static readonly DivinityModVersion2 Empty = new(0); 114 | } 115 | -------------------------------------------------------------------------------- /src/Core/Models/DivinityModWorkshopCachedData.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Models; 2 | 3 | public class DivinityModWorkshopCachedData 4 | { 5 | public string UUID { get; set; } 6 | public string WorkshopID { get; set; } 7 | public long Created { get; set; } 8 | public long LastUpdated { get; set; } 9 | public List Tags { get; set; } = new List(); 10 | } 11 | -------------------------------------------------------------------------------- /src/Core/Models/DivinityModWorkshopData.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Models; 2 | 3 | public class DivinityModWorkshopData : ReactiveObject 4 | { 5 | private string id = ""; 6 | 7 | public string ID 8 | { 9 | get => id; 10 | set { this.RaiseAndSetIfChanged(ref id, value); } 11 | } 12 | 13 | private DateTime createdDate; 14 | 15 | public DateTime CreatedDate 16 | { 17 | get => createdDate; 18 | set { this.RaiseAndSetIfChanged(ref createdDate, value); } 19 | } 20 | 21 | private DateTime updatedDate; 22 | 23 | public DateTime UpdatedDate 24 | { 25 | get => updatedDate; 26 | set { this.RaiseAndSetIfChanged(ref updatedDate, value); } 27 | } 28 | 29 | private List tags; 30 | 31 | public List Tags 32 | { 33 | get => tags; 34 | set 35 | { 36 | this.RaiseAndSetIfChanged(ref tags, value); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Core/Models/DivinityPathwayData.cs: -------------------------------------------------------------------------------- 1 |  2 | using DivinityModManager.Extensions; 3 | 4 | namespace DivinityModManager.Models; 5 | 6 | public class DivinityPathwayData : ReactiveObject 7 | { 8 | /// 9 | /// The path to the root game folder, i.e. SteamLibrary\steamapps\common\Baldur's Gate 3 10 | /// 11 | [Reactive] public string InstallPath { get; set; } 12 | 13 | /// 14 | /// The path to %LOCALAPPDATA%\Larian Studios\Baldur's Gate 3 15 | /// 16 | [Reactive] public string AppDataGameFolder { get; set; } 17 | 18 | /// 19 | /// The path to %LOCALAPPDATA%\Larian Studios\Baldur's Gate 3\Mods 20 | /// 21 | [Reactive] public string AppDataModsPath { get; set; } 22 | 23 | /// 24 | /// The path to %LOCALAPPDATA%\Larian Studios\Baldur's Gate 3\PlayerProfiles 25 | /// 26 | [Reactive] public string AppDataProfilesPath { get; set; } 27 | 28 | [Reactive] public string LastSaveFilePath { get; set; } 29 | 30 | [Reactive] public string ScriptExtenderLatestReleaseUrl { get; set; } 31 | [Reactive] public string ScriptExtenderLatestReleaseVersion { get; set; } 32 | 33 | public DivinityPathwayData() 34 | { 35 | InstallPath = ""; 36 | AppDataGameFolder = ""; 37 | AppDataModsPath = ""; 38 | LastSaveFilePath = ""; 39 | ScriptExtenderLatestReleaseUrl = ""; 40 | ScriptExtenderLatestReleaseVersion = ""; 41 | } 42 | 43 | public string ScriptExtenderSettingsFile(DivinityModManagerSettings settings) 44 | { 45 | if (settings.GameExecutablePath.IsExistingFile()) 46 | { 47 | return Path.Combine(Path.GetDirectoryName(settings.GameExecutablePath), DivinityApp.EXTENDER_CONFIG_FILE); 48 | } 49 | return ""; 50 | } 51 | 52 | public string ScriptExtenderUpdaterConfigFile(DivinityModManagerSettings settings) 53 | { 54 | if (settings.GameExecutablePath.IsExistingFile()) 55 | { 56 | return Path.Combine(Path.GetDirectoryName(settings.GameExecutablePath), DivinityApp.EXTENDER_UPDATER_CONFIG_FILE); 57 | } 58 | return ""; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Core/Models/DivinityProfileActiveModData.cs: -------------------------------------------------------------------------------- 1 | using LSLib.LS; 2 | 3 | namespace DivinityModManager.Models; 4 | 5 | public class DivinityProfileActiveModData 6 | { 7 | public string Folder { get; set; } 8 | public string MD5 { get; set; } 9 | public string Name { get; set; } 10 | public string UUID { get; set; } 11 | public ulong Version { get; set; } 12 | 13 | private static readonly NodeSerializationSettings _serializationSettings = new() 14 | { 15 | ByteSwapGuids = false, 16 | DefaultByteSwapGuids = false 17 | }; 18 | 19 | private static string GetAttributeAsString(Dictionary attributes, string name, string fallBack) 20 | { 21 | if (attributes.TryGetValue(name, out var attribute)) 22 | { 23 | return attribute.AsString(_serializationSettings); 24 | } 25 | return fallBack; 26 | } 27 | 28 | private static ulong GetULongAttribute(Dictionary attributes, string name, ulong fallBack) 29 | { 30 | if (attributes.TryGetValue(name, out var attribute)) 31 | { 32 | if (attribute.Value is string att) 33 | { 34 | if (ulong.TryParse(att, out ulong val)) 35 | { 36 | return val; 37 | } 38 | else 39 | { 40 | return fallBack; 41 | } 42 | } 43 | else if (attribute.Value is ulong val) 44 | { 45 | return val; 46 | } 47 | } 48 | return fallBack; 49 | } 50 | 51 | public void LoadFromAttributes(Dictionary attributes) 52 | { 53 | Folder = GetAttributeAsString(attributes, "Folder", ""); 54 | MD5 = GetAttributeAsString(attributes, "MD5", ""); 55 | Name = GetAttributeAsString(attributes, "Name", ""); 56 | UUID = GetAttributeAsString(attributes, "UUID", ""); 57 | Version = GetULongAttribute(attributes, "Version", 0UL); 58 | 59 | //DivinityApp.LogMessage($"[DivinityProfileActiveModData] Name({Name}) UUID({UUID})"); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Core/Models/DivinityProfileData.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using DivinityModManager.Util; 4 | 5 | using DynamicData; 6 | 7 | namespace DivinityModManager.Models; 8 | 9 | public class DivinityProfileData : ReactiveObject 10 | { 11 | [Reactive] public string Name { get; set; } 12 | 13 | /// 14 | /// The stored name in the profile.lsb or profile5.lsb file. 15 | /// 16 | [Reactive] public string ProfileName { get; set; } 17 | [Reactive] public string UUID { get; set; } 18 | [Reactive] public string ModSettingsFile { get; set; } 19 | 20 | [Reactive] public string Folder { get; private set; } 21 | 22 | /// 23 | /// The mod data under the Mods node, from modsettings.lsx. 24 | /// 25 | public List ActiveMods { get; set; } 26 | 27 | public DivinityLoadOrder GetLoadOrder(SourceCache mods) 28 | { 29 | var order = new DivinityLoadOrder() { Name = "Current", FilePath = Path.Combine(Folder, "modsettings.lsx"), IsModSettings = true }; 30 | var i = 0; 31 | foreach (var activeMod in ActiveMods) 32 | { 33 | var mod = mods.Items.FirstOrDefault(m => m.UUID.Equals(activeMod.UUID, StringComparison.OrdinalIgnoreCase)); 34 | if (mod != null) 35 | { 36 | order.Add(mod); 37 | } 38 | i++; 39 | } 40 | return order; 41 | } 42 | 43 | public DivinityProfileData(string uuid, string modSettingsFile) 44 | { 45 | UUID = uuid; 46 | ModSettingsFile = modSettingsFile; 47 | ActiveMods = []; 48 | 49 | this.WhenAnyValue(x => x.ModSettingsFile).Select(DivinityFileUtils.GetParentOrEmpty).BindTo(this, x => x.Folder); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Core/Models/DivinitySerializedModData.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace DivinityModManager.Models; 4 | 5 | [DataContract] 6 | public class DivinitySerializedModData : IDivinityModData 7 | { 8 | [DataMember] public int Index { get; set; } 9 | [DataMember] public string FileName { get; set; } 10 | [DataMember] public string UUID { get; set; } 11 | [DataMember] public string Folder { get; set; } 12 | [DataMember] public string Name { get; set; } 13 | [DataMember] public string Description { get; set; } 14 | [DataMember] public string Author { get; set; } 15 | 16 | [DataMember] public DivinityModVersion2 Version { get; set; } 17 | 18 | [DataMember] public string Type { get; set; } 19 | [DataMember] public List Modes { get; set; } 20 | 21 | [DataMember] public string Targets { get; set; } 22 | 23 | [DataMember] public DivinityModScriptExtenderConfig ScriptExtenderData { get; set; } 24 | [DataMember] public List Dependencies { get; set; } 25 | 26 | [DataMember] public string MD5 { get; set; } 27 | 28 | public static DivinitySerializedModData FromMod(DivinityModData mod) 29 | { 30 | return new DivinitySerializedModData 31 | { 32 | Author = mod.Author, 33 | Dependencies = mod.Dependencies.Items.ToList(), 34 | Description = mod.Description, 35 | FileName = mod.FileName, 36 | Folder = mod.Folder, 37 | Name = mod.Name, 38 | Version = mod.Version, 39 | Type = mod.ModType, 40 | Index = mod.Index, 41 | ScriptExtenderData = mod.ScriptExtenderData, 42 | UUID = mod.UUID, 43 | MD5 = mod.MD5 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Core/Models/Extender/ScriptExtenderUpdateConfig.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Enums.Extender; 2 | using DivinityModManager.Extensions; 3 | 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Converters; 6 | 7 | using System.ComponentModel; 8 | using System.Runtime.Serialization; 9 | 10 | namespace DivinityModManager.Models.Extender; 11 | 12 | [DataContract] 13 | public class ScriptExtenderUpdateConfig : ReactiveObject 14 | { 15 | [Reactive] public bool UpdaterIsAvailable { get; set; } 16 | [Reactive] public int UpdaterVersion { get; set; } 17 | 18 | [DefaultValue(ExtenderUpdateChannel.Release)] 19 | [SettingsEntry("Update Channel", "Use a specific update channel", HideFromUI = true)] 20 | [DataMember, Reactive] 21 | public ExtenderUpdateChannel UpdateChannel { get; set; } 22 | 23 | [DefaultValue("")] 24 | [SettingsEntry("Target Version", "Update to a specific version of the script extender (ex. '5.0.0.0')")] 25 | [DataMember, Reactive] 26 | public string TargetVersion { get; set; } 27 | 28 | [DefaultValue("")] 29 | [SettingsEntry("Target Resource Digest", "Use a specific Digest for the target update", true)] 30 | [DataMember, Reactive] 31 | public string TargetResourceDigest { get; set; } 32 | 33 | [DefaultValue(false)] 34 | [SettingsEntry("Disable Updates", "Disable automatic updating to the latest extender version")] 35 | [DataMember, Reactive] 36 | public bool DisableUpdates { get; set; } 37 | 38 | [DefaultValue(false)] 39 | [SettingsEntry("IPv4Only", "Use only IPv4 when fetching the latest update")] 40 | [DataMember, Reactive] 41 | public bool IPv4Only { get; set; } 42 | 43 | [DefaultValue(false)] 44 | [SettingsEntry("Debug", "Enable debug mode in the extender updater, which prints more messages to the console window")] 45 | [DataMember, Reactive] 46 | public bool Debug { get; set; } 47 | 48 | [DefaultValue("")] 49 | [SettingsEntry("Manifest URL", "", true)] 50 | [DataMember, Reactive] 51 | public string ManifestURL { get; set; } 52 | 53 | [DefaultValue("")] 54 | [SettingsEntry("Manifest Name", "", true)] 55 | [DataMember, Reactive] 56 | public string ManifestName { get; set; } 57 | 58 | [DefaultValue("")] 59 | [SettingsEntry("CachePath", "", true)] 60 | [DataMember, Reactive] 61 | public string CachePath { get; set; } 62 | 63 | [DefaultValue(false)] 64 | [SettingsEntry("Validate Signature", "", true)] 65 | [DataMember, Reactive] 66 | public bool ValidateSignature { get; set; } 67 | 68 | public ScriptExtenderUpdateConfig() 69 | { 70 | this.SetToDefault(); 71 | UpdaterVersion = -1; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Core/Models/Extender/ScriptExtenderUpdateData.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Runtime.Serialization; 3 | 4 | namespace DivinityModManager.Models.Extender; 5 | 6 | [DataContract] 7 | public class ScriptExtenderUpdateData 8 | { 9 | [DataMember] public int ManifestMinorVersion { get; set; } 10 | [DataMember] public int ManifestVersion { get; set; } 11 | [DataMember] public string NoMatchingVersionNotice { get; set; } 12 | [DataMember] public List Resources { get; set; } 13 | } 14 | 15 | [DataContract] 16 | public class ScriptExtenderUpdateResource 17 | { 18 | [DataMember] public string Name { get; set; } 19 | [DataMember] public List Versions { get; set; } 20 | } 21 | 22 | [DataContract] 23 | public class ScriptExtenderUpdateVersion : ReactiveObject 24 | { 25 | [DataMember][Reactive] public long BuildDate { get; set; } 26 | [DataMember][Reactive] public string Digest { get; set; } 27 | [DataMember][Reactive] public string MinGameVersion { get; set; } 28 | [DataMember][Reactive] public string Notice { get; set; } 29 | [DataMember][Reactive] public string URL { get; set; } 30 | [DataMember][Reactive] public string Version { get; set; } 31 | [DataMember][Reactive] public string Signature { get; set; } 32 | 33 | private readonly ObservableAsPropertyHelper _displayName; 34 | public string DisplayName => _displayName.Value; 35 | 36 | private readonly ObservableAsPropertyHelper _buildDateDate; 37 | public string BuildDateDisplayString => _buildDateDate.Value; 38 | 39 | private readonly ObservableAsPropertyHelper _isEmpty; 40 | public bool IsEmpty => _isEmpty.Value; 41 | 42 | private string TimestampToReadableString(long timestamp) 43 | { 44 | var date = DateTime.FromFileTime(timestamp); 45 | return date.ToString(DivinityApp.DateTimeExtenderBuildFormat, CultureInfo.InstalledUICulture); 46 | } 47 | 48 | private string ToDisplayName(ValueTuple data) 49 | { 50 | if (String.IsNullOrEmpty(data.Item1)) return "Latest"; 51 | var result = data.Item1; 52 | if (!String.IsNullOrEmpty(data.Item2)) 53 | { 54 | result += $" ({data.Item2})"; 55 | } 56 | if (!String.IsNullOrEmpty(data.Item3)) 57 | { 58 | result += $" - {data.Item3}"; 59 | } 60 | return result; 61 | } 62 | 63 | public ScriptExtenderUpdateVersion() 64 | { 65 | _isEmpty = this.WhenAnyValue(x => x.Version).Select(x => String.IsNullOrEmpty(x)).ToProperty(this, nameof(IsEmpty), true, RxApp.MainThreadScheduler); 66 | _buildDateDate = this.WhenAnyValue(x => x.BuildDate).Select(TimestampToReadableString).ToProperty(this, nameof(BuildDateDisplayString), true, RxApp.MainThreadScheduler); 67 | _displayName = this.WhenAnyValue(x => x.Version, x => x.MinGameVersion, x => x.BuildDateDisplayString).Select(ToDisplayName).ToProperty(this, nameof(DisplayName), true, RxApp.MainThreadScheduler); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Core/Models/Github/GithubModData.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Models.Github; 2 | 3 | public class GithubModData : ReactiveObject 4 | { 5 | [Reactive] public string Author { get; set; } 6 | [Reactive] public string Repository { get; set; } 7 | [Reactive] public Uri LatestRelease { get; set; } 8 | 9 | public void Update(GithubModData data) 10 | { 11 | //TODO 12 | Author = data.Author; 13 | Repository = data.Repository; 14 | LatestRelease = data.LatestRelease; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Core/Models/ISelectable.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace DivinityModManager.Models; 4 | 5 | public interface ISelectable 6 | { 7 | bool IsSelected { get; set; } 8 | Visibility Visibility { get; set; } 9 | bool CanDrag { get; } 10 | } 11 | -------------------------------------------------------------------------------- /src/Core/Models/ModFileDeletionData.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Models; 2 | 3 | public class ModFileDeletionData : ReactiveObject 4 | { 5 | [Reactive] public bool IsSelected { get; set; } 6 | [Reactive] public string FilePath { get; set; } 7 | [Reactive] public string DisplayName { get; set; } 8 | [Reactive] public string UUID { get; set; } 9 | [Reactive] public string Duplicates { get; set; } 10 | 11 | public static ModFileDeletionData FromMod(DivinityModData mod, bool isDeletingDuplicates = false, List loadedMods = null) 12 | { 13 | var data = new ModFileDeletionData { FilePath = mod.FilePath, DisplayName = mod.DisplayName, IsSelected = true, UUID = mod.UUID}; 14 | if (isDeletingDuplicates && loadedMods != null) 15 | { 16 | var duplicatesStr = loadedMods.FirstOrDefault(x => x.UUID == mod.UUID)?.FilePath; 17 | if (!String.IsNullOrEmpty(duplicatesStr)) 18 | { 19 | data.Duplicates = duplicatesStr; 20 | } 21 | } 22 | return data; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Core/Models/ModuleShortDesc.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace DivinityModManager.Models; 4 | 5 | [DataContract] 6 | public class ModuleShortDesc : ReactiveObject, IDivinityModData 7 | { 8 | [DataMember, Reactive] public string UUID { get; set; } 9 | [DataMember, Reactive] public string Name { get; set; } 10 | [DataMember, Reactive] public string Folder { get; set; } 11 | [DataMember, Reactive] public string MD5 { get; set; } 12 | [DataMember, Reactive] public ulong PublishHandle { get; set; } 13 | [DataMember, Reactive] public DivinityModVersion2 Version { get; set; } 14 | public DateTimeOffset? LastModified { get; set; } 15 | 16 | public override string ToString() => $"[ModuleShortDesc] Name({Name}) UUID({UUID}) Version({Version?.Version})"; 17 | 18 | public static ModuleShortDesc FromModData(DivinityModData m) 19 | { 20 | return new ModuleShortDesc 21 | { 22 | Folder = m.Folder, 23 | Name = m.Name, 24 | UUID = m.UUID, 25 | MD5 = m.MD5, 26 | PublishHandle = m.PublishHandle, 27 | Version = m.Version, 28 | LastModified = m.LastModified 29 | }; 30 | } 31 | 32 | public ModuleShortDesc() 33 | { 34 | UUID = ""; 35 | Name = ""; 36 | Folder = ""; 37 | MD5 = ""; 38 | PublishHandle = 0ul; 39 | Version = new DivinityModVersion2(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Core/Models/NexusMods/NexusModFileVersionData.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace DivinityModManager.Models.NexusMods; 4 | 5 | public struct NexusModFileVersionData 6 | { 7 | public long ModId { get; set; } 8 | public long FileId { get; set; } 9 | public bool Success { get; set; } 10 | 11 | static readonly Regex _filePattern = new(@"^.*?-(\d+)-(.*?)(\d+)"); 12 | 13 | public static NexusModFileVersionData FromFilePath(string path) 14 | { 15 | var name = Path.GetFileNameWithoutExtension(path); 16 | var match = _filePattern.Match(name); 17 | 18 | long modId = -1; 19 | long fileId = -1; 20 | 21 | if (match.Success) 22 | { 23 | if (long.TryParse(match.Groups[1]?.Value, out var mid)) 24 | { 25 | modId = mid; 26 | } 27 | if (long.TryParse(match.Groups[3]?.Value, out var fid)) 28 | { 29 | fileId = fid; 30 | } 31 | } 32 | 33 | return new NexusModFileVersionData() 34 | { 35 | ModId = modId, 36 | FileId = fileId, 37 | Success = match.Success 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Core/Models/NexusMods/NexusModsModDownloadLink.cs: -------------------------------------------------------------------------------- 1 | using NexusModsNET.DataModels; 2 | 3 | namespace DivinityModManager.Models.NexusMods; 4 | 5 | public struct NexusModsModDownloadLink 6 | { 7 | public DivinityModData Mod { get; set; } 8 | public NexusModFileDownloadLink DownloadLink { get; set; } 9 | 10 | public NexusModsModDownloadLink(DivinityModData mod, NexusModFileDownloadLink link) 11 | { 12 | Mod = mod; 13 | DownloadLink = link; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Core/Models/Steam/IWorkshopPublishFileDetails.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Models.Steam; 2 | 3 | public interface IWorkshopPublishFileDetails 4 | { 5 | string publishedfileid { get; set; } 6 | long time_created { get; set; } 7 | long time_updated { get; set; } 8 | 9 | List tags { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/Core/Models/Steam/PublishedFileDetails.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Models.Steam; 2 | 3 | public struct WorkshopTag 4 | { 5 | public string tag { get; set; } 6 | } 7 | 8 | public class PublishedFileDetailsResponse 9 | { 10 | public PublishedFileDetailsResponseData response { get; set; } 11 | } 12 | 13 | public class PublishedFileDetailsResponseData 14 | { 15 | public int result { get; set; } 16 | public int resultcount { get; set; } 17 | 18 | public List publishedfiledetails { get; set; } 19 | } 20 | 21 | 22 | public class PublishedFileDetails : IWorkshopPublishFileDetails 23 | { 24 | public string publishedfileid { get; set; } 25 | public int result { get; set; } 26 | public string creator { get; set; } 27 | public int creator_app_id { get; set; } 28 | public int consumer_app_id { get; set; } 29 | public string filename { get; set; } 30 | public string file_size { get; set; } 31 | public string file_url { get; set; } 32 | public string hcontent_file { get; set; } 33 | public string preview_url { get; set; } 34 | public string hcontent_preview { get; set; } 35 | public string title { get; set; } 36 | public string description { get; set; } 37 | public long time_created { get; set; } 38 | public long time_updated { get; set; } 39 | public int visibility { get; set; } 40 | public bool banned { get; set; } 41 | public string ban_reason { get; set; } 42 | public int subscriptions { get; set; } 43 | public int favorited { get; set; } 44 | public int lifetime_subscriptions { get; set; } 45 | public int lifetime_favorited { get; set; } 46 | public int views { get; set; } 47 | public List tags { get; set; } 48 | } 49 | -------------------------------------------------------------------------------- /src/Core/Models/Steam/QueryFilesResponseData.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Util; 2 | 3 | using Newtonsoft.Json; 4 | 5 | namespace DivinityModManager.Models.Steam; 6 | 7 | public class QueryFilesResponse 8 | { 9 | public QueryFilesResponseData response { get; set; } 10 | } 11 | 12 | public class QueryFilesResponseData 13 | { 14 | public int total { get; set; } 15 | 16 | public List publishedfiledetails { get; set; } 17 | } 18 | 19 | public class QueryFilesPublishedFileDetails : IWorkshopPublishFileDetails 20 | { 21 | public int result { get; set; } 22 | public string publishedfileid { get; set; } 23 | public string creator { get; set; } 24 | public string filename { get; set; } 25 | public string file_size { get; set; } 26 | public string file_url { get; set; } 27 | public string preview_url { get; set; } 28 | public string url { get; set; } 29 | public string title { get; set; } 30 | public string description { get; set; } 31 | public long time_created { get; set; } 32 | public long time_updated { get; set; } 33 | public int visibility { get; set; } 34 | public int flags { get; set; } 35 | public List tags { get; set; } 36 | public string metadata { get; set; } 37 | [JsonIgnore] public QueryFilesPublishedFileDivinityMetadataMain MetaData { get; set; } 38 | public int language { get; set; } 39 | public string revision_change_number { get; set; } 40 | public int revision { get; set; } 41 | 42 | public string GetGuid() 43 | { 44 | if (this.MetaData != null) 45 | { 46 | try 47 | { 48 | return this.MetaData.root.regions.MetaData.Guid.Value; 49 | } 50 | catch 51 | { 52 | } 53 | } 54 | return null; 55 | } 56 | 57 | public void DeserializeMetadata() 58 | { 59 | if (!String.IsNullOrEmpty(metadata)) 60 | { 61 | MetaData = DivinityJsonUtils.SafeDeserialize(metadata); 62 | } 63 | } 64 | } 65 | 66 | public class QueryFilesPublishedFileDivinityMetadataMain 67 | { 68 | public QueryFilesPublishedFileDivinityMetadataRoot root { get; set; } 69 | } 70 | 71 | public class QueryFilesPublishedFileDivinityMetadataRoot 72 | { 73 | public QueryFilesPublishedFileDivinityMetadataHeader header { get; set; } 74 | public QueryFilesPublishedFileDivinityMetadataRegions regions { get; set; } 75 | } 76 | 77 | public class QueryFilesPublishedFileDivinityMetadataHeader 78 | { 79 | public int time { get; set; } 80 | public string version { get; set; } 81 | } 82 | 83 | public class QueryFilesPublishedFileDivinityMetadataRegions 84 | { 85 | public QueryFilesPublishedFileDivinityMetadataEntry MetaData { get; set; } 86 | } 87 | 88 | public class QueryFilesPublishedFileDivinityMetadataEntry 89 | { 90 | public QueryFilesPublishedFileDivinityMetadataEntryAttribute Guid { get; set; } 91 | public QueryFilesPublishedFileDivinityMetadataEntryAttribute Type { get; set; } 92 | public QueryFilesPublishedFileDivinityMetadataEntryAttribute Version { get; set; } 93 | } 94 | public class QueryFilesPublishedFileDivinityMetadataEntryAttribute 95 | { 96 | public int type { get; set; } 97 | public object value { get; set; } 98 | 99 | [JsonIgnore] 100 | public T Value 101 | { 102 | get 103 | { 104 | return (T)value; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Core/Models/Updates/UpdateResult.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Models.Updates; 2 | 3 | public class UpdateResult 4 | { 5 | public List UpdatedMods { get; set; } 6 | public string FailureMessage { get; set; } 7 | public bool Success { get; set; } 8 | 9 | public UpdateResult() 10 | { 11 | UpdatedMods = new List(); 12 | Success = true; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Core/Models/View/EnumEntry.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Reflection; 4 | 5 | namespace DivinityModManager.Models.View; 6 | 7 | public class EnumEntry : ReactiveObject 8 | { 9 | [Reactive] public string Name { get; set; } 10 | [Reactive] public string Description { get; set; } 11 | [Reactive] public object Value { get; set; } 12 | 13 | public EnumEntry(string description, object value, string name = "") 14 | { 15 | Name = name; 16 | Description = description; 17 | Value = value; 18 | } 19 | 20 | public EnumEntry(Enum enumValue) 21 | { 22 | Value = enumValue; 23 | Name = ""; 24 | Description = ""; 25 | var member = enumValue.GetType().GetMember(enumValue.ToString()).FirstOrDefault(); 26 | if (member != null) 27 | { 28 | var descriptionAttribute = member.GetCustomAttribute(false); 29 | 30 | if (descriptionAttribute == null) 31 | { 32 | var displayAttribute = member.GetCustomAttribute(false); 33 | if (displayAttribute != null) 34 | { 35 | Name = displayAttribute.Name; 36 | Description = displayAttribute.Description; 37 | } 38 | } 39 | else 40 | { 41 | Name = descriptionAttribute.Description; 42 | } 43 | } 44 | } 45 | 46 | public EnumEntry() { } 47 | } 48 | -------------------------------------------------------------------------------- /src/Core/Models/WindowSettings.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace DivinityModManager.Models; 4 | 5 | public class WindowSettings 6 | { 7 | public bool Maximized { get; set; } 8 | 9 | [DefaultValue(0)] 10 | public int Screen { get; set; } 11 | 12 | [DefaultValue(-1)] 13 | public double X { get; set; } 14 | 15 | [DefaultValue(-1)] 16 | public double Y { get; set; } 17 | 18 | [DefaultValue(-1)] 19 | public double Width { get; set; } 20 | 21 | [DefaultValue(-1)] 22 | public double Height { get; set; } 23 | 24 | public WindowSettings() 25 | { 26 | Maximized = false; 27 | X = -1; 28 | Y = -1; 29 | Width = -1; 30 | Height = -1; 31 | Screen = 0; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Core/Services.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager; 2 | 3 | public static class Services 4 | { 5 | public static IScreenReaderService ScreenReader => Get(); 6 | 7 | public static T Get(string contract = null) 8 | { 9 | return Locator.Current.GetService(contract); 10 | } 11 | 12 | public static void Register(Func constructorCallback, string contract = null) 13 | { 14 | Locator.CurrentMutable.Register(constructorCallback, typeof(T), contract); 15 | } 16 | 17 | public static void RegisterSingleton(T instance, string contract = null) 18 | { 19 | Locator.CurrentMutable.RegisterConstant(instance, typeof(T), contract); 20 | } 21 | 22 | /// 23 | /// Register a singleton which won't get created until the first user accesses it. 24 | /// 25 | /// 26 | /// 27 | /// 28 | public static void RegisterLazySingleton(Func constructorCallback, string contract = null) 29 | { 30 | Locator.CurrentMutable.RegisterLazySingleton(constructorCallback, typeof(T), contract); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Core/Usings.cs: -------------------------------------------------------------------------------- 1 | global using ReactiveUI; 2 | global using ReactiveUI.Fody.Helpers; 3 | 4 | global using Splat; 5 | 6 | global using System; 7 | global using System.Collections; 8 | global using System.Collections.Generic; 9 | global using System.Collections.ObjectModel; 10 | global using System.IO; 11 | global using System.Linq; 12 | global using System.Net.Http; 13 | global using System.Reactive; 14 | global using System.Reactive.Concurrency; 15 | global using System.Reactive.Disposables; 16 | global using System.Reactive.Linq; 17 | global using System.Text.Json; 18 | global using System.Text.Json.Serialization; 19 | global using System.Threading; 20 | global using System.Threading.Tasks; 21 | 22 | global using RxCommandUnit = ReactiveUI.ReactiveCommand; 23 | global using JsonIgnoreAttribute = Newtonsoft.Json.JsonIgnoreAttribute; 24 | global using JsonConverterAttribute = Newtonsoft.Json.JsonConverterAttribute; -------------------------------------------------------------------------------- /src/Core/Util/DateUtils.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Util; 2 | 3 | public static class DateUtils 4 | { 5 | public static DateTime UnixTimeStampToDateTime(long unixTimeStamp) 6 | { 7 | // Unix timestamp is seconds past epoch 8 | System.DateTime dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc); 9 | long unixTimeStampInTicks = (long)(unixTimeStamp * TimeSpan.TicksPerSecond); 10 | return new DateTime(dtDateTime.Ticks + unixTimeStampInTicks, DateTimeKind.Utc); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Core/Util/DivinityGlobalEvents.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Util; 2 | 3 | public class OrderNameChangedArgs : EventArgs 4 | { 5 | public string LastName { get; set; } 6 | public string NewName { get; set; } 7 | } 8 | 9 | public class DivinityGlobalEvents 10 | { 11 | public event EventHandler OrderNameChanged; 12 | 13 | public void OnOrderNameChanged(string lastName, string newName) 14 | { 15 | EventHandler handler = OrderNameChanged; 16 | if (handler != null) 17 | { 18 | handler(this, new OrderNameChangedArgs { LastName = lastName, NewName = newName }); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Core/Util/DivinityJsonUtils.cs: -------------------------------------------------------------------------------- 1 |  2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | using Newtonsoft.Json.Serialization; 5 | 6 | using System.Text; 7 | 8 | namespace DivinityModManager.Util; 9 | 10 | public static class DivinityJsonUtils 11 | { 12 | private static readonly JsonSerializerSettings _errorHandleSettings = new() 13 | { 14 | Error = delegate (object sender, Newtonsoft.Json.Serialization.ErrorEventArgs args) 15 | { 16 | DivinityApp.Log(args.ErrorContext.Error.Message); 17 | args.ErrorContext.Handled = true; 18 | } 19 | }; 20 | 21 | public static T GetValue(this JToken jToken, string key, T defaultValue = default) 22 | { 23 | dynamic ret = jToken[key]; 24 | if (ret == null) return defaultValue; 25 | if (ret is JObject) return JsonConvert.DeserializeObject(ret.ToString()); 26 | return (T)ret; 27 | } 28 | 29 | public static T SafeDeserialize(string text, JsonSerializerSettings? settings = null) 30 | { 31 | var result = JsonConvert.DeserializeObject(text, settings ?? _errorHandleSettings); 32 | if (result != null) 33 | { 34 | return result; 35 | } 36 | return default; 37 | } 38 | 39 | public static T SafeDeserializeFromPath(string path) 40 | { 41 | try 42 | { 43 | if (File.Exists(path)) 44 | { 45 | string contents = File.ReadAllText(path); 46 | return SafeDeserialize(contents); 47 | } 48 | else 49 | { 50 | DivinityApp.Log($"Error deserializing json: File '{path}' does not exist."); 51 | } 52 | } 53 | catch (Exception ex) 54 | { 55 | DivinityApp.Log("Error deserializing json:\n" + ex.ToString()); 56 | } 57 | return default; 58 | } 59 | 60 | public static bool TrySafeDeserialize(string text, out T result) 61 | { 62 | result = JsonConvert.DeserializeObject(text, _errorHandleSettings); 63 | return result != null; 64 | } 65 | 66 | public static bool TrySafeDeserializeFromPath(string path, out T result) 67 | { 68 | if (File.Exists(path)) 69 | { 70 | string contents = File.ReadAllText(path); 71 | result = JsonConvert.DeserializeObject(contents, _errorHandleSettings); 72 | return result != null; 73 | } 74 | result = default; 75 | return false; 76 | } 77 | 78 | public static async Task DeserializeFromPathAsync(string path, CancellationToken cts) 79 | { 80 | var fileBytes = await DivinityFileUtils.LoadFileAsBytesAsync(path, cts); 81 | if (fileBytes != null) 82 | { 83 | var contents = Encoding.UTF8.GetString(fileBytes); 84 | if (!String.IsNullOrEmpty(contents)) 85 | { 86 | return JsonConvert.DeserializeObject(contents, _errorHandleSettings); 87 | } 88 | } 89 | return default; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Core/Util/DivinityModSorter.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Models; 2 | 3 | namespace DivinityModManager.Util; 4 | 5 | public static class DivinityModSorter 6 | { 7 | public static IEnumerable SortAlphabetical(IEnumerable mods) 8 | { 9 | return mods.OrderBy(x => x.DisplayName, StringComparer.OrdinalIgnoreCase); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Core/Util/DivinityStreamUtils.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Util; 2 | 3 | public static class DivinityStreamUtils 4 | { 5 | public static byte[] ReadToEnd(System.IO.Stream stream) 6 | { 7 | long originalPosition = 0; 8 | 9 | if (stream.CanSeek) 10 | { 11 | originalPosition = stream.Position; 12 | stream.Position = 0; 13 | } 14 | 15 | try 16 | { 17 | byte[] readBuffer = new byte[4096]; 18 | 19 | int totalBytesRead = 0; 20 | int bytesRead; 21 | 22 | while ((bytesRead = stream.Read(readBuffer, totalBytesRead, readBuffer.Length - totalBytesRead)) > 0) 23 | { 24 | totalBytesRead += bytesRead; 25 | 26 | if (totalBytesRead == readBuffer.Length) 27 | { 28 | int nextByte = stream.ReadByte(); 29 | if (nextByte != -1) 30 | { 31 | byte[] temp = new byte[readBuffer.Length * 2]; 32 | Buffer.BlockCopy(readBuffer, 0, temp, 0, readBuffer.Length); 33 | Buffer.SetByte(temp, totalBytesRead, (byte)nextByte); 34 | readBuffer = temp; 35 | totalBytesRead++; 36 | } 37 | } 38 | } 39 | 40 | byte[] buffer = readBuffer; 41 | if (readBuffer.Length != totalBytesRead) 42 | { 43 | buffer = new byte[totalBytesRead]; 44 | Buffer.BlockCopy(readBuffer, 0, buffer, 0, totalBytesRead); 45 | } 46 | return buffer; 47 | } 48 | finally 49 | { 50 | if (stream.CanSeek) 51 | { 52 | stream.Position = originalPosition; 53 | } 54 | } 55 | } 56 | 57 | public static int IndexOf(this byte[] arrayToSearchThrough, byte[] patternToFind) 58 | { 59 | if (patternToFind.Length > arrayToSearchThrough.Length) 60 | return -1; 61 | for (int i = 0; i < arrayToSearchThrough.Length - patternToFind.Length; i++) 62 | { 63 | bool found = true; 64 | for (int j = 0; j < patternToFind.Length; j++) 65 | { 66 | if (arrayToSearchThrough[i + j] != patternToFind[j]) 67 | { 68 | found = false; 69 | break; 70 | } 71 | } 72 | if (found) 73 | { 74 | return i; 75 | } 76 | } 77 | return -1; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Core/Util/GithubHelper.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | 3 | namespace DivinityModManager.Util; 4 | 5 | public static class GithubHelper 6 | { 7 | private static readonly string GIT_URL_REPO_LATEST = "https://api.github.com/repos/{0}/releases/latest"; 8 | private static readonly string GIT_URL_REPO_RELEASES = "https://api.github.com/repos/{0}/releases"; 9 | 10 | public static async Task GetLatestReleaseDataAsync(string repo, CancellationToken t) 11 | { 12 | return await WebHelper.DownloadUrlAsStringAsync(String.Format(GIT_URL_REPO_LATEST, repo), t); 13 | } 14 | 15 | public static async Task GetAllReleaseDataAsync(string repo, CancellationToken t) 16 | { 17 | return await WebHelper.DownloadUrlAsStringAsync(String.Format(GIT_URL_REPO_RELEASES, repo), t); 18 | } 19 | 20 | private static string GetBrowserDownloadUrl(string dataString) 21 | { 22 | var jsonData = DivinityJsonUtils.SafeDeserialize>(dataString); 23 | if (jsonData != null) 24 | { 25 | if (jsonData.TryGetValue("assets", out var assetsArray)) 26 | { 27 | JArray assets = (JArray)assetsArray; 28 | foreach (var obj in assets.Children()) 29 | { 30 | if (obj.TryGetValue("browser_download_url", StringComparison.OrdinalIgnoreCase, out var browserUrl)) 31 | { 32 | return browserUrl.ToString(); 33 | } 34 | } 35 | } 36 | #if DEBUG 37 | var lines = jsonData.Select(kvp => kvp.Key + ": " + kvp.Value.ToString()); 38 | DivinityApp.Log($"Can't find 'browser_download_url' in:\n{String.Join(Environment.NewLine, lines)}"); 39 | #endif 40 | } 41 | return ""; 42 | } 43 | 44 | public static async Task GetLatestReleaseLinkAsync(string repo, CancellationToken t) 45 | { 46 | return GetBrowserDownloadUrl(await WebHelper.DownloadUrlAsStringAsync(String.Format(GIT_URL_REPO_LATEST, repo), t)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Core/Util/NativeLibraryHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace DivinityModManager.Util; 4 | public static partial class NativeLibraryHelper 5 | { 6 | [LibraryImport("kernel32", EntryPoint = "LoadLibraryA")] 7 | public static partial nint LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName); 8 | } -------------------------------------------------------------------------------- /src/Core/Util/PropertyConverters.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace DivinityModManager.Util; 4 | 5 | public static class PropertyConverters 6 | { 7 | public static Visibility BoolToVisibility(bool b) => b ? Visibility.Visible : Visibility.Collapsed; 8 | public static Visibility BoolToVisibilityReversed(bool b) => !b ? Visibility.Visible : Visibility.Collapsed; 9 | public static Visibility BoolTupleToVisibility(ValueTuple b) => b.Item1 || b.Item2 || b.Item3 || b.Item4 || b.Item5 ? Visibility.Visible : Visibility.Collapsed; 10 | /// 11 | /// Visible if not null or empty, otherwise collapsed. 12 | /// 13 | /// 14 | /// 15 | public static Visibility StringToVisibility(string str, Visibility fallback = Visibility.Collapsed) => !String.IsNullOrEmpty(str) ? Visibility.Visible : fallback; 16 | public static Visibility StringToVisibility(string str) => StringToVisibility(str, Visibility.Collapsed); 17 | public static Visibility StringToVisibilityReversed(string str, Visibility fallback = Visibility.Collapsed) => String.IsNullOrEmpty(str) ? Visibility.Visible : fallback; 18 | public static Visibility StringToVisibilityReversed(string str) => StringToVisibilityReversed(str, Visibility.Collapsed); 19 | public static Visibility UriToVisibility(Uri uri) => !String.IsNullOrEmpty(uri?.ToString()) ? Visibility.Visible : Visibility.Collapsed; 20 | public static Visibility IntToVisibility(int i) => i > 0 ? Visibility.Visible : Visibility.Collapsed; 21 | } 22 | -------------------------------------------------------------------------------- /src/Core/Util/RecycleBinHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualBasic.FileIO; 2 | 3 | using System.Runtime.InteropServices; 4 | 5 | namespace DivinityModManager.Util; 6 | 7 | public static class RecycleBinHelper 8 | { 9 | // Source: http://csharphelper.com/blog/2015/07/manage-the-recycle-bin-wastebasket-in-c/ 10 | // Structure used by SHQueryRecycleBin. 11 | [StructLayout(LayoutKind.Sequential)] 12 | private struct SHQUERYRBINFO 13 | { 14 | public int cbSize; 15 | public long i64Size; 16 | public long i64NumItems; 17 | } 18 | 19 | // Get information from recycle bin. 20 | [DllImport("shell32.dll")] 21 | private static extern int SHQueryRecycleBin(string pszRootPath, 22 | ref SHQUERYRBINFO pSHQueryRBInfo); 23 | 24 | // Empty the recycle bin. 25 | [DllImport("shell32.dll")] 26 | static extern int SHEmptyRecycleBin(IntPtr hWnd, 27 | string pszRootPath, uint dwFlags); 28 | 29 | // Return the number of items in the recycle bin. 30 | 31 | // Note: In Windows 2000, you need to supply the root 32 | // directory to the call to SHQueryRecycleBin so to get 33 | // the total number of files in the recycle you must add 34 | // up the results for each disk. See: 35 | // http://www.pinvoke.net/default.aspx/shell32/SHQueryRecycleBin.html 36 | public static int NumberOfFilesInRecycleBin() 37 | { 38 | SHQUERYRBINFO sqrbi = new SHQUERYRBINFO(); 39 | sqrbi.cbSize = Marshal.SizeOf(typeof(SHQUERYRBINFO)); 40 | int hresult = SHQueryRecycleBin(string.Empty, ref sqrbi); 41 | return (int)sqrbi.i64NumItems; 42 | } 43 | 44 | // Delete a file or move it to the recycle bin. 45 | public static bool DeleteFile(string filename, bool confirm, bool deletePermanently = false) 46 | { 47 | UIOption uiDisplayOptions = confirm ? UIOption.AllDialogs : UIOption.OnlyErrorDialogs; 48 | RecycleOption reyclingOptions = deletePermanently ? RecycleOption.DeletePermanently : RecycleOption.SendToRecycleBin; 49 | 50 | try 51 | { 52 | Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(filename, uiDisplayOptions, reyclingOptions); 53 | return true; 54 | } 55 | catch (Exception ex) 56 | { 57 | DivinityApp.Log("Error deleting file.\n" + ex.ToString()); 58 | } 59 | return false; 60 | } 61 | 62 | // Empty the wastebasket. 63 | [Flags] 64 | private enum RecycleFlags : uint 65 | { 66 | SHERB_NOCONFIRMATION = 0x1, 67 | SHERB_NOPROGRESSUI = 0x2, 68 | SHERB_NOSOUND = 0x4 69 | } 70 | public static void EmptyWastebasket(bool show_progress, 71 | bool play_sound, bool confirm) 72 | { 73 | RecycleFlags options = 0; 74 | if (!show_progress) options = 75 | options | RecycleFlags.SHERB_NOPROGRESSUI; 76 | if (!play_sound) options = 77 | options | RecycleFlags.SHERB_NOSOUND; 78 | if (!confirm) options = 79 | options | RecycleFlags.SHERB_NOCONFIRMATION; 80 | 81 | try 82 | { 83 | SHEmptyRecycleBin(IntPtr.Zero, null, (uint)options); 84 | } 85 | catch (Exception ex) 86 | { 87 | DivinityApp.Log("Error emptying wastebasket.\n" + ex.ToString()); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Core/Util/TempFile.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Util; 2 | 3 | public class TempFile : IDisposable 4 | { 5 | private readonly FileStream _stream; 6 | private readonly string _path; 7 | private readonly string _sourcePath; 8 | 9 | private readonly int _bufferSize; 10 | 11 | public FileStream Stream => _stream; 12 | public string FilePath => _path; 13 | public string SourceFilePath => _sourcePath; 14 | 15 | //128 KB since we're using asynchronous streams, default is 4 KB 16 | private TempFile(string sourcePath, int bufferSize = 128000) 17 | { 18 | _bufferSize = bufferSize; 19 | var tempDir = DivinityApp.GetAppDirectory("Temp"); 20 | Directory.CreateDirectory(tempDir); 21 | _path = Path.Join(tempDir, Path.GetFileName(sourcePath)); 22 | _sourcePath = sourcePath; 23 | _stream = File.Create(_path, _bufferSize, FileOptions.Asynchronous | FileOptions.DeleteOnClose); 24 | } 25 | 26 | public static async Task CreateAsync(string sourcePath, CancellationToken token) 27 | { 28 | var temp = new TempFile(sourcePath); 29 | await temp.CopyAsync(token); 30 | return temp; 31 | } 32 | 33 | public static async Task CreateAsync(string sourcePath, Stream sourceStream, CancellationToken token) 34 | { 35 | var temp = new TempFile(sourcePath); 36 | await temp.CopyAsync(sourceStream, token); 37 | return temp; 38 | } 39 | 40 | private async Task CopyAsync(CancellationToken token) 41 | { 42 | using var sourceStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true); 43 | await sourceStream.CopyToAsync(_stream, _bufferSize, token); 44 | } 45 | 46 | private async Task CopyAsync(Stream sourceStream, CancellationToken token) 47 | { 48 | await sourceStream.CopyToAsync(_stream, _bufferSize, token); 49 | } 50 | 51 | public void Dispose() 52 | { 53 | _stream?.Dispose(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Core/Util/WebHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace DivinityModManager.Util; 6 | 7 | public static class WebHelper 8 | { 9 | private static HttpClient Client => Services.Get(); 10 | 11 | public static Task GetAsync([StringSyntax("Uri")] string? requestUri) => Client.GetAsync(requestUri); 12 | public static Task GetAsync([StringSyntax("Uri")] string? requestUri, CancellationToken cancellationToken) => Client.GetAsync(requestUri, cancellationToken); 13 | public static Task GetAsync([StringSyntax("Uri")] string? requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken) => Client.GetAsync(requestUri, completionOption, cancellationToken); 14 | 15 | public static Task PostAsync([StringSyntax("Uri")] string? requestUri, HttpContent? content) => Client.PostAsync(requestUri, content); 16 | public static Task PostAsync([StringSyntax("Uri")] string? requestUri, HttpContent? content, CancellationToken token) => Client.PostAsync(requestUri, content, token); 17 | 18 | public static async Task DownloadFileAsStreamAsync(string downloadUrl, CancellationToken token) 19 | { 20 | try 21 | { 22 | var fileStream = await Client.GetStreamAsync(downloadUrl, token); 23 | return fileStream; 24 | } 25 | catch (Exception ex) 26 | { 27 | DivinityApp.Log($"Error downloading url ({downloadUrl}):\n{ex}"); 28 | return Stream.Null; 29 | } 30 | } 31 | 32 | public static async Task DownloadUrlAsStringAsync(string downloadUrl, CancellationToken token) 33 | { 34 | try 35 | { 36 | var result = await Client.GetStringAsync(downloadUrl, token); 37 | return result; 38 | } 39 | catch (Exception ex) 40 | { 41 | DivinityApp.Log($"Error downloading url ({downloadUrl}):\n{ex}"); 42 | } 43 | return String.Empty; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Core/ViewModels/BaseHistoryViewModel.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Models; 2 | 3 | using Reactive.Bindings.Extensions; 4 | 5 | using ReactiveHistory; 6 | 7 | using System.Windows.Input; 8 | 9 | namespace DivinityModManager.ViewModels; 10 | 11 | public interface IHistoryViewModel 12 | { 13 | IHistory History { get; } 14 | 15 | void Undo(); 16 | void Redo(); 17 | } 18 | 19 | public static class HistoryViewModelExtensions 20 | { 21 | public static void ChangeListWithHistory(this IHistoryViewModel vm, IList source, IList oldValue, IList newValue) 22 | { 23 | if (vm.History != null) 24 | { 25 | void undo() => source = oldValue; 26 | void redo() => source = newValue; 27 | vm.History.Snapshot(undo, redo); 28 | source = newValue; 29 | } 30 | } 31 | 32 | public static void AddWithHistory(this IHistoryViewModel vm, IList source, T item) 33 | { 34 | if (vm.History != null) 35 | { 36 | int index = source.Count; 37 | void redo() => source.Insert(index, item); 38 | void undo() => source.RemoveAt(index); 39 | vm.History.Snapshot(undo, redo); 40 | redo(); 41 | } 42 | } 43 | 44 | public static void RemoveWithHistory(this IHistoryViewModel vm, IList source, T item) 45 | { 46 | if (vm.History != null) 47 | { 48 | int index = source.IndexOf(item); 49 | void redo() => source.RemoveAt(index); 50 | void undo() => source.Insert(index, item); 51 | vm.History.Snapshot(undo, redo); 52 | redo(); 53 | } 54 | } 55 | 56 | public static void CreateSnapshot(this IHistoryViewModel vm, Action undo, Action redo) 57 | { 58 | vm.History?.Snapshot(undo, redo); 59 | } 60 | } 61 | 62 | public abstract class BaseHistoryViewModel : BaseHistoryObject, IHistoryViewModel, IDisposable 63 | { 64 | public CompositeDisposable Disposables { get; internal set; } 65 | 66 | public ICommand UndoCommand { get; set; } 67 | public ICommand RedoCommand { get; set; } 68 | public ICommand ClearHistoryCommand { get; set; } 69 | 70 | public void Dispose() 71 | { 72 | this.Disposables?.Dispose(); 73 | } 74 | 75 | public void Undo() 76 | { 77 | History.Undo(); 78 | } 79 | 80 | public void Redo() 81 | { 82 | History.Redo(); 83 | } 84 | 85 | public BaseHistoryViewModel() 86 | { 87 | Disposables = new CompositeDisposable(); 88 | 89 | var history = new StackHistory().AddTo(Disposables); 90 | History = history; 91 | 92 | var undo = ReactiveCommand.Create(Undo, History.CanUndo); 93 | undo.Subscribe().DisposeWith(this.Disposables); 94 | UndoCommand = undo; 95 | 96 | var redo = ReactiveCommand.Create(Redo, History.CanRedo); 97 | redo.Subscribe().DisposeWith(this.Disposables); 98 | RedoCommand = redo; 99 | 100 | var clear = ReactiveCommand.Create(History.Clear, History.CanClear); 101 | clear.Subscribe().DisposeWith(this.Disposables); 102 | ClearHistoryCommand = clear; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Core/ViewModels/BaseViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.ViewModels; 2 | 3 | public class BaseViewModel : ReactiveObject, IDisposable 4 | { 5 | public CompositeDisposable Disposables { get; private set; } 6 | 7 | public void Dispose() 8 | { 9 | this.Disposables?.Dispose(); 10 | } 11 | 12 | public BaseViewModel() 13 | { 14 | Disposables = new CompositeDisposable(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Core/ViewModels/IDivinityAppViewModel.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Models; 2 | 3 | using DynamicData.Binding; 4 | 5 | namespace DivinityModManager.ViewModels; 6 | 7 | public interface IDivinityAppViewModel 8 | { 9 | IEnumerable ActiveMods { get; } 10 | IEnumerable InactiveMods { get; } 11 | ObservableCollectionExtended Profiles { get; } 12 | ReadOnlyObservableCollection Mods { get; } 13 | 14 | bool IsDragging { get; } 15 | bool IsRefreshing { get; } 16 | bool IsLocked { get; } 17 | 18 | int ActiveSelected { get; } 19 | int InactiveSelected { get; } 20 | 21 | void ShowAlert(string message, AlertType alertType = AlertType.Info, int timeout = 0); 22 | void DeleteMod(DivinityModData mod); 23 | void ClearMissingMods(); 24 | void AddActiveMod(DivinityModData mod); 25 | void RemoveActiveMod(DivinityModData mod); 26 | } 27 | -------------------------------------------------------------------------------- /src/GUI/App.xaml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/GUI/App.xaml.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | using DivinityModManager.AppServices; 4 | using DivinityModManager.Util; 5 | using DivinityModManager.ViewModels; 6 | using DivinityModManager.Views; 7 | 8 | using AutoUpdaterDotNET; 9 | 10 | using System.Globalization; 11 | using System.Net.Http; 12 | using System.Windows; 13 | using System.Windows.Input; 14 | using System.Windows.Markup; 15 | 16 | namespace DivinityModManager; 17 | 18 | /// 19 | /// Interaction logic for App.xaml 20 | /// 21 | public partial class App : Application 22 | { 23 | public SplashScreen Splash { get; set; } 24 | 25 | public App() 26 | { 27 | Services.RegisterSingleton(new FileWatcherService()); 28 | Services.RegisterSingleton(new ScreenReaderService()); 29 | 30 | var client = new HttpClient(); 31 | client.DefaultRequestHeaders.Add("User-Agent", AppDomain.CurrentDomain.FriendlyName); 32 | Services.RegisterSingleton(client); 33 | 34 | var appUpdateVM = new AppUpdateWindowViewModel(); 35 | 36 | AutoUpdater.HttpUserAgent = "BG3ModManagerUser"; 37 | AutoUpdater.RunUpdateAsAdmin = false; 38 | AutoUpdater.Synchronous = false; 39 | AutoUpdater.CheckForUpdateEvent += (e) => 40 | { 41 | RxApp.TaskpoolScheduler.Schedule(() => 42 | { 43 | appUpdateVM.OnUpdateCheckCommand.Execute(e).Subscribe(); 44 | }); 45 | }; 46 | 47 | Services.RegisterSingleton(appUpdateVM); 48 | 49 | // POCO type warning suppression 50 | Services.Register(() => new DivinityModManager.Util.CustomPropertyResolver()); 51 | #if DEBUG 52 | RxApp.SuppressViewCommandBindingMessage = false; 53 | #else 54 | RxApp.DefaultExceptionHandler = new RxExceptionHandler(); 55 | RxApp.SuppressViewCommandBindingMessage = true; 56 | #endif 57 | } 58 | 59 | protected override void OnStartup(StartupEventArgs e) 60 | { 61 | base.OnStartup(e); 62 | 63 | //For making date display use the current system's culture 64 | FrameworkElement.LanguageProperty.OverrideMetadata( 65 | typeof(FrameworkElement), 66 | new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag))); 67 | 68 | EventManager.RegisterClassHandler(typeof(Window), Window.PreviewMouseDownEvent, new MouseButtonEventHandler(OnPreviewMouseDown)); 69 | 70 | var splashFade = new System.Threading.Thread(() => 71 | { 72 | Splash.Close(TimeSpan.FromSeconds(1)); 73 | }); 74 | 75 | var mainWindow = new MainWindow(); 76 | splashFade.Start(); 77 | mainWindow.Show(); 78 | } 79 | 80 | private static void OnPreviewMouseDown(object sender, MouseButtonEventArgs e) 81 | { 82 | DivinityApp.IsKeyboardNavigating = false; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/GUI/AppCommands.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | 3 | namespace DivinityModManager; 4 | 5 | public static class AppCommands 6 | { 7 | public static ReactiveCommand Clear { get; } 8 | 9 | private static void ClearText(object sender) 10 | { 11 | if (sender is MenuItem menuItem) 12 | { 13 | var cm = menuItem.FindVisualParent(); 14 | if (cm != null && cm.PlacementTarget is TextBox tb) 15 | { 16 | tb.Clear(); 17 | } 18 | } 19 | else if (sender is TextBox textBox) 20 | { 21 | textBox.Clear(); 22 | } 23 | else if (sender is ContextMenu cm) 24 | { 25 | if (cm.PlacementTarget is TextBox tb) 26 | { 27 | tb.Clear(); 28 | } 29 | } 30 | } 31 | 32 | static AppCommands() 33 | { 34 | Clear = ReactiveCommand.Create(ClearText); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/GUI/BG3ModManager.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/BG3ModManager.ico -------------------------------------------------------------------------------- /src/GUI/Controls/AutomationTooltip.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Util; 2 | 3 | using System.Windows.Automation.Peers; 4 | using System.Windows.Controls; 5 | 6 | namespace DivinityModManager.Controls; 7 | 8 | public class AutomationTooltip : ToolTip 9 | { 10 | protected override AutomationPeer OnCreateAutomationPeer() 11 | { 12 | return new AutomationTooltipPeer(this); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/GUI/Controls/Behavior/TextBlockSettingsEntryAttributeBehavior.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | 5 | namespace DivinityModManager.Controls.Behavior; 6 | 7 | public static class TextBlockSettingsEntryAttributeBehavior 8 | { 9 | public static readonly DependencyProperty PropertyProperty = 10 | DependencyProperty.RegisterAttached( 11 | "Property", 12 | typeof(string), 13 | typeof(TextBlockSettingsEntryAttributeBehavior), 14 | new UIPropertyMetadata("", OnPropertySet)); 15 | 16 | public static readonly DependencyProperty TargetTypeProperty = 17 | DependencyProperty.RegisterAttached( 18 | "TargetType", 19 | typeof(Type), 20 | typeof(TextBlockSettingsEntryAttributeBehavior), 21 | new UIPropertyMetadata(null, OnTargetTypeSet)); 22 | 23 | public static string GetProperty(DependencyObject element) 24 | { 25 | return (string)element.GetValue(PropertyProperty); 26 | } 27 | 28 | public static void SetProperty(DependencyObject element, string value) 29 | { 30 | element.SetValue(PropertyProperty, value); 31 | } 32 | 33 | public static Type GetTargetType(DependencyObject element) 34 | { 35 | return (Type)element.GetValue(TargetTypeProperty); 36 | } 37 | 38 | public static void SetTargetType(DependencyObject element, Type value) 39 | { 40 | element.SetValue(TargetTypeProperty, value); 41 | } 42 | 43 | private static void UpdateElement(TextBlock element, string propName = "", Type targetType = null) 44 | { 45 | if (targetType == null) targetType = GetTargetType(element); 46 | if (String.IsNullOrEmpty(propName)) propName = GetProperty(element); 47 | if (targetType != null && !String.IsNullOrEmpty(propName)) 48 | { 49 | PropertyInfo prop = targetType.GetProperty(propName); 50 | SettingsEntryAttribute settingsEntry = prop.GetCustomAttribute(); 51 | if (settingsEntry != null) 52 | { 53 | element.Text = settingsEntry.DisplayName; 54 | element.ToolTip = settingsEntry.Tooltip; 55 | } 56 | } 57 | } 58 | 59 | static void OnPropertySet(DependencyObject sender, DependencyPropertyChangedEventArgs e) 60 | { 61 | if (sender is TextBlock element && e.NewValue is string propName) 62 | { 63 | UpdateElement(element, propName); 64 | } 65 | } 66 | 67 | static void OnTargetTypeSet(DependencyObject sender, DependencyPropertyChangedEventArgs e) 68 | { 69 | if (sender is TextBlock element && e.NewValue is Type type) 70 | { 71 | UpdateElement(element, "", type); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/GUI/Controls/Behavior/ToolTipHelperBehavior.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace DivinityModManager.Controls.Behavior; 4 | 5 | public static class ToolTipHelperBehavior 6 | { 7 | public static bool GetDisableMouseEvents(DependencyObject element) 8 | { 9 | return (bool)element.GetValue(DisableMouseEventsProperty); 10 | } 11 | 12 | public static void SetDisableMouseEvents(DependencyObject element, bool value) 13 | { 14 | element.SetValue(DisableMouseEventsProperty, value); 15 | } 16 | 17 | public static readonly DependencyProperty DisableMouseEventsProperty = 18 | DependencyProperty.RegisterAttached( 19 | "DisableMouseEvents", 20 | typeof(bool), 21 | typeof(ScreenReaderHelperBehavior), 22 | new UIPropertyMetadata(false, OnDisableMouseEvents)); 23 | 24 | static void OnDisableMouseEvents(DependencyObject depObj, DependencyPropertyChangedEventArgs e) 25 | { 26 | if (depObj is UIElement element) 27 | { 28 | if ((bool)e.NewValue == true) 29 | { 30 | element.PreviewMouseDown += OnPreviewMouseDown; 31 | } 32 | else 33 | { 34 | element.PreviewMouseDown -= OnPreviewMouseDown; 35 | } 36 | } 37 | } 38 | 39 | private static void OnPreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) 40 | { 41 | e.Handled = true; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/GUI/Controls/BusyIndicator.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | 3 | namespace DivinityModManager.Controls; 4 | 5 | /// 6 | /// Interaction logic for BusyIndicator.xaml 7 | /// 8 | public partial class BusyIndicator : UserControl 9 | { 10 | public BusyIndicator() 11 | { 12 | InitializeComponent(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/GUI/Controls/CircleDecorator.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using System.Windows.Media; 4 | 5 | namespace DivinityModManager.Controls; 6 | 7 | public class CircleDecorator : Border 8 | { 9 | protected override void OnRender(System.Windows.Media.DrawingContext drawingContext) 10 | { 11 | Double width = this.ActualWidth; 12 | Double height = this.ActualHeight; 13 | Double a = width / 2; 14 | Double b = height / 2; 15 | Point centerPoint = new Point(a, b); 16 | Double thickness = this.BorderThickness.Left; 17 | EllipseGeometry ellipse = new EllipseGeometry(centerPoint, a, b); 18 | drawingContext.PushClip(ellipse); 19 | drawingContext.DrawGeometry( 20 | this.Background, 21 | new Pen(this.BorderBrush, thickness), 22 | ellipse); 23 | } 24 | 25 | protected override Size MeasureOverride(Size constraint) 26 | { 27 | return base.MeasureOverride(constraint); 28 | } 29 | 30 | protected override Size ArrangeOverride(Size finalSize) 31 | { 32 | Double a = finalSize.Width / 2; 33 | Double b = finalSize.Height / 2; 34 | Double PI = 3.1415926; 35 | Double x = a * Math.Cos(45 * PI / 180); 36 | Double y = b * Math.Sin(45 * PI / 180); 37 | Rect rect = new Rect(new Point(a - x, b - y), new Point(a + x, b + y)); 38 | if (base.Child != null) 39 | { 40 | base.Child.Arrange(rect); 41 | } 42 | 43 | return finalSize; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/GUI/Controls/Extensions/DependencyExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2021 Charlie Brown 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | Source: https://github.com/carbonrobot/wpf-autogrid 25 | */ 26 | 27 | using System.Windows; 28 | 29 | namespace DivinityModManager.Controls.Extensions; 30 | 31 | public static class DependencyExtensions 32 | { 33 | /// 34 | /// Sets the value of the only if it hasn't been explicitly set. 35 | /// 36 | public static bool SetIfDefault(this DependencyObject o, DependencyProperty property, T value) 37 | { 38 | if (DependencyPropertyHelper.GetValueSource(o, property).BaseValueSource == BaseValueSource.Default) 39 | { 40 | o.SetValue(property, value); 41 | 42 | return true; 43 | } 44 | 45 | return false; 46 | } 47 | } -------------------------------------------------------------------------------- /src/GUI/Controls/Extensions/EnumExtension.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Models.View; 2 | 3 | using System.ComponentModel; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Reflection; 6 | using System.Windows.Markup; 7 | 8 | namespace DivinityModManager.Controls.Extensions; 9 | 10 | public class EnumExtension : MarkupExtension 11 | { 12 | private Type _enumType; 13 | 14 | public EnumExtension(Type enumType) 15 | { 16 | if (enumType == null) 17 | throw new ArgumentNullException("enumType"); 18 | 19 | EnumType = enumType; 20 | } 21 | 22 | public Type EnumType 23 | { 24 | get { return _enumType; } 25 | private set 26 | { 27 | if (_enumType == value) 28 | return; 29 | 30 | var enumType = Nullable.GetUnderlyingType(value) ?? value; 31 | 32 | if (enumType.IsEnum == false) 33 | throw new ArgumentException("Type must be an Enum."); 34 | 35 | _enumType = value; 36 | } 37 | } 38 | 39 | public override object ProvideValue(IServiceProvider serviceProvider) 40 | { 41 | var enumValues = Enum.GetValues(EnumType); 42 | 43 | var result = new List(); 44 | 45 | foreach(var enumValue in enumValues) 46 | { 47 | var entry = new EnumEntry() 48 | { 49 | Value = enumValue 50 | }; 51 | var field = EnumType.GetField(enumValue.ToString()); 52 | var descriptionAttribute = field.GetCustomAttribute(false); 53 | 54 | if (descriptionAttribute == null) 55 | { 56 | var displayAttribute = field.GetCustomAttribute(false); 57 | if (displayAttribute != null) 58 | { 59 | entry.Name = displayAttribute.Name; 60 | entry.Description = displayAttribute.Description; 61 | } 62 | } 63 | else 64 | { 65 | entry.Description = descriptionAttribute.Description; 66 | } 67 | 68 | result.Add(entry); 69 | } 70 | 71 | return result; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/GUI/Controls/Extensions/HyperlinkExtensions.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Util; 2 | 3 | using System.Diagnostics; 4 | using System.Windows; 5 | using System.Windows.Documents; 6 | 7 | namespace DivinityModManager.Controls.Extensions; 8 | 9 | /// 10 | /// Source: https://stackoverflow.com/a/11433814 11 | /// 12 | public static class HyperlinkExtensions 13 | { 14 | public static bool GetIsExternal(DependencyObject obj) 15 | { 16 | return (bool)obj.GetValue(IsExternalProperty); 17 | } 18 | 19 | public static void SetIsExternal(DependencyObject obj, bool value) 20 | { 21 | obj.SetValue(IsExternalProperty, value); 22 | } 23 | public static readonly DependencyProperty IsExternalProperty = 24 | DependencyProperty.RegisterAttached("IsExternal", typeof(bool), typeof(HyperlinkExtensions), new UIPropertyMetadata(false, OnIsExternalChanged)); 25 | 26 | private static void OnIsExternalChanged(object sender, DependencyPropertyChangedEventArgs args) 27 | { 28 | var hyperlink = sender as Hyperlink; 29 | 30 | if ((bool)args.NewValue) 31 | hyperlink.RequestNavigate += Hyperlink_RequestNavigate; 32 | else 33 | hyperlink.RequestNavigate -= Hyperlink_RequestNavigate; 34 | } 35 | 36 | private static void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) 37 | { 38 | ProcessHelper.TryOpenUrl(e.Uri.AbsoluteUri); 39 | e.Handled = true; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/GUI/Controls/HotkeyEditorControl.xaml: -------------------------------------------------------------------------------- 1 |  15 | 16 | 49 | 50 | 51 | 52 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/GUI/Controls/HyperlinkText.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/GUI/Controls/HyperlinkText.xaml.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Util; 2 | 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | using System.Windows.Documents; 6 | using System.Windows.Navigation; 7 | 8 | namespace DivinityModManager.Controls; 9 | 10 | /// 11 | /// Interaction logic for HyperlinkText.xaml 12 | /// 13 | public partial class HyperlinkText : TextBlock 14 | { 15 | public string URL 16 | { 17 | get { return (string)GetValue(URLProperty); } 18 | set { SetValue(URLProperty, value); } 19 | } 20 | 21 | // Using a DependencyProperty as the backing store for URL. This enables animation, styling, binding, etc... 22 | public static readonly DependencyProperty URLProperty = 23 | DependencyProperty.Register("URL", typeof(string), typeof(HyperlinkText), new FrameworkPropertyMetadata("", new PropertyChangedCallback(OnURLChanged))); 24 | 25 | private static void OnURLChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 26 | { 27 | string url = (string)e.NewValue; 28 | Uri uri = null; 29 | if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri)) 30 | { 31 | HyperlinkText x = (HyperlinkText)d; 32 | x.Hyperlink.NavigateUri = uri; 33 | x.ToolTip = url; 34 | if (String.IsNullOrEmpty(x.DisplayText) || x.UseUrlForDisplayText) 35 | { 36 | x.DisplayText = url; 37 | } 38 | } 39 | } 40 | 41 | public string DisplayText 42 | { 43 | get { return (string)GetValue(DisplayTextProperty); } 44 | set { SetValue(DisplayTextProperty, value); } 45 | } 46 | 47 | // Using a DependencyProperty as the backing store for DisplayText. This enables animation, styling, binding, etc... 48 | public static readonly DependencyProperty DisplayTextProperty = 49 | DependencyProperty.Register("DisplayText", typeof(string), typeof(HyperlinkText), new PropertyMetadata("")); 50 | 51 | public bool UseUrlForDisplayText 52 | { 53 | get { return (bool)GetValue(UseUrlForDisplayTextProperty); } 54 | set { SetValue(UseUrlForDisplayTextProperty, value); } 55 | } 56 | 57 | public static readonly DependencyProperty UseUrlForDisplayTextProperty = 58 | DependencyProperty.Register("UseUrlForDisplayText", typeof(bool), typeof(HyperlinkText), new PropertyMetadata(false)); 59 | 60 | public HyperlinkText() 61 | { 62 | InitializeComponent(); 63 | 64 | Hyperlink.RequestNavigate += Hyperlink_RequestNavigate; 65 | } 66 | 67 | private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) 68 | { 69 | ProcessHelper.TryOpenUrl(e.Uri.AbsoluteUri); 70 | e.Handled = true; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/GUI/Controls/ModEntryGrid.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Util.ScreenReader; 2 | 3 | using System.Windows.Automation.Peers; 4 | using System.Windows.Controls; 5 | 6 | namespace DivinityModManager.Controls; 7 | 8 | public class ModEntryGrid : Grid 9 | { 10 | public ModEntryGrid() : base() { } 11 | 12 | protected override AutomationPeer OnCreateAutomationPeer() 13 | { 14 | return new ModEntryGridAutomationPeer(this); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/GUI/Controls/SelectableTextBlock.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using System.Windows.Documents; 4 | using System.Windows.Input; 5 | using System.Windows.Media; 6 | 7 | namespace DivinityModManager.Controls; 8 | 9 | // Source: https://stackoverflow.com/a/32870521 10 | public partial class SelectableTextBlock : TextBlock 11 | { 12 | TextPointer StartSelectPosition; 13 | TextPointer EndSelectPosition; 14 | public String SelectedText = ""; 15 | 16 | public delegate void TextSelectedHandler(string SelectedText); 17 | public event TextSelectedHandler TextSelected; 18 | 19 | protected override void OnMouseDown(MouseButtonEventArgs e) 20 | { 21 | base.OnMouseDown(e); 22 | Point mouseDownPoint = e.GetPosition(this); 23 | StartSelectPosition = this.GetPositionFromPoint(mouseDownPoint, true); 24 | } 25 | 26 | protected override void OnMouseUp(MouseButtonEventArgs e) 27 | { 28 | base.OnMouseUp(e); 29 | Point mouseUpPoint = e.GetPosition(this); 30 | EndSelectPosition = this.GetPositionFromPoint(mouseUpPoint, true); 31 | 32 | TextRange otr = new TextRange(this.ContentStart, this.ContentEnd); 33 | otr.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.GreenYellow)); 34 | 35 | TextRange ntr = new TextRange(StartSelectPosition, EndSelectPosition); 36 | ntr.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.White)); 37 | 38 | SelectedText = ntr.Text; 39 | if (!(TextSelected == null)) 40 | { 41 | TextSelected(SelectedText); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/GUI/Controls/TemplateSelectors/TagTemplateSelector.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | 4 | namespace DivinityModManager.Controls.TemplateSelectors; 5 | 6 | public class TagTemplateSelector : DataTemplateSelector 7 | { 8 | private string[] modeValues = { "Story", "GM", "Arena" }; 9 | private string[] defaultWorkshopTags = { "Armors", "Balancing/Stats", "Classes", "Companions", "Consumables", "Maps", "Origins", "Overhauls", "Quality of Life", "Quests", "Races", "Runes/Boosts", "Skills", "Utility", "Visual Overrides", "Weapons" }; 10 | 11 | public override DataTemplate SelectTemplate(object item, DependencyObject container) 12 | { 13 | FrameworkElement element = container as FrameworkElement; 14 | 15 | if (element != null && item != null && item is string tag) 16 | { 17 | if (modeValues.Any(x => x.Equals(tag, StringComparison.OrdinalIgnoreCase))) 18 | { 19 | return element.FindResource("ModeTagTemplate") as DataTemplate; 20 | } 21 | else if (defaultWorkshopTags.Any(x => x.Equals(tag, StringComparison.OrdinalIgnoreCase))) 22 | { 23 | return element.FindResource("TagTemplate") as DataTemplate; 24 | } 25 | else 26 | { 27 | return element.FindResource("CustomTagTemplate") as DataTemplate; 28 | } 29 | } 30 | 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/GUI/Controls/UnfocusableTextBox.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using System.Windows.Data; 4 | using System.Windows.Input; 5 | 6 | namespace DivinityModManager.Controls; 7 | 8 | public class UnfocusableTextBox : TextBox 9 | { 10 | public bool CanUndoTextOnEscape 11 | { 12 | get { return (bool)GetValue(CanUndoTextOnEscapeProperty); } 13 | set { SetValue(CanUndoTextOnEscapeProperty, value); } 14 | } 15 | 16 | public static readonly DependencyProperty CanUndoTextOnEscapeProperty = 17 | DependencyProperty.Register("CanUndoTextOnEscape", 18 | typeof(bool), typeof(UnfocusableTextBox), 19 | new PropertyMetadata(false)); 20 | 21 | public bool UpdateBindingOnFocusLost 22 | { 23 | get { return (bool)GetValue(UpdateBindingOnFocusLostProperty); } 24 | set { SetValue(UpdateBindingOnFocusLostProperty, value); } 25 | } 26 | 27 | public static readonly DependencyProperty UpdateBindingOnFocusLostProperty = 28 | DependencyProperty.Register("UpdateBindingOnFocusLost", 29 | typeof(bool), typeof(UnfocusableTextBox), 30 | new PropertyMetadata(false)); 31 | 32 | public UnfocusableTextBox() 33 | { 34 | 35 | } 36 | 37 | private string lastText = ""; 38 | 39 | protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e) 40 | { 41 | base.OnGotKeyboardFocus(e); 42 | lastText = Text; 43 | } 44 | 45 | protected override void OnKeyDown(KeyEventArgs e) 46 | { 47 | base.OnKeyDown(e); 48 | if (e.Key == Key.Return) 49 | { 50 | //Keyboard.ClearFocus(); 51 | MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); 52 | lastText = Text; 53 | if (UpdateBindingOnFocusLost) 54 | { 55 | var bindingExpression = BindingOperations.GetBindingExpression(this, TextBox.TextProperty); 56 | if (bindingExpression != null) 57 | { 58 | bindingExpression.UpdateSource(); 59 | } 60 | } 61 | e.Handled = true; 62 | } 63 | else if (e.Key == Key.Escape && CanUndoTextOnEscape) 64 | { 65 | if (CanUndoTextOnEscape && Text != lastText) Text = lastText; 66 | //Keyboard.ClearFocus(); 67 | MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); 68 | e.Handled = true; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/GUI/Converters/BoolToVisibilityConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows; 3 | using System.Windows.Data; 4 | 5 | namespace DivinityModManager.Converters; 6 | 7 | public class BoolToVisibilityConverter : IValueConverter 8 | { 9 | public static Visibility FromBool(bool b) => b ? Visibility.Visible : Visibility.Collapsed; 10 | 11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 12 | { 13 | bool reverse = false; 14 | if (parameter != null) 15 | { 16 | if (parameter is int reverseInt) 17 | { 18 | reverse = reverseInt > 0; 19 | } 20 | else if (parameter is bool r) 21 | { 22 | reverse = r; 23 | } 24 | DivinityApp.Log($"BoolToVisibilityConverter param: {parameter} | {parameter.GetType()}"); 25 | } 26 | 27 | if (value is bool b) 28 | { 29 | if (!reverse) 30 | { 31 | return b ? Visibility.Visible : Visibility.Collapsed; 32 | } 33 | else 34 | { 35 | return !b ? Visibility.Visible : Visibility.Collapsed; 36 | } 37 | } 38 | return Visibility.Visible; 39 | } 40 | 41 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 42 | { 43 | if (value is Visibility visbility) 44 | { 45 | if (visbility == Visibility.Visible) 46 | { 47 | return true; 48 | } 49 | } 50 | return false; 51 | } 52 | } 53 | 54 | public class BoolToVisibilityConverterReversed : IValueConverter 55 | { 56 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 57 | { 58 | if (value is bool b) 59 | { 60 | return !b ? Visibility.Visible : Visibility.Collapsed; 61 | } 62 | return Visibility.Collapsed; 63 | } 64 | 65 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 66 | { 67 | if (value is Visibility visbility) 68 | { 69 | if (visbility == Visibility.Visible) 70 | { 71 | return false; 72 | } 73 | } 74 | return true; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/GUI/Converters/EnumToStringConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Data; 2 | 3 | namespace DivinityModManager.Converters; 4 | 5 | class DivinityGameLaunchWindowActionToStringConverter : IValueConverter 6 | { 7 | public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) 8 | { 9 | return value.ToString(); 10 | } 11 | 12 | public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) 13 | { 14 | return (DivinityGameLaunchWindowAction)Enum.Parse(typeof(DivinityGameLaunchWindowAction), value.ToString(), true); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/GUI/Converters/IntToVisibilityConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows; 3 | using System.Windows.Data; 4 | 5 | namespace DivinityModManager.Converters; 6 | 7 | public class IntToVisibilityConverter : IValueConverter 8 | { 9 | public static Visibility FromInt(int v) => v > 0 ? Visibility.Visible : Visibility.Collapsed; 10 | 11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 12 | { 13 | if (value is int intVal) 14 | { 15 | return intVal == 0 ? Visibility.Collapsed : Visibility.Visible; 16 | } 17 | return Visibility.Visible; 18 | } 19 | 20 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 21 | { 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/GUI/Converters/ModExistsConverter.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Models; 2 | 3 | using System.Globalization; 4 | using System.Windows.Data; 5 | 6 | namespace DivinityModManager.Converters; 7 | 8 | public class ModExistsConverter : IValueConverter 9 | { 10 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 11 | { 12 | if (value is IDivinityModData data) 13 | { 14 | var registry = Services.Get(); 15 | if (registry != null && registry.ModExists(data.UUID)) 16 | { 17 | return true; 18 | } 19 | } 20 | 21 | return false; 22 | } 23 | 24 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 25 | { 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/GUI/Converters/ModIsActiveConverter.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Models; 2 | 3 | using System.Globalization; 4 | using System.Windows.Data; 5 | 6 | namespace DivinityModManager.Converters; 7 | 8 | public class ModIsActiveConverter : IValueConverter 9 | { 10 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 11 | { 12 | if (value is IDivinityModData data) 13 | { 14 | var registry = Services.Get(); 15 | if (registry != null && registry.ModIsActive(data.UUID)) 16 | { 17 | return true; 18 | } 19 | } 20 | 21 | return false; 22 | } 23 | 24 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 25 | { 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/GUI/Converters/ModToDisplayNameConverter.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Models; 2 | 3 | using System.Globalization; 4 | using System.Windows.Data; 5 | 6 | namespace DivinityModManager.Converters; 7 | 8 | public class ModToDisplayNameConverter : IValueConverter 9 | { 10 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 11 | { 12 | if (value is ModuleShortDesc moduleDesc) 13 | { 14 | var registry = Services.Get(); 15 | if (registry != null && registry.TryGetDisplayName(moduleDesc.UUID, out var name)) 16 | { 17 | return name; 18 | } 19 | return moduleDesc.Name; 20 | } 21 | else if (value is string uuid) 22 | { 23 | var registry = Services.Get(); 24 | if (registry != null && registry.TryGetDisplayName(uuid, out var name)) 25 | { 26 | return name; 27 | } 28 | return uuid; 29 | } 30 | return ""; 31 | } 32 | 33 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 34 | { 35 | return null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/GUI/Converters/StringNotEmptyToVisibilityConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows; 3 | using System.Windows.Data; 4 | 5 | namespace DivinityModManager.Converters; 6 | 7 | public class StringNotEmptyToVisibilityConverter : IValueConverter 8 | { 9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 10 | { 11 | bool reverse = false; 12 | if (parameter != null) 13 | { 14 | if (parameter is int reverseInt) 15 | { 16 | reverse = reverseInt > 0; 17 | } 18 | else if (parameter is bool r) 19 | { 20 | reverse = r; 21 | } 22 | } 23 | 24 | if (value is string v) 25 | { 26 | if (!reverse) 27 | { 28 | return !String.IsNullOrWhiteSpace(v) ? Visibility.Visible : Visibility.Collapsed; 29 | } 30 | else 31 | { 32 | return String.IsNullOrWhiteSpace(v) ? Visibility.Visible : Visibility.Collapsed; 33 | } 34 | } 35 | return Visibility.Visible; 36 | } 37 | 38 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 39 | { 40 | return null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/GUI/Converters/StringToLinearBrushConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows.Data; 3 | using System.Windows.Media; 4 | 5 | namespace DivinityModManager.Converters; 6 | 7 | public class StringToLinearBrushConverter : IValueConverter 8 | { 9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 10 | { 11 | if (value is string str) 12 | { 13 | var startColor = (Color)ColorConverter.ConvertFromString(str); 14 | var endColor = Color.FromArgb(startColor.A, (byte)(startColor.R * 0.7), (byte)(startColor.G * 0.7), (byte)(startColor.B * 0.7)); 15 | return new LinearGradientBrush(startColor, endColor, 90.0d); 16 | } 17 | return null; 18 | } 19 | 20 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 21 | { 22 | if (value is Uri uri) 23 | { 24 | return uri.OriginalString; 25 | } 26 | return ""; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/GUI/Converters/StringToSolidBrushConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows.Data; 3 | using System.Windows.Media; 4 | 5 | namespace DivinityModManager.Converters; 6 | 7 | public class StringToSolidBrushConverter : IValueConverter 8 | { 9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 10 | { 11 | if (value is string str) 12 | { 13 | var color = (Color)ColorConverter.ConvertFromString(str); 14 | return new SolidColorBrush(color); 15 | } 16 | return null; 17 | } 18 | 19 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 20 | { 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/GUI/Converters/StringToUriConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows.Data; 3 | 4 | namespace DivinityModManager.Converters; 5 | 6 | public class StringToUriConverter : IValueConverter 7 | { 8 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 9 | { 10 | if (value is string str) 11 | { 12 | Uri result = null; 13 | if (Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out result)) 14 | { 15 | return result; 16 | } 17 | } 18 | return null; 19 | } 20 | 21 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 22 | { 23 | if (value is Uri uri) 24 | { 25 | return uri.OriginalString; 26 | } 27 | return ""; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/GUI/Converters/UriToBitmapImageConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows.Data; 3 | using System.Windows.Media.Imaging; 4 | 5 | namespace DivinityModManager.Converters; 6 | 7 | internal class UriToBitmapImageConverter : IValueConverter 8 | { 9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 10 | { 11 | if (value is Uri uri) 12 | { 13 | try 14 | { 15 | var bitmap = new BitmapImage(); 16 | bitmap.BeginInit(); 17 | bitmap.UriSource = uri; 18 | bitmap.EndInit(); 19 | return bitmap; 20 | } 21 | catch (Exception ex) 22 | { 23 | DivinityApp.Log($"Failed to create BitmapImage from '{uri}':\n{ex}"); 24 | } 25 | } 26 | return null; 27 | } 28 | 29 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 30 | { 31 | return ""; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/GUI/Directory.build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(SolutionDir)obj\GUI\ 4 | 5 | -------------------------------------------------------------------------------- /src/GUI/Extensions/DependencyObjectExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Media; 3 | 4 | namespace DivinityModManager; 5 | 6 | public static class DependencyObjectExtensions 7 | { 8 | public static IEnumerable FindVisualChildren(this DependencyObject depObj) where T : DependencyObject 9 | { 10 | if (depObj != null) 11 | { 12 | for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++) 13 | { 14 | DependencyObject child = VisualTreeHelper.GetChild(depObj, i); 15 | if (child != null && child is T) 16 | { 17 | yield return (T)child; 18 | } 19 | 20 | foreach (T childOfChild in FindVisualChildren(child)) 21 | { 22 | yield return childOfChild; 23 | } 24 | } 25 | } 26 | } 27 | public static T FindVisualParent(this DependencyObject depObj) where T : DependencyObject 28 | { 29 | if (depObj != null) 30 | { 31 | //get parent item 32 | DependencyObject parentObject = VisualTreeHelper.GetParent(depObj); 33 | 34 | //we've reached the end of the tree 35 | if (parentObject == null) return null; 36 | 37 | //check if the parent matches the type we're looking for 38 | T parent = parentObject as T; 39 | if (parent != null) 40 | return parent; 41 | else 42 | return FindVisualParent(parentObject); 43 | } 44 | return null; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/GUI/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /src/GUI/FodyWeavers.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. 12 | 13 | 14 | 15 | 16 | A comma-separated list of error codes that can be safely ignored in assembly verification. 17 | 18 | 19 | 20 | 21 | 'false' to turn off automatic generation of the XML Schema file. 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/GUI/Program.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Util.ScreenReader; 2 | 3 | using System.Reflection; 4 | using System.Windows; 5 | 6 | namespace DivinityModManager; 7 | 8 | internal class Program 9 | { 10 | private static SplashScreen _splash; 11 | private static string _libDirectory; 12 | 13 | private static Assembly ResolveAssembly(object sender, ResolveEventArgs args) 14 | { 15 | var assyName = new AssemblyName(args.Name); 16 | 17 | var newPath = Path.Combine(_libDirectory, assyName.Name); 18 | if (!newPath.EndsWith(".dll")) 19 | { 20 | newPath += ".dll"; 21 | } 22 | 23 | if (File.Exists(newPath)) 24 | { 25 | var assy = Assembly.LoadFile(newPath); 26 | return assy; 27 | } 28 | return null; 29 | } 30 | 31 | private static void OnAppExit(object sender, EventArgs e) 32 | { 33 | //CrossSpeakManager: Make sure to always call the Close() method before your application closes. 34 | Services.ScreenReader?.Close(); 35 | } 36 | 37 | [STAThread] 38 | static void Main(string[] args) 39 | { 40 | _libDirectory = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "_Lib"); 41 | AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly; 42 | 43 | _splash = new SplashScreen("Resources/BG3MMSplashScreen.png"); 44 | _splash.Show(false, false); 45 | 46 | var app = new App 47 | { 48 | Splash = _splash 49 | }; 50 | app.Exit += OnAppExit; 51 | app.InitializeComponent(); 52 | app.Run(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/GUI/Resources/AppFeatures.json: -------------------------------------------------------------------------------- 1 | { 2 | "ScriptExtender": true, 3 | "NexusMods": false, 4 | "Workshop": false 5 | } 6 | -------------------------------------------------------------------------------- /src/GUI/Resources/BG3MMSplashScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/BG3MMSplashScreen.png -------------------------------------------------------------------------------- /src/GUI/Resources/DefaultPathways.json: -------------------------------------------------------------------------------- 1 | { 2 | "Steam": { 3 | "AppID": "1086940", 4 | "Registry_32": "SOFTWARE\\Valve\\Steam\\Apps\\1086940", 5 | "Registry_64": "SOFTWARE\\Wow6432Node\\Valve\\Steam\\Apps\\1086940", 6 | "RootFolderName": "Baldurs Gate 3", 7 | "ExePath": "bin\\bg3.exe" 8 | }, 9 | "GOG": { 10 | "AppID": "360014030297", 11 | "Registry_32": "SOFTWARE\\GOG.com\\Games\\360014030297", 12 | "Registry_64": "SOFTWARE\\Wow6432Node\\GOG.com\\Games\\360014030297", 13 | "RootFolderName": "Baldurs Gate 3", 14 | "ExePath": "bin\\bg3.exe" 15 | }, 16 | "DocumentsGameFolder": "Larian Studios\\Baldur's Gate 3", 17 | "AppDataGameFolder": "%LOCALAPPDATA%\\Larian Studios\\Baldur's Gate 3", 18 | "GameDataFolder": "Data" 19 | } -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/AddItem_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/AddItem_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/AlertBar_Close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/AlertBar_Close.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/AlertBar_Close_Hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/AlertBar_Close_Hover.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/AlertBar_Danger_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/AlertBar_Danger_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/AlertBar_Information_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/AlertBar_Information_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/AlertBar_Success_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/AlertBar_Success_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/AlertBar_Warning_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/AlertBar_Warning_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/BG3_64x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/BG3_64x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/BrowserLink_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/BrowserLink_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/Builder_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/Builder_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/CopyToClipboard_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/CopyToClipboard_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/DefaultIcon_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/DefaultIcon_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/DeleteAzureResource_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/DeleteAzureResource_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/DivinityEngine2_64x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/DivinityEngine2_64x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/DivinityEngineGlasses_64x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/DivinityEngineGlasses_64x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/ExclamationPoint_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/ExclamationPoint_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/ExpandChevronDown_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/ExpandChevronDown_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/ExpandChevronDown_lightGray_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/ExpandChevronDown_lightGray_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/ExpandChevronUp_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/ExpandChevronUp_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/ExpandChevronUp_lightGrey_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/ExpandChevronUp_lightGrey_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/ExportData_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/ExportData_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/ExportScript_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/ExportScript_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/FileMissing_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/FileMissing_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/Folder_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/Folder_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/Github_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/Github_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/HighPriority_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/HighPriority_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/Kofi_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/Kofi_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/LoadCampaignOrder_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/LoadCampaignOrder_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/Log_Normal_32x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/Log_Normal_32x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/OpenFile_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/OpenFile_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/OpenFolder_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/OpenFolder_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/Osiris_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/Osiris_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/Osiris_ModFixer_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/Osiris_ModFixer_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/Refresh_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/Refresh_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/Rename_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/Rename_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/SaveAs_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/SaveAs_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/SaveGrey_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/SaveGrey_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/Save_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/Save_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/StatusCriticalError_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/StatusCriticalError_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/StatusWarning_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/StatusWarning_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/Steam_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/Steam_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/XMLSchemaError_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/XMLSchemaError_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/ZipFileAs_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/ZipFileAs_16x.png -------------------------------------------------------------------------------- /src/GUI/Resources/Icons/ZipFile_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaughingLeader/BG3ModManager/4b2fa9c04e98784a2e0ef73589f5efb3e75b69d5/src/GUI/Resources/Icons/ZipFile_16x.png -------------------------------------------------------------------------------- /src/GUI/Themes/Dark.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | #495588 11 | #497688 12 | #5e4988 13 | #33000000 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/GUI/Themes/DivinityColors.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace DivinityModManager.Themes; 4 | 5 | public static class DivinityColors 6 | { 7 | public static ComponentResourceKey TagBackgroundColor => new(typeof(DivinityColors), "TagBackgroundColor"); 8 | public static ComponentResourceKey CustomTagBackgroundColor => new(typeof(DivinityColors), "CustomTagBackgroundColor"); 9 | public static ComponentResourceKey ModeTagBackgroundColor => new(typeof(DivinityColors), "ModeTagBackgroundColor"); 10 | public static ComponentResourceKey ListInactiveColor => new(typeof(DivinityColors), "ListInactiveColor"); 11 | 12 | public static ComponentResourceKey TagBackgroundBrush => new(typeof(DivinityColors), "TagBackgroundBrush"); 13 | public static ComponentResourceKey CustomTagBackgroundBrush => new(typeof(DivinityColors), "CustomTagBackgroundBrush"); 14 | public static ComponentResourceKey ModeTagBackgroundBrush => new(typeof(DivinityColors), "ModeTagBackgroundBrush"); 15 | public static ComponentResourceKey ListInactiveBrush => new(typeof(DivinityColors), "ListInactiveBrush"); 16 | 17 | public static ComponentResourceKey ListInactiveRectangle => new(typeof(DivinityColors), "ListInactiveRectangle"); 18 | } 19 | -------------------------------------------------------------------------------- /src/GUI/Themes/Light.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | #495588 11 | #497688 12 | #5e4988 13 | #10000000 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/GUI/Themes/ResourceAliasHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Markup; 2 | using System.Xaml; 3 | 4 | namespace DivinityModManager.Themes; 5 | 6 | internal class ResourceAliasHelper : MarkupExtension 7 | { 8 | public object ResourceKey { get; set; } 9 | 10 | public override object ProvideValue(IServiceProvider serviceProvider) 11 | { 12 | IRootObjectProvider rootObjectProvider = (IRootObjectProvider)serviceProvider.GetService(typeof(IRootObjectProvider)); 13 | IDictionary dictionary = rootObjectProvider?.RootObject as IDictionary; 14 | return dictionary?[ResourceKey]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/GUI/Usings.cs: -------------------------------------------------------------------------------- 1 | global using ReactiveUI; 2 | global using ReactiveUI.Fody.Helpers; 3 | 4 | global using System; 5 | global using System.Collections; 6 | global using System.Collections.Generic; 7 | global using System.IO; 8 | global using System.Linq; 9 | global using System.Reactive; 10 | global using System.Reactive.Concurrency; 11 | global using System.Reactive.Disposables; 12 | global using System.Reactive.Linq; 13 | global using System.Threading; 14 | global using System.Threading.Tasks; 15 | 16 | global using RxCommandUnit = ReactiveUI.ReactiveCommand; 17 | -------------------------------------------------------------------------------- /src/GUI/Util/AutomationTooltipPeer.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Automation.Peers; 2 | using System.Windows.Controls; 3 | 4 | namespace DivinityModManager.Util; 5 | 6 | /// 7 | /// This simply disables the automation stuff for the tooltip, to prevent the ElementNotAvailableException from happening. 8 | /// 9 | public class AutomationTooltipPeer : ToolTipAutomationPeer 10 | { 11 | public AutomationTooltipPeer(ToolTip owner) : base(owner) { } 12 | 13 | protected override string GetNameCore() 14 | { 15 | return "AutomationTooltipPeer"; 16 | } 17 | 18 | protected override AutomationControlType GetAutomationControlTypeCore() 19 | { 20 | return AutomationControlType.ToolTip; 21 | } 22 | 23 | protected override List GetChildrenCore() 24 | { 25 | return new List(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/GUI/Util/BindingHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using System.Windows.Data; 3 | 4 | namespace DivinityModManager.Util; 5 | 6 | public static class BindingHelper 7 | { 8 | public static void CreateCommandBinding(Button button, string vmProperty, object source) 9 | { 10 | Binding binding = new Binding(vmProperty); 11 | binding.Source = source; 12 | binding.Mode = BindingMode.OneWay; 13 | button.SetBinding(Button.CommandProperty, binding); 14 | } 15 | public static void CreateCommandBinding(MenuItem button, string vmProperty, object source) 16 | { 17 | Binding binding = new Binding(vmProperty); 18 | binding.Source = source; 19 | binding.Mode = BindingMode.OneWay; 20 | button.SetBinding(MenuItem.CommandProperty, binding); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/GUI/Util/CustomPropertyResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Windows; 3 | 4 | namespace DivinityModManager.Util; 5 | 6 | public class CustomPropertyResolver : ICreatesObservableForProperty 7 | { 8 | public int GetAffinityForObject(Type type, string propertyName, bool beforeChanged = false) 9 | { 10 | if (!typeof(FrameworkElement).IsAssignableFrom(type)) 11 | return 0; 12 | var fi = type.GetTypeInfo().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly) 13 | .FirstOrDefault(x => x.Name == propertyName); 14 | 15 | return fi != null ? 2 /* POCO affinity+1 */ : 0; 16 | } 17 | 18 | public IObservable> GetNotificationForProperty(object sender, System.Linq.Expressions.Expression expression, string propertyName, bool beforeChanged = false, bool suppressWarnings = false) 19 | { 20 | var foo = (FrameworkElement)sender; 21 | var value = sender.GetType().GetProperty(propertyName)?.GetValue(sender, null); 22 | return Observable.Return(new ObservedChange(sender, expression, value), new DispatcherScheduler(foo.Dispatcher)) 23 | .Concat(Observable.Never>()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/GUI/Util/FocusHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace DivinityModManager.Util; 4 | 5 | public static class FocusHelper 6 | { 7 | public static bool HasKeyboardFocus(FrameworkElement element) 8 | { 9 | if (element == null) return false; 10 | if (element.IsKeyboardFocused || element.IsKeyboardFocusWithin) 11 | { 12 | return true; 13 | } 14 | foreach (var child in element.FindVisualChildren()) 15 | { 16 | if (HasKeyboardFocus(child)) 17 | { 18 | return true; 19 | } 20 | } 21 | return false; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/GUI/Util/LogTraceListener.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace DivinityModManager.Util; 4 | 5 | public class LogTraceListener : TextWriterTraceListener 6 | { 7 | private readonly Dictionary replacePaths = []; 8 | 9 | private void MaybeAddReplacement(string key, string path) 10 | { 11 | if (!String.IsNullOrEmpty(path)) 12 | { 13 | replacePaths.Add(key, path); 14 | } 15 | } 16 | 17 | public LogTraceListener(string fileName, string name) : base(fileName, name) 18 | { 19 | MaybeAddReplacement("%LOCALAPPDATA%", Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)); 20 | MaybeAddReplacement("%APPDATA%", Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); 21 | MaybeAddReplacement("%USERPROFILE%", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); 22 | } 23 | 24 | private string ReplaceText(string message) 25 | { 26 | if (!String.IsNullOrEmpty(message)) 27 | { 28 | foreach (var kvp in replacePaths) 29 | { 30 | message = message.Replace(kvp.Value, kvp.Key); 31 | } 32 | } 33 | return message; 34 | } 35 | 36 | public override void Write(string message) 37 | { 38 | base.Write(ReplaceText(message)); 39 | } 40 | 41 | public override void WriteLine(string message) 42 | { 43 | base.WriteLine(ReplaceText(message)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/GUI/Util/RxExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Views; 2 | 3 | using System.Windows; 4 | 5 | namespace DivinityModManager.Util; 6 | 7 | class RxExceptionHandler : IObserver 8 | { 9 | public static MainWindow view { get; set; } 10 | public void OnNext(Exception value) 11 | { 12 | //if (Debugger.IsAttached) Debugger.Break(); 13 | 14 | var message = $"(OnNext) Exception encountered:\nType: {value.GetType()}\tMessage: {value.Message}\nSource: {value.Source}\nStackTrace: {value.StackTrace}"; 15 | DivinityApp.Log(message); 16 | MessageBox.Show(message, "Error Encountered", MessageBoxButton.OK, MessageBoxImage.Error); 17 | //MessageBoxResult result = Xceed.Wpf.Toolkit.MessageBox.Show(view, message, "Error Encountered", MessageBoxButton.OK, MessageBoxImage.Error, MessageBoxResult.OK, view.MainWindowMessageBox_OK.Style); 18 | //RxApp.MainThreadScheduler.Schedule(() => { throw value; }); 19 | } 20 | 21 | public void OnError(Exception value) 22 | { 23 | var message = $"(OnError) Exception encountered:\nType: {value.GetType()}\tMessage: {value.Message}\nSource: {value.Source}\nStackTrace: {value.StackTrace}"; 24 | DivinityApp.Log(message); 25 | //MessageBoxResult result = Xceed.Wpf.Toolkit.MessageBox.Show(view, message, "Error Encountered", MessageBoxButton.OK, MessageBoxImage.Error, MessageBoxResult.OK, view.MainWindowMessageBox_OK.Style); 26 | } 27 | 28 | public void OnCompleted() 29 | { 30 | //if (Debugger.IsAttached) Debugger.Break(); 31 | //RxApp.MainThreadScheduler.Schedule(() => { throw new NotImplementedException(); }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/GUI/Util/ScreenReader/AlertBarAutomationPeer.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Controls; 2 | 3 | using System.Windows.Automation.Peers; 4 | 5 | namespace DivinityModManager.Util.ScreenReader; 6 | 7 | public class AlertBarAutomationPeer : FrameworkElementAutomationPeer 8 | { 9 | private AlertBar alertBar; 10 | 11 | public AlertBarAutomationPeer(AlertBar owner) : base(owner) 12 | { 13 | alertBar = owner; 14 | } 15 | protected override string GetNameCore() 16 | { 17 | return alertBar.GetText(); 18 | } 19 | 20 | protected override AutomationControlType GetAutomationControlTypeCore() 21 | { 22 | return AutomationControlType.StatusBar; 23 | } 24 | 25 | protected override List GetChildrenCore() 26 | { 27 | List peers = new List(); 28 | var textElements = alertBar.GetTextElements(); 29 | if (textElements.Count > 0) 30 | { 31 | foreach (var element in textElements) 32 | { 33 | var peer = UIElementAutomationPeer.CreatePeerForElement(element); 34 | if (peer != null) 35 | { 36 | peers.Add(peer); 37 | } 38 | } 39 | } 40 | return peers; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/GUI/Util/ScreenReader/CachedAutomationPeer.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Automation.Peers; 3 | using System.Windows.Media; 4 | 5 | namespace DivinityModManager.Util.ScreenReader; 6 | 7 | public class CachedAutomationPeer : FrameworkElementAutomationPeer 8 | { 9 | public CachedAutomationPeer(FrameworkElement owner) : base(owner) { } 10 | 11 | private List _cachedAutomationPeers; 12 | 13 | private static AutomationPeer CreatePeerForElementSafe(UIElement element) 14 | { 15 | try 16 | { 17 | return FrameworkElementAutomationPeer.CreatePeerForElement(element); 18 | } 19 | catch (Exception) 20 | { 21 | return null; 22 | } 23 | } 24 | 25 | internal static List GetChildrenRecursively(UIElement uiElement) 26 | { 27 | List children = new List(); 28 | int childrenCount = VisualTreeHelper.GetChildrenCount(uiElement); 29 | 30 | for (int child = 0; child < childrenCount; child++) 31 | { 32 | if (!(VisualTreeHelper.GetChild(uiElement, child) is UIElement element)) 33 | continue; 34 | 35 | AutomationPeer peer = CreatePeerForElementSafe(element); 36 | if (peer != null) 37 | children.Add(peer); 38 | else 39 | { 40 | List returnedChildren = GetChildrenRecursively(element); 41 | if (returnedChildren != null) 42 | children.AddRange(returnedChildren); 43 | } 44 | } 45 | 46 | if (children.Count == 0) 47 | return null; 48 | 49 | return children; 50 | } 51 | 52 | public virtual bool HasNullChildElement() 53 | { 54 | foreach (var c in this.Owner.FindVisualChildren()) 55 | { 56 | if (c == null) 57 | { 58 | return true; 59 | } 60 | } 61 | return false; 62 | } 63 | 64 | public virtual List GetPeersFromElements() 65 | { 66 | return GetChildrenRecursively(Owner); 67 | } 68 | 69 | protected override List GetChildrenCore() 70 | { 71 | if (HasNullChildElement()) 72 | { 73 | return _cachedAutomationPeers; 74 | } 75 | else 76 | { 77 | _cachedAutomationPeers = GetPeersFromElements(); 78 | } 79 | return _cachedAutomationPeers; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/GUI/Util/ScreenReader/MainWindowAutomationPeer.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Views; 2 | 3 | using System.Windows.Automation.Peers; 4 | 5 | namespace DivinityModManager.Util.ScreenReader; 6 | 7 | public class MainWindowAutomationPeer : CachedAutomationPeer 8 | { 9 | private MainWindow mainWindow; 10 | public MainWindowAutomationPeer(MainWindow owner) : base(owner) 11 | { 12 | mainWindow = owner; 13 | } 14 | 15 | protected override string GetNameCore() 16 | { 17 | if (mainWindow.ViewModel != null) 18 | { 19 | return mainWindow.ViewModel.Title; 20 | } 21 | else 22 | { 23 | return "Divinity Mod Manager"; 24 | } 25 | } 26 | 27 | protected override AutomationControlType GetAutomationControlTypeCore() 28 | { 29 | return AutomationControlType.Window; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/GUI/Util/ScreenReader/ModEntryGridAutomationPeer.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Controls; 2 | 3 | using System.Windows.Automation; 4 | using System.Windows.Automation.Peers; 5 | using System.Windows.Controls; 6 | 7 | namespace DivinityModManager.Util.ScreenReader; 8 | 9 | public class ModEntryGridAutomationPeer : CachedAutomationPeer 10 | { 11 | private ModEntryGrid grid; 12 | public ModEntryGridAutomationPeer(ModEntryGrid owner) : base(owner) 13 | { 14 | grid = owner; 15 | } 16 | 17 | protected override string GetNameCore() 18 | { 19 | return grid.GetValue(AutomationProperties.NameProperty) as string ?? string.Empty; 20 | } 21 | 22 | protected override AutomationControlType GetAutomationControlTypeCore() 23 | { 24 | return AutomationControlType.ListItem; 25 | } 26 | 27 | private AutomationPeer _textPeer; 28 | 29 | override public bool HasNullChildElement() 30 | { 31 | var text = ElementHelper.FindChild(grid, "ModNameText"); 32 | if (text != null) 33 | { 34 | var peer = UIElementAutomationPeer.CreatePeerForElement(text); 35 | if (peer != null) 36 | { 37 | _textPeer = peer; 38 | return true; 39 | } 40 | } 41 | return true; 42 | } 43 | 44 | override public List GetPeersFromElements() 45 | { 46 | return new List(1) { _textPeer }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/GUI/Util/ScreenReader/ModListViewAutomationPeer.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Controls; 2 | 3 | using System.Windows.Automation; 4 | using System.Windows.Automation.Peers; 5 | 6 | namespace DivinityModManager.Util.ScreenReader; 7 | 8 | public class ModListViewAutomationPeer : CachedAutomationPeer 9 | { 10 | private ModListView _listView; 11 | 12 | public ModListViewAutomationPeer(ModListView owner) : base(owner) 13 | { 14 | _listView = owner; 15 | } 16 | 17 | protected override string GetNameCore() 18 | { 19 | return Owner.GetValue(AutomationProperties.NameProperty) as string ?? string.Empty; 20 | } 21 | 22 | protected override AutomationControlType GetAutomationControlTypeCore() 23 | { 24 | return AutomationControlType.List; 25 | } 26 | 27 | override public bool HasNullChildElement() 28 | { 29 | foreach (var c in _listView.Items) 30 | { 31 | if (c == null) 32 | { 33 | DivinityApp.Log("Found a null entry in ModListViewAutomationPeer"); 34 | return true; 35 | } 36 | } 37 | return false; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/GUI/ViewModels/BaseProgressViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.ViewModels; 2 | 3 | public class BaseProgressViewModel : ReactiveObject 4 | { 5 | [Reactive] public bool CanRun { get; set; } 6 | [Reactive] public bool CanClose { get; set; } 7 | [Reactive] public bool IsVisible { get; set; } 8 | [Reactive] public bool IsProgressActive { get; set; } 9 | [Reactive] public string ProgressTitle { get; set; } 10 | [Reactive] public string ProgressWorkText { get; set; } 11 | [Reactive] public double ProgressValue { get; set; } 12 | 13 | private readonly ObservableAsPropertyHelper _isRunning; 14 | /// 15 | /// True when the RunCommand is executing. 16 | /// 17 | public bool IsRunning => _isRunning.Value; 18 | 19 | public ReactiveCommand RunCommand { get; private set; } 20 | public RxCommandUnit CancelRunCommand { get; private set; } 21 | public RxCommandUnit CloseCommand { get; private set; } 22 | 23 | internal async Task UpdateProgress(string title = "", string workText = "", double value = -1) 24 | { 25 | await Observable.Start(() => 26 | { 27 | if (!String.IsNullOrEmpty(title)) 28 | { 29 | ProgressTitle = title; 30 | } 31 | if (!String.IsNullOrEmpty(workText)) 32 | { 33 | ProgressWorkText = workText; 34 | } 35 | if (value > -1) 36 | { 37 | ProgressValue = value; 38 | } 39 | }, RxApp.MainThreadScheduler); 40 | return Unit.Default; 41 | } 42 | 43 | public virtual async Task Run(CancellationToken cts) 44 | { 45 | return true; 46 | } 47 | 48 | public virtual void Close() 49 | { 50 | CanClose = true; 51 | IsVisible = false; 52 | IsProgressActive = false; 53 | } 54 | 55 | public BaseProgressViewModel() 56 | { 57 | CanClose = true; 58 | var canRun = this.WhenAnyValue(x => x.CanRun); 59 | RunCommand = ReactiveCommand.CreateFromObservable(() => Observable.StartAsync(cts => Run(cts)).TakeUntil(this.CancelRunCommand), canRun); 60 | 61 | _isRunning = this.RunCommand.IsExecuting.ToProperty(this, nameof(IsRunning), true, RxApp.MainThreadScheduler); 62 | 63 | CancelRunCommand = ReactiveCommand.Create(() => { }, this.WhenAnyValue(x => x.IsRunning)); 64 | 65 | var canClose = this.WhenAnyValue(x => x.CanClose, x => x.IsRunning, (b1, b2) => b1 && !b2); 66 | CloseCommand = ReactiveCommand.Create(Close, canClose); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/GUI/ViewModels/MainWindowExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace DivinityModManager.ViewModels; 4 | 5 | public class MainWindowExceptionHandler : IObserver 6 | { 7 | private MainWindowViewModel _viewModel; 8 | 9 | public MainWindowExceptionHandler(MainWindowViewModel vm) 10 | { 11 | _viewModel = vm; 12 | } 13 | 14 | public void OnNext(Exception value) 15 | { 16 | DivinityApp.Log($"Error: [{value.Source}]({value.GetType()}): {value.Message}\n{value.StackTrace}"); 17 | //if (Debugger.IsAttached) Debugger.Break(); 18 | //RxApp.MainThreadScheduler.Schedule(() => { throw value; }); 19 | } 20 | 21 | public void OnError(Exception error) 22 | { 23 | DivinityApp.Log($"Error: [{error.Source}]({error.GetType()}): {error.Message}\n{error.StackTrace}"); 24 | if (Debugger.IsAttached) Debugger.Break(); 25 | 26 | RxApp.MainThreadScheduler.Schedule(() => 27 | { 28 | if (_viewModel.MainProgressIsActive) 29 | { 30 | _viewModel.MainProgressIsActive = false; 31 | } 32 | _viewModel.View.AlertBar.SetDangerAlert(error.Message); 33 | //throw error; 34 | }); 35 | } 36 | 37 | public void OnCompleted() 38 | { 39 | if (Debugger.IsAttached) Debugger.Break(); 40 | //RxApp.MainThreadScheduler.Schedule(() => { throw new NotImplementedException(); }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/GUI/Views/AboutWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace DivinityModManager.Views; 2 | 3 | public class AboutWindowBase : HideWindowBase { } 4 | 5 | public class AboutWindowViewModel : ReactiveObject 6 | { 7 | [Reactive] public string Title { get; set; } 8 | 9 | public AboutWindowViewModel() 10 | { 11 | Title = "About"; 12 | } 13 | } 14 | 15 | /// 16 | /// Interaction logic for AboutWindow.xaml 17 | /// 18 | public partial class AboutWindow : AboutWindowBase 19 | { 20 | public AboutWindow() 21 | { 22 | InitializeComponent(); 23 | 24 | ViewModel = new AboutWindowViewModel(); 25 | 26 | this.WhenActivated(d => 27 | { 28 | d(this.OneWayBind(ViewModel, vm => vm.Title, v => v.TitleText.Text)); 29 | d(this.OneWayBind(ViewModel, vm => vm.Title, v => v.Title)); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/GUI/Views/AppUpdateWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Controls; 2 | using DivinityModManager.ViewModels; 3 | 4 | using System.ComponentModel; 5 | using System.Windows.Documents; 6 | 7 | using ReactiveMarbles.ObservableEvents; 8 | using AutoUpdaterDotNET; 9 | 10 | namespace DivinityModManager.Views; 11 | 12 | public class AppUpdateWindowBase : HideWindowBase { } 13 | 14 | public partial class AppUpdateWindow : AppUpdateWindowBase 15 | { 16 | private readonly Lazy _fallbackMarkdown = new(() => new Markdown()); 17 | private readonly Markdown _defaultMarkdown; 18 | 19 | private FlowDocument StringToMarkdown(string text) 20 | { 21 | var markdown = _defaultMarkdown ?? _fallbackMarkdown.Value; 22 | var doc = markdown.Transform(text); 23 | return doc; 24 | } 25 | 26 | public override void HideWindow_Closing(object sender, CancelEventArgs e) 27 | { 28 | base.HideWindow_Closing(sender, e); 29 | ViewModel.IsVisible = false; 30 | } 31 | 32 | public AppUpdateWindow() 33 | { 34 | InitializeComponent(); 35 | 36 | ViewModel = Services.Get(); 37 | 38 | var obj = TryFindResource("DefaultMarkdown"); 39 | if (obj != null && obj is Markdown markdown) 40 | { 41 | _defaultMarkdown = markdown; 42 | } 43 | 44 | this.WhenActivated(d => 45 | { 46 | d(this.BindCommand(ViewModel, vm => vm.ConfirmCommand, v => v.ConfirmButton)); 47 | d(this.BindCommand(ViewModel, vm => vm.SkipCommand, v => v.SkipButton)); 48 | d(this.OneWayBind(ViewModel, vm => vm.SkipButtonText, v => v.SkipButton.Content)); 49 | d(this.OneWayBind(ViewModel, vm => vm.UpdateDescription, v => v.UpdateDescription.Text)); 50 | d(this.OneWayBind(ViewModel, vm => vm.UpdateChangelogView, v => v.UpdateChangelogView.Document, StringToMarkdown)); 51 | 52 | this.Events().IsVisibleChanged.Select(x => x.NewValue).BindTo(ViewModel, x => x.IsVisible); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/GUI/Views/ExportOrderToArchiveView.xaml.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Converters; 2 | using DivinityModManager.ViewModels; 3 | 4 | namespace DivinityModManager.Views; 5 | 6 | public class ExportOrderToArchiveViewViewBase : ReactiveUserControl { } 7 | 8 | /// 9 | /// Interaction logic for ExportOrderToArchiveView.xaml 10 | /// 11 | public partial class ExportOrderToArchiveView : ExportOrderToArchiveViewViewBase 12 | { 13 | public ExportOrderToArchiveView() 14 | { 15 | InitializeComponent(); 16 | 17 | ViewModel = new ExportOrderToArchiveViewModel(); 18 | DataContext = ViewModel; 19 | 20 | this.WhenActivated(d => 21 | { 22 | if (this.ViewModel != null) 23 | { 24 | d(this.OneWayBind(ViewModel, vm => vm.IsVisible, view => view.Visibility, BoolToVisibilityConverter.FromBool)); 25 | 26 | d(this.OneWayBind(ViewModel, vm => vm.Entries, v => v.FilesListView.ItemsSource)); 27 | 28 | d(this.OneWayBind(ViewModel, vm => vm.IsRunning, v => v.ProgressIndicator.IsBusy)); 29 | d(this.OneWayBind(ViewModel, vm => vm.ProgressTitle, v => v.TaskProgressTitleText.Text)); 30 | d(this.OneWayBind(ViewModel, vm => vm.ProgressWorkText, v => v.TaskProgressWorkText.Text)); 31 | d(this.OneWayBind(ViewModel, vm => vm.ProgressValue, v => v.TaskProgressBar.Value)); 32 | d(this.OneWayBind(ViewModel, vm => vm.IsProgressActive, view => view.TaskProgressBar.Visibility, BoolToVisibilityConverter.FromBool)); 33 | 34 | d(this.Bind(ViewModel, vm => vm.IncludeOverrides, view => view.IncludeOverridesCheckbox.IsChecked)); 35 | d(this.Bind(ViewModel, vm => vm.SelectedOrderType, view => view.OrderTypeComboBox.SelectedItem)); 36 | d(this.Bind(ViewModel, vm => vm.OrderTypes, view => view.OrderTypeComboBox.ItemsSource)); 37 | 38 | //d(this.BindCommand(ViewModel, vm => vm.SelectAllCommand, v => v.ConfirmButton)); 39 | d(this.BindCommand(ViewModel, vm => vm.RunCommand, v => v.ConfirmButton)); 40 | d(this.BindCommand(ViewModel, vm => vm.CancelRunCommand, v => v.CancelProgressButton)); 41 | d(this.BindCommand(ViewModel, vm => vm.CloseCommand, v => v.CancelButton)); 42 | } 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/GUI/Views/HelpWindow.xaml: -------------------------------------------------------------------------------- 1 |  12 | 13 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/GUI/Views/HelpWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using DivinityModManager.Controls; 2 | 3 | using System.Windows.Documents; 4 | 5 | namespace DivinityModManager.Views; 6 | 7 | public class HelpWindowBase : HideWindowBase { } 8 | 9 | public class HelpWindowViewModel : ReactiveObject 10 | { 11 | [Reactive] public string WindowTitle { get; set; } 12 | [Reactive] public string HelpTitle { get; set; } 13 | [Reactive] public string HelpText { get; set; } 14 | 15 | public HelpWindowViewModel() 16 | { 17 | WindowTitle = "Help"; 18 | HelpTitle = ""; 19 | HelpText = ""; 20 | } 21 | } 22 | 23 | public partial class HelpWindow : HelpWindowBase 24 | { 25 | private readonly Lazy _fallbackMarkdown = new(() => new Markdown()); 26 | private Markdown _defaultMarkdown; 27 | 28 | private FlowDocument StringToMarkdown(string text) 29 | { 30 | var markdown = _defaultMarkdown ?? _fallbackMarkdown.Value; 31 | var doc = markdown.Transform(text); 32 | return doc; 33 | } 34 | 35 | public HelpWindow() 36 | { 37 | InitializeComponent(); 38 | 39 | ViewModel = new HelpWindowViewModel(); 40 | 41 | this.WhenActivated(d => 42 | { 43 | var obj = TryFindResource("DefaultMarkdown"); 44 | if (obj != null && obj is Markdown markdown) 45 | { 46 | _defaultMarkdown = markdown; 47 | } 48 | 49 | d(this.OneWayBind(ViewModel, vm => vm.WindowTitle, v => v.Title)); 50 | d(this.OneWayBind(ViewModel, vm => vm.HelpTitle, v => v.HelpTitleText.Text)); 51 | d(this.OneWayBind(ViewModel, vm => vm.HelpText, v => v.MarkdownViewer.Document, StringToMarkdown)); 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/GUI/Views/HideWindowBase.cs: -------------------------------------------------------------------------------- 1 | using AdonisUI.Controls; 2 | 3 | using System.ComponentModel; 4 | using System.Windows; 5 | using System.Windows.Controls; 6 | using System.Windows.Input; 7 | 8 | namespace DivinityModManager.Views; 9 | 10 | public class HideWindowBase : AdonisWindow, IViewFor where TViewModel : class 11 | { 12 | /// 13 | /// The view model dependency property. 14 | /// 15 | public static readonly DependencyProperty ViewModelProperty = 16 | DependencyProperty.Register("ViewModel", 17 | typeof(TViewModel), 18 | typeof(HideWindowBase), 19 | new PropertyMetadata(null)); 20 | 21 | /// 22 | /// Gets the binding root view model. 23 | /// 24 | public TViewModel BindingRoot => ViewModel; 25 | 26 | /// 27 | public TViewModel ViewModel 28 | { 29 | get => (TViewModel)GetValue(ViewModelProperty); 30 | set => SetValue(ViewModelProperty, value); 31 | } 32 | 33 | /// 34 | object IViewFor.ViewModel 35 | { 36 | get => ViewModel; 37 | set => ViewModel = (TViewModel)value; 38 | } 39 | 40 | public HideWindowBase() 41 | { 42 | Closing += HideWindow_Closing; 43 | KeyDown += (o, e) => 44 | { 45 | if (!e.Handled && e.Key == System.Windows.Input.Key.Escape) 46 | { 47 | if (Keyboard.FocusedElement == null || Keyboard.FocusedElement.GetType() != typeof(TextBox)) 48 | { 49 | Hide(); 50 | } 51 | } 52 | }; 53 | } 54 | 55 | protected override void OnSourceInitialized(EventArgs e) 56 | { 57 | base.OnSourceInitialized(e); 58 | } 59 | 60 | public virtual void HideWindow_Closing(object sender, CancelEventArgs e) 61 | { 62 | e.Cancel = true; 63 | Hide(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/GUI/Views/LeaderLibSettingsWindow.xaml: -------------------------------------------------------------------------------- 1 |  12 | 13 | 14 | -------------------------------------------------------------------------------- /src/GUI/Views/LeaderLibSettingsWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace DivinityModManager.Views; 4 | 5 | /// 6 | /// Interaction logic for LeaderLibSettingsWindow.xaml 7 | /// 8 | public partial class LeaderLibSettingsWindow : Window 9 | { 10 | public LeaderLibSettingsWindow() 11 | { 12 | InitializeComponent(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/GUI/Views/VerticalModLayout.xaml: -------------------------------------------------------------------------------- 1 |  15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 37 | 38 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/GUI/Views/VerticalModLayout.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | 3 | namespace DivinityModManager.Views; 4 | 5 | /// 6 | /// Interaction logic for VerticalModLayout.xaml 7 | /// 8 | public partial class VerticalModLayout : UserControl 9 | { 10 | public VerticalModLayout() 11 | { 12 | InitializeComponent(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/GUI/app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | true 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Toolbox/Args/MainArgs.cs: -------------------------------------------------------------------------------- 1 | using PowerArgs; 2 | 3 | using Toolbox.ScriptExtender; 4 | 5 | namespace Toolbox.Args; 6 | 7 | [ArgExceptionBehavior(ArgExceptionPolicy.StandardExceptionHandling)] 8 | public class MainArgs 9 | { 10 | [ArgActionMethod, ArgDescription("Updates the script extender using the updater dll")] 11 | public static void UpdateScriptExtender(ScriptExtenderUpdaterArgs args) 12 | { 13 | if (!File.Exists(args.Updater) || !Directory.Exists(args.BinFolder)) 14 | { 15 | throw new FileNotFoundException($"-u ({args.Updater}) and -b ({args.BinFolder}) args must be valid file paths."); 16 | } 17 | using (var updater = new Updater(args.Updater, args.BinFolder)) 18 | { 19 | //updater.ShowConsoleWindow(); 20 | //updater.SetGameVersion(Path.Join(args.BinFolder, "bg3.exe"); 21 | Console.WriteLine($"Updating..."); 22 | updater.Update(); 23 | } 24 | Console.WriteLine("Update finished"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Toolbox/Args/ScriptExtenderUpdaterArgs.cs: -------------------------------------------------------------------------------- 1 | using PowerArgs; 2 | 3 | namespace Toolbox.Args; 4 | 5 | public class ScriptExtenderUpdaterArgs 6 | { 7 | [ArgShortcut("-u"), ArgDescription("The path to DWrite.dll"), ArgRequired] 8 | public string? Updater { get; set; } 9 | 10 | [ArgShortcut("-b"), ArgDescription("The path to the game's bin folder, where ScriptExtenderUpdaterConfig.json / bg3.ex is")] 11 | public string? BinFolder { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/Toolbox/Directory.build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(SolutionDir)obj\Toolbox\ 4 | 5 | -------------------------------------------------------------------------------- /src/Toolbox/NativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Toolbox; 4 | 5 | public static partial class NativeMethods 6 | { 7 | [LibraryImport("kernel32.dll", EntryPoint = "LoadLibraryA")] 8 | public static partial nint LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName); 9 | 10 | [LibraryImport("kernel32.dll", SetLastError = true)] 11 | public static partial nint GetProcAddress(nint hModule, [MarshalAs(UnmanagedType.LPStr)] string procedureName); 12 | 13 | [LibraryImport("kernel32.dll")] 14 | [return: MarshalAs(UnmanagedType.Bool)] 15 | public static partial bool FreeLibrary(nint libraryReference); 16 | 17 | public static void ThrowExceptionForLastWin32Error() 18 | { 19 | var errorCode = Marshal.GetHRForLastWin32Error(); 20 | Marshal.ThrowExceptionForHR(errorCode); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Toolbox/Program.cs: -------------------------------------------------------------------------------- 1 |  2 | using PowerArgs; 3 | 4 | using Toolbox.Args; 5 | 6 | Args.InvokeAction(args); -------------------------------------------------------------------------------- /src/Toolbox/Toolbox.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net8.0-windows 5 | $(SolutionDir)bin\$(Configuration)\Tools\ 6 | enable 7 | enable 8 | true 9 | en-US 10 | false 11 | x64 12 | Debug;Release;Publish;PublishTest 13 | none 14 | True 15 | 16 | 17 | full 18 | False 19 | 20 | 21 | 22 | 23 | 24 | --------------------------------------------------------------------------------