├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── Directory.Build.props ├── FireAxe.Core ├── AddonChildProblem.cs ├── AddonFileNotExistProblem.cs ├── AddonGroup.cs ├── AddonGroupSave.cs ├── AddonNameExistsException.cs ├── AddonNode.cs ├── AddonNodeContainerService.cs ├── AddonNodeFileFinder.cs ├── AddonNodeMoveDeniedException.cs ├── AddonNodeSave.cs ├── AddonNodeSearchUtils.cs ├── AddonProblem.cs ├── AddonRoot.cs ├── AddonRootSave.cs ├── AddonTags.cs ├── AutoUpdateStrategy.cs ├── DisposableUtils.cs ├── DownloadFailedProblem.cs ├── DownloadService.cs ├── DownloadStatus.cs ├── FileExistException.cs ├── FileUtils.cs ├── FireAxe.Core.csproj ├── GamePathUtils.cs ├── IAddonNodeContainer.cs ├── IAddonNodeContainerExtensions.cs ├── IDownloadItem.cs ├── IDownloadService.cs ├── IHierarchyNode.cs ├── IHierarchyNodeExtensions.cs ├── ISaveable.cs ├── InvalidGamePathException.cs ├── InvalidPublishedFileIdProblem.cs ├── LocalVpkAddon.cs ├── LocalVpkAddonSave.cs ├── MoveFileException.cs ├── ObservableObject.cs ├── PublishedFileDetails.cs ├── PublishedFileDetailsUtils.cs ├── VpkAddon.cs ├── VpkAddonInfo.cs ├── VpkAddonSave.cs ├── VpkUtils.cs ├── WorkshopCollectionUtils.cs ├── WorkshopVpkAddon.cs ├── WorkshopVpkAddonSave.cs ├── WorkshopVpkFileNotLoadProblem.cs └── WorkshopVpkMetaInfo.cs ├── FireAxe.CrashReporter ├── FireAxe.CrashReporter.csproj └── Program.cs ├── FireAxe.GUI ├── AddonProblemExplanations.cs ├── App.axaml ├── App.axaml.cs ├── AppGlobal.cs ├── AppSettings.cs ├── AppWindowManager.cs ├── Assets │ ├── AppLogo.ico │ ├── CommonResources.axaml │ └── avalonia-logo.ico ├── DataTemplates │ ├── ExceptionExplainer.cs │ └── ViewLocator.cs ├── DescriptiveObject.cs ├── DesignHelper.cs ├── ErrorOperationReply.cs ├── ExceptionExplanationScene.cs ├── ExceptionExplanations.cs ├── FireAxe.GUI.csproj ├── IAppWindowManager.cs ├── LanguageManager.cs ├── MarkupExtensions │ └── EnumValues.cs ├── ObjectExplanationManager.cs ├── Program.cs ├── Resources │ ├── Texts.Designer.cs │ ├── Texts.resx │ └── Texts.zh-Hans.resx ├── SaveManager.cs ├── SelectionModelHelper.cs ├── Utils.cs ├── ValueConverters │ ├── EnumDescriptionConverter.cs │ ├── LanguageNativeNameConverter.cs │ └── ObjectExplanationConverter.cs ├── ViewModels │ ├── AddAddonTagViewModel.cs │ ├── AddonGroupViewModel.cs │ ├── AddonGroupViewModelDesign.cs │ ├── AddonNodeComparer.cs │ ├── AddonNodeContainerViewModel.cs │ ├── AddonNodeContainerViewModelDesign.cs │ ├── AddonNodeCustomizeImageViewModel.cs │ ├── AddonNodeEnableState.cs │ ├── AddonNodeExplorerViewModel.cs │ ├── AddonNodeExplorerViewModelDesign.cs │ ├── AddonNodeListItemViewKind.cs │ ├── AddonNodeListItemViewModel.cs │ ├── AddonNodeListItemViewModelDesign.cs │ ├── AddonNodeNavBarItemViewModel.cs │ ├── AddonNodeSimpleViewModel.cs │ ├── AddonNodeSortMethod.cs │ ├── AddonNodeViewModel.cs │ ├── AddonNodeViewModelDesign.cs │ ├── AddonTagEditorViewModel.cs │ ├── AddonTagManagerViewModel.cs │ ├── AppSettingsViewModel.cs │ ├── DownloadItemListViewModel.cs │ ├── DownloadItemViewModel.cs │ ├── FlatVpkAddonListViewModel.cs │ ├── FlatVpkAddonViewModel.cs │ ├── LocalVpkAddonViewModel.cs │ ├── MainWindowViewModel.cs │ ├── NewWorkshopCollectionViewModel.cs │ ├── UpdateRequestReply.cs │ ├── ViewModelBase.cs │ ├── VpkAddonViewModel.cs │ └── WorkshopVpkAddonViewModel.cs ├── Views │ ├── AboutWindow.axaml │ ├── AboutWindow.axaml.cs │ ├── AddAddonTagWindow.axaml │ ├── AddAddonTagWindow.axaml.cs │ ├── AddonGroupSectionView.axaml │ ├── AddonGroupSectionView.axaml.cs │ ├── AddonNodeContainerView.axaml │ ├── AddonNodeContainerView.axaml.cs │ ├── AddonNodeCustomizeImageWindow.axaml │ ├── AddonNodeCustomizeImageWindow.axaml.cs │ ├── AddonNodeEnableButton.axaml │ ├── AddonNodeEnableButton.axaml.cs │ ├── AddonNodeExplorerView.axaml │ ├── AddonNodeExplorerView.axaml.cs │ ├── AddonNodeGridRowView.axaml │ ├── AddonNodeGridRowView.axaml.cs │ ├── AddonNodeListItemView.cs │ ├── AddonNodeNavBarItemView.axaml │ ├── AddonNodeNavBarItemView.axaml.cs │ ├── AddonNodeNavBarView.axaml │ ├── AddonNodeNavBarView.axaml.cs │ ├── AddonNodeSectionViewDecorator.axaml │ ├── AddonNodeSectionViewDecorator.axaml.cs │ ├── AddonNodeTileView.axaml │ ├── AddonNodeTileView.axaml.cs │ ├── AddonNodeView.axaml │ ├── AddonNodeView.axaml.cs │ ├── AddonTagCheckBox.axaml │ ├── AddonTagCheckBox.axaml.cs │ ├── AddonTagCheckBoxList.axaml │ ├── AddonTagCheckBoxList.axaml.cs │ ├── AddonTagEditorWindow.axaml │ ├── AddonTagEditorWindow.axaml.cs │ ├── AddonTagManagerWindow.axaml │ ├── AddonTagManagerWindow.axaml.cs │ ├── AddonTagView.axaml │ ├── AddonTagView.axaml.cs │ ├── AppSettingsWindow.axaml │ ├── AppSettingsWindow.axaml.cs │ ├── CheckingUpdateWindow.axaml │ ├── CheckingUpdateWindow.axaml.cs │ ├── CommonMessageBoxes.cs │ ├── DownloadItemListView.axaml │ ├── DownloadItemListView.axaml.cs │ ├── DownloadItemListWindow.axaml │ ├── DownloadItemListWindow.axaml.cs │ ├── DownloadItemView.axaml │ ├── DownloadItemView.axaml.cs │ ├── EditableTextBlock.axaml │ ├── EditableTextBlock.axaml.cs │ ├── FlatVpkAddonListItemView.axaml │ ├── FlatVpkAddonListItemView.axaml.cs │ ├── FlatVpkAddonListWindow.axaml │ ├── FlatVpkAddonListWindow.axaml.cs │ ├── MainWindow.axaml │ ├── MainWindow.axaml.cs │ ├── NewWorkshopCollectionWindow.axaml │ ├── NewWorkshopCollectionWindow.axaml.cs │ ├── VpkAddonSectionView.axaml │ ├── VpkAddonSectionView.axaml.cs │ ├── WorkshopVpkAddonSectionView.axaml │ └── WorkshopVpkAddonSectionView.axaml.cs ├── WindowReference.cs └── app.manifest ├── FireAxe.sln ├── LICENSE.txt ├── README.md └── README.zh-Hans.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.6.1 2 | - add: addon preview image customization (issue #11) 3 | - upgrade: update dependent packages 4 | - add: workshop preview image cache (issue #11) 5 | - fix: hotkeys conflict with TextBox (issue #12) 6 | - bugfixes 7 | # v0.6.0 8 | - change: rename L4D2AddonAssistant to FireAxe 9 | - add: addon tag management 10 | - add: add the ability to apply tags from workshop 11 | - add: add some menu items to AddonNodeExplorerView 12 | - fix: push will fail if there're files with the same name 13 | - improve: improve performance 14 | - improve: add version check when opening directory 15 | - improve: auto remove WorkshopVpkFileNotLoadProblem when the download completes 16 | - change: rename the menu item "File->Open" to "File->Open Directory" 17 | - add: add menu item "File->Close Directory" 18 | - bugfixes 19 | # v0.5.1 20 | - fix: too many download tasks will crash the program (issue #7) 21 | - fix: cancelling a workshop collection creation will crash the program 22 | - fix: avoid circular references in linked workshop collections 23 | - add: moving addons display 24 | - add: support Ctrl+X and Ctrl+V to move addons and support mouse side button to goto the parent group 25 | # v0.5.0 26 | - add: workshop collection creation 27 | - add: auto set the name of the workshop addon after download 28 | - add: auto redownload items (disabled by default) 29 | - add: flat vpk list window 30 | - add: open workshop page button 31 | # v0.4.1 32 | - improve: select all text when edit text 33 | - fix: some workshop links can't be parsed 34 | - improve: auto refresh images after downloading 35 | - fix: add try/catch block on opening clipboard 36 | - fix: "Single" strategy of enablement problem 37 | # v0.4.0 38 | - fix: renaming bug 39 | - improve: change the deletion behavior from direct deletion to move to recycle bin (Windows only) 40 | - add: show in the file explorer (Windows only) 41 | - add: auto detect workshop item link in the clipboard 42 | - add: vpk priority setting 43 | # v0.3.0 44 | - add: creation time display 45 | - add: sort by creation time 46 | - add: randomly select 47 | - fix: fix some bugs 48 | - add: download list window 49 | - add: an error dialog box will pop up if the user opens the directory "left4dead2/addons" 50 | # v0.2.0 51 | - improve: AddonNodeView 52 | - fix: WorkshopVpkAddon.FullVpkFilePath is sometimes not updated 53 | - improve: WorkshopVpkAddonSectionView 54 | - improve: check addons after import and check addons before push 55 | - add: check for updates 56 | - add: addon problems display -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 0.6.1 4 | 5 | -------------------------------------------------------------------------------- /FireAxe.Core/AddonChildProblem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class AddonChildProblem : AddonProblem 6 | { 7 | public AddonChildProblem(AddonGroup source) : base(source) 8 | { 9 | 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /FireAxe.Core/AddonFileNotExistProblem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace FireAxe 8 | { 9 | public class AddonFileNotExistProblem : AddonProblem 10 | { 11 | private string _filePath; 12 | 13 | public AddonFileNotExistProblem(AddonNode source) : base(source) 14 | { 15 | _filePath = source.FilePath; 16 | } 17 | 18 | public string FilePath => _filePath; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /FireAxe.Core/AddonGroupSave.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class AddonGroupSave : AddonNodeSave 6 | { 7 | public override Type TargetType => typeof(AddonGroup); 8 | 9 | public AddonNodeSave[] Children { get; set; } = []; 10 | 11 | public AddonGroupEnableStrategy EnableStrategy { get; set; } = AddonGroupEnableStrategy.None; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /FireAxe.Core/AddonNameExistsException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class AddonNameExistsException : Exception 6 | { 7 | public AddonNameExistsException(string addonName) 8 | { 9 | ArgumentNullException.ThrowIfNull(addonName); 10 | AddonName = addonName; 11 | } 12 | public string AddonName { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /FireAxe.Core/AddonNodeContainerService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | 4 | namespace FireAxe 5 | { 6 | internal class AddonNodeContainerService 7 | { 8 | private ObservableCollection _nodes; 9 | private ReadOnlyObservableCollection _nodesReadOnly; 10 | 11 | private Dictionary _nodeNames = new(); 12 | 13 | public AddonNodeContainerService() 14 | { 15 | _nodes = new(); 16 | _nodesReadOnly = new(_nodes); 17 | } 18 | 19 | public ReadOnlyObservableCollection Nodes => _nodesReadOnly; 20 | 21 | public void AddUncheckName(AddonNode node) 22 | { 23 | ArgumentNullException.ThrowIfNull(node); 24 | 25 | ChangeNameUnchecked(null, node.Name, node); 26 | _nodes.Add(node); 27 | } 28 | 29 | public void Remove(AddonNode node) 30 | { 31 | ArgumentNullException.ThrowIfNull(node); 32 | 33 | var name = node.Name; 34 | if (name.Length > 0) 35 | { 36 | _nodeNames.Remove(name); 37 | } 38 | _nodes.Remove(node); 39 | } 40 | 41 | public string GetUniqueName(string name) 42 | { 43 | ArgumentNullException.ThrowIfNull(name); 44 | 45 | if (!NameExists(name)) 46 | { 47 | return name; 48 | } 49 | int i = 1; 50 | while (true) 51 | { 52 | string nameTry = name + $"({i})"; 53 | if (!NameExists(nameTry)) 54 | { 55 | return nameTry; 56 | } 57 | i++; 58 | } 59 | } 60 | 61 | public void ThrowIfNameInvalid(string name) 62 | { 63 | ArgumentNullException.ThrowIfNull(name); 64 | 65 | if (NameExists(name)) 66 | { 67 | throw new AddonNameExistsException(name); 68 | } 69 | } 70 | 71 | public bool NameExists(string name) 72 | { 73 | ArgumentNullException.ThrowIfNull(name); 74 | 75 | return _nodeNames.ContainsKey(name); 76 | } 77 | 78 | public void ChangeNameUnchecked(string? oldName, string newName, AddonNode node) 79 | { 80 | ArgumentNullException.ThrowIfNull(newName); 81 | ArgumentNullException.ThrowIfNull(node); 82 | 83 | if (oldName != null && oldName.Length > 0) 84 | { 85 | _nodeNames.Remove(oldName); 86 | } 87 | _nodeNames[newName] = node; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /FireAxe.Core/AddonNodeMoveDeniedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class AddonNodeMoveDeniedException : Exception 6 | { 7 | public AddonNode AddonNode { get; } 8 | 9 | public AddonNodeMoveDeniedException(AddonNode addonNode) 10 | { 11 | ArgumentNullException.ThrowIfNull(addonNode); 12 | AddonNode = addonNode; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /FireAxe.Core/AddonNodeSave.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | 4 | namespace FireAxe 5 | { 6 | public class AddonNodeSave 7 | { 8 | [JsonIgnore] 9 | public virtual Type TargetType => typeof(AddonNode); 10 | 11 | public bool IsEnabled { get; set; } = false; 12 | 13 | public string Name { get; set; } = ""; 14 | 15 | public DateTime CreationTime { get; set; } 16 | 17 | public string[] Tags { get; set; } = []; 18 | 19 | public string? CustomImagePath { get; set; } = null; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /FireAxe.Core/AddonProblem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public abstract class AddonProblem 6 | { 7 | public AddonProblem(AddonNode source) 8 | { 9 | ArgumentNullException.ThrowIfNull(source); 10 | Source = source; 11 | } 12 | 13 | public AddonNode Source { get; } 14 | 15 | public virtual bool CanAutoSolve => false; 16 | 17 | public bool TryAutoSolve() 18 | { 19 | if (OnAutoSolve()) 20 | { 21 | Source.RemoveProblem(this); 22 | return true; 23 | } 24 | else 25 | { 26 | return false; 27 | } 28 | } 29 | 30 | protected virtual bool OnAutoSolve() 31 | { 32 | return false; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /FireAxe.Core/AddonRootSave.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class AddonRootSave 6 | { 7 | public AddonNodeSave[] Nodes { get; set; } = Array.Empty(); 8 | 9 | public string[] CustomTags { get; set; } = []; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /FireAxe.Core/AddonTags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public static class AddonTags 6 | { 7 | #region Built-in Tags 8 | private static readonly HashSet s_builtInTags = 9 | [ 10 | "Survivors", 11 | "Bill", 12 | "Francis", 13 | "Louis", 14 | "Zoey", 15 | "Coach", 16 | "Ellis", 17 | "Nick", 18 | "Rochelle", 19 | 20 | "Common Infected", 21 | "Special Infected", 22 | "Boomer", 23 | "Charger", 24 | "Hunter", 25 | "Jockey", 26 | "Smoker", 27 | "Spitter", 28 | "Tank", 29 | "Witch", 30 | 31 | "Campaigns", 32 | "Weapons", 33 | "Items", 34 | "Sounds", 35 | "Scripts", 36 | "UI", 37 | "Miscellaneous", 38 | "Models", 39 | "Textures", 40 | 41 | "Single Player", 42 | "Co-op", 43 | "Versus", 44 | "Scavenge", 45 | "Survival", 46 | "Realism", 47 | "Realism Versus", 48 | "Mutations", 49 | 50 | "Grenade Launcher", 51 | "M60", 52 | "Melee", 53 | "Pistol", 54 | "Rifle", 55 | "Shotgun", 56 | "SMG", 57 | "Sniper", 58 | "Throwable", 59 | 60 | "Adrenaline", 61 | "Defibrillator", 62 | "Medkit", 63 | "Pills", 64 | "Other" 65 | ]; 66 | #endregion 67 | 68 | public static ISet BuiltInTags => s_builtInTags; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /FireAxe.Core/AutoUpdateStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public enum AutoUpdateStrategy 6 | { 7 | Default, 8 | Enabled, 9 | Disabled 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /FireAxe.Core/DisposableUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | internal static class DisposableUtils 6 | { 7 | private class Disposable : IDisposable 8 | { 9 | private Action _dispose; 10 | 11 | internal Disposable(Action dispose) 12 | { 13 | _dispose = dispose; 14 | } 15 | 16 | public void Dispose() 17 | { 18 | _dispose(); 19 | } 20 | } 21 | 22 | public static IDisposable Create(Action dispose) 23 | { 24 | ArgumentNullException.ThrowIfNull(dispose); 25 | 26 | return new Disposable(dispose); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /FireAxe.Core/DownloadFailedProblem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class DownloadFailedProblem : AddonProblem 6 | { 7 | public DownloadFailedProblem(AddonNode source) : base(source) 8 | { 9 | 10 | } 11 | 12 | public required string Url { get; init; } 13 | 14 | public required string FilePath { get; init; } 15 | 16 | public Exception? Exception { get; init; } = null; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /FireAxe.Core/DownloadStatus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public enum DownloadStatus 6 | { 7 | Preparing, 8 | PreparingAndPaused, 9 | Running, 10 | Paused, 11 | Succeeded, 12 | Cancelled, 13 | Failed 14 | } 15 | 16 | public static class DownloadStatusExtensions 17 | { 18 | public static bool IsCompleted(this DownloadStatus status) 19 | { 20 | return status == DownloadStatus.Succeeded || status == DownloadStatus.Cancelled || status == DownloadStatus.Failed; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /FireAxe.Core/FileExistException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class FileExistException : Exception 6 | { 7 | public FileExistException(string filePath) 8 | { 9 | FilePath = filePath; 10 | } 11 | 12 | public string FilePath { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /FireAxe.Core/FireAxe.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | FireAxe 8 | Debug;Release;release-win-x64 9 | AnyCPU;x64 10 | 11 | 12 | 13 | 14 | compile;contentfiles;analyzers;build 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /FireAxe.Core/GamePathUtils.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System; 3 | 4 | namespace FireAxe 5 | { 6 | public static class GamePathUtils 7 | { 8 | public static bool CheckValidity(string gamePath) 9 | { 10 | ArgumentNullException.ThrowIfNull(gamePath); 11 | 12 | if (!FileUtils.IsValidPath(gamePath) || !Path.IsPathRooted(gamePath)) 13 | { 14 | return false; 15 | } 16 | 17 | string appidPath = Path.Join(gamePath, "steam_appid.txt"); 18 | try 19 | { 20 | if (File.Exists(appidPath)) 21 | { 22 | return File.ReadAllText(appidPath).Trim(' ', '\n', '\0') == "550"; 23 | } 24 | } 25 | catch (Exception ex) 26 | { 27 | Log.Warning(ex, "Exception in GamePathUtils.CheckValidity."); 28 | } 29 | 30 | return false; 31 | } 32 | 33 | public static string GetAddonsPath(string gamePath) 34 | { 35 | ArgumentNullException.ThrowIfNull(gamePath); 36 | 37 | return Path.Join(gamePath, "left4dead2", "addons"); 38 | } 39 | 40 | public static string GetAddonListPath(string gamePath) 41 | { 42 | ArgumentNullException.ThrowIfNull(gamePath); 43 | 44 | return Path.Join(gamePath, "left4dead2", "addonlist.txt"); 45 | } 46 | 47 | public static bool IsAddonsPath(string path) 48 | { 49 | ArgumentNullException.ThrowIfNull(path); 50 | 51 | try 52 | { 53 | var path2 = Path.GetDirectoryName(path); 54 | if (path2 == null) 55 | { 56 | return false; 57 | } 58 | path2 = Path.GetDirectoryName(path2); 59 | if (path2 == null) 60 | { 61 | return false; 62 | } 63 | return CheckValidity(path2); 64 | } 65 | catch (Exception ex) 66 | { 67 | Log.Warning(ex, "Exception occurred during GamePathUtils.IsAddonsPath"); 68 | } 69 | 70 | return false; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /FireAxe.Core/IAddonNodeContainer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | 4 | namespace FireAxe 5 | { 6 | public interface IAddonNodeContainer : IHierarchyNode 7 | { 8 | IEnumerable IHierarchyNode.Children => Nodes; 9 | 10 | bool IHierarchyNode.IsNonterminal => true; 11 | 12 | ReadOnlyObservableCollection Nodes { get; } 13 | 14 | IAddonNodeContainer? Parent { get; } 15 | 16 | AddonRoot Root { get; } 17 | 18 | string GetUniqueNodeName(string name); 19 | } 20 | 21 | internal interface IAddonNodeContainerInternal 22 | { 23 | void ThrowIfNodeNameInvalid(string name); 24 | 25 | void ChangeNameUnchecked(string? oldName, string newName, AddonNode node); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /FireAxe.Core/IAddonNodeContainerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public static class IAddonNodeContainerExtensions 6 | { 7 | public static IEnumerable GetAllNodes(this IAddonNodeContainer container) 8 | { 9 | foreach (var node in container.GetDescendantsByDfsPreorder()) 10 | { 11 | yield return node; 12 | } 13 | } 14 | 15 | public static void CheckAll(this IAddonNodeContainer container) 16 | { 17 | foreach (var node in container.GetDescendantsByDfsPostorder()) 18 | { 19 | node.Check(); 20 | } 21 | if (container is AddonNode containerNode) 22 | { 23 | containerNode.Check(); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /FireAxe.Core/IDownloadItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public interface IDownloadItem : IDisposable 6 | { 7 | string Url { get; } 8 | 9 | string FilePath { get; } 10 | 11 | long DownloadedBytes { get; } 12 | 13 | long TotalBytes { get; } 14 | 15 | double BytesPerSecondSpeed { get; } 16 | 17 | DownloadStatus Status { get; } 18 | 19 | Exception Exception { get; } 20 | 21 | void Pause(); 22 | 23 | void Resume(); 24 | 25 | void Cancel(); 26 | 27 | void Wait(); 28 | 29 | Task WaitAsync(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /FireAxe.Core/IDownloadService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public interface IDownloadService : IDisposable 6 | { 7 | IDownloadItem Download(string url, string filePath); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /FireAxe.Core/IHierarchyNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace FireAxe 5 | { 6 | public interface IHierarchyNode where T : IHierarchyNode 7 | { 8 | bool IsNonterminal { get; } 9 | 10 | IEnumerable Children { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /FireAxe.Core/ISaveable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public interface ISaveable 6 | { 7 | bool RequestSave { get; set; } 8 | 9 | void Save(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /FireAxe.Core/InvalidGamePathException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class InvalidGamePathException : Exception 6 | { 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /FireAxe.Core/InvalidPublishedFileIdProblem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class InvalidPublishedFileIdProblem : AddonProblem 6 | { 7 | public InvalidPublishedFileIdProblem(WorkshopVpkAddon source) : base(source) 8 | { 9 | 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /FireAxe.Core/LocalVpkAddon.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class LocalVpkAddon : VpkAddon 6 | { 7 | private Guid _vpkGuid = Guid.Empty; 8 | 9 | public LocalVpkAddon(AddonRoot root, AddonGroup? group) : base(root, group) 10 | { 11 | 12 | } 13 | 14 | public override string? FullVpkFilePath => FullFilePath; 15 | 16 | public override string FileExtension => ".vpk"; 17 | 18 | public override Type SaveType => typeof(LocalVpkAddonSave); 19 | 20 | public Guid VpkGuid 21 | { 22 | get => _vpkGuid; 23 | set 24 | { 25 | if (NotifyAndSetIfChanged(ref _vpkGuid, value)) 26 | { 27 | Root.RequestSave = true; 28 | } 29 | } 30 | } 31 | 32 | protected override void OnCreateSave(AddonNodeSave save) 33 | { 34 | base.OnCreateSave(save); 35 | 36 | var save1 = (LocalVpkAddonSave)save; 37 | save1.VpkGuid = VpkGuid; 38 | } 39 | 40 | protected override void OnLoadSave(AddonNodeSave save) 41 | { 42 | base.OnLoadSave(save); 43 | 44 | var save1 = (LocalVpkAddonSave)save; 45 | VpkGuid = save1.VpkGuid; 46 | } 47 | 48 | public void ValidateVpkGuid() 49 | { 50 | if (VpkGuid == Guid.Empty) 51 | { 52 | GenerateVpkGuid(); 53 | } 54 | } 55 | 56 | public void GenerateVpkGuid() 57 | { 58 | VpkGuid = Guid.NewGuid(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /FireAxe.Core/LocalVpkAddonSave.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class LocalVpkAddonSave : VpkAddonSave 6 | { 7 | public override Type TargetType => typeof(LocalVpkAddon); 8 | 9 | public Guid VpkGuid { get; set; } = Guid.Empty; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /FireAxe.Core/MoveFileException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class MoveFileException : Exception 6 | { 7 | public MoveFileException(string sourcePath, string targetPath, Exception? innerException = null) : base(null, innerException) 8 | { 9 | SourcePath = sourcePath; 10 | TargetPath = targetPath; 11 | } 12 | 13 | public string SourcePath { get; } 14 | 15 | public string TargetPath { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /FireAxe.Core/ObservableObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace FireAxe 6 | { 7 | public abstract class ObservableObject : INotifyPropertyChanged 8 | { 9 | public event PropertyChangedEventHandler? PropertyChanged = null; 10 | 11 | protected bool NotifyAndSetIfChanged(ref T field, in T value, [CallerMemberName] string? propertyName = null) 12 | { 13 | ArgumentNullException.ThrowIfNull(propertyName); 14 | 15 | if (EqualityComparer.Default.Equals(value, field)) 16 | { 17 | return false; 18 | } 19 | 20 | field = value; 21 | NotifyChanged(propertyName); 22 | return true; 23 | } 24 | 25 | protected void NotifyChanged([CallerMemberName] string? propertyName = null) 26 | { 27 | ArgumentNullException.ThrowIfNull(propertyName); 28 | 29 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /FireAxe.Core/PublishedFileDetails.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | 4 | namespace FireAxe 5 | { 6 | public class PublishedFileDetails 7 | { 8 | public class TagObject 9 | { 10 | [JsonProperty("tag")] 11 | public required string Tag { get; init; } 12 | } 13 | 14 | [JsonProperty("file_url")] 15 | public required string FileUrl { get; init; } 16 | 17 | [JsonProperty("preview_url")] 18 | public required string PreviewUrl { get; init; } 19 | 20 | [JsonProperty("title")] 21 | public required string Title { get; init; } 22 | 23 | [JsonProperty("description")] 24 | public required string Description { get; init; } 25 | 26 | [JsonProperty("time_created")] 27 | public required ulong TimeCreated { get; init; } 28 | 29 | [JsonProperty("time_updated")] 30 | public required ulong TimeUpdated { get; init; } 31 | 32 | [JsonProperty("subscriptions")] 33 | public required uint Subscriptions { get; init; } 34 | 35 | [JsonProperty("favorited")] 36 | public required uint Favorited { get; init; } 37 | 38 | [JsonProperty("lifetime_subscriptions")] 39 | public required uint LifetimeSubscriptions { get; init; } 40 | 41 | [JsonProperty("lifetime_favorited")] 42 | public required uint LifetimeFavorited { get; init; } 43 | 44 | [JsonProperty("views")] 45 | public required uint Views { get; init; } 46 | 47 | [JsonProperty("tags")] 48 | public IReadOnlyList? Tags { get; init; } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /FireAxe.Core/PublishedFileDetailsUtils.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using Serilog; 4 | using System; 5 | using System.Text; 6 | 7 | namespace FireAxe 8 | { 9 | public static class PublishedFileDetailsUtils 10 | { 11 | public static async Task GetPublishedFileDetailsAsync(ulong publishedFileId, HttpClient httpClient, CancellationToken cancellationToken) 12 | { 13 | PublishedFileDetails? content = null; 14 | GetPublishedFileDetailsResultStatus status = GetPublishedFileDetailsResultStatus.Failed; 15 | 16 | try 17 | { 18 | var postContent = new StringContent($"itemcount=1&publishedfileids[0]={publishedFileId}", Encoding.UTF8, "application/x-www-form-urlencoded"); 19 | var response = await httpClient.PostAsync("https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/", postContent, cancellationToken).ConfigureAwait(false); 20 | var responseContentStr = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); 21 | var json = JObject.Parse(responseContentStr); 22 | if (json.TryGetValue("response", out var responseToken) && responseToken is JObject responseObj) 23 | { 24 | if (responseObj.TryGetValue("publishedfiledetails", out var detailsToken) && detailsToken is JArray detailsArray) 25 | { 26 | if (detailsArray.Count == 1) 27 | { 28 | var elementToken = detailsArray[0]; 29 | if (elementToken is JObject element) 30 | { 31 | if (element.TryGetValue("result", out var resultTypeToken) && resultTypeToken.Type == JTokenType.Integer) 32 | { 33 | int resultType = (int)resultTypeToken; 34 | if (resultType == 1) 35 | { 36 | if (element.TryGetValue("consumer_app_id", out var consumerAppIdToken) && consumerAppIdToken.Type == JTokenType.Integer && (int)consumerAppIdToken == 550) 37 | { 38 | content = element.ToObject(); 39 | } 40 | else 41 | { 42 | status = GetPublishedFileDetailsResultStatus.InvalidPublishedFileId; 43 | } 44 | 45 | } 46 | else if (resultType == 9) 47 | { 48 | status = GetPublishedFileDetailsResultStatus.InvalidPublishedFileId; 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | catch (OperationCanceledException) 57 | { 58 | throw; 59 | } 60 | catch (Exception ex) 61 | { 62 | Log.Warning(ex, "Exception occurred during the task of PublishedFileDetailsUtils.GetPublishedFileDetailsAsync."); 63 | } 64 | 65 | if (content != null) 66 | { 67 | status = GetPublishedFileDetailsResultStatus.Succeeded; 68 | } 69 | return new GetPublishedFileDetailsResult(content, status); 70 | } 71 | } 72 | 73 | public class GetPublishedFileDetailsResult 74 | { 75 | private PublishedFileDetails? _content; 76 | private GetPublishedFileDetailsResultStatus _status; 77 | 78 | internal GetPublishedFileDetailsResult(PublishedFileDetails? content, GetPublishedFileDetailsResultStatus status) 79 | { 80 | if (status == GetPublishedFileDetailsResultStatus.Succeeded && content == null) 81 | { 82 | throw new ArgumentNullException(nameof(content)); 83 | } 84 | 85 | _content = content; 86 | _status = status; 87 | } 88 | 89 | public PublishedFileDetails Content 90 | { 91 | get 92 | { 93 | if (!IsSucceeded) 94 | { 95 | throw new InvalidOperationException("The status isn't succeeded."); 96 | } 97 | return _content!; 98 | } 99 | } 100 | 101 | public GetPublishedFileDetailsResultStatus Status => _status; 102 | 103 | public bool IsSucceeded => _status == GetPublishedFileDetailsResultStatus.Succeeded; 104 | } 105 | 106 | public enum GetPublishedFileDetailsResultStatus 107 | { 108 | Succeeded, 109 | Failed, 110 | InvalidPublishedFileId, 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /FireAxe.Core/VpkAddon.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using SteamDatabase.ValvePak; 3 | using System; 4 | using System.Diagnostics.CodeAnalysis; 5 | 6 | namespace FireAxe 7 | { 8 | public abstract class VpkAddon : AddonNode 9 | { 10 | private int _vpkPriority = 0; 11 | 12 | private WeakReference _addonInfo = new(null); 13 | 14 | public VpkAddon(AddonRoot root, AddonGroup? group) : base(root, group) 15 | { 16 | 17 | } 18 | 19 | public int VpkPriority 20 | { 21 | get => _vpkPriority; 22 | set 23 | { 24 | if (NotifyAndSetIfChanged(ref _vpkPriority, value)) 25 | { 26 | Root.RequestSave = true; 27 | } 28 | } 29 | } 30 | 31 | public abstract string? FullVpkFilePath 32 | { 33 | get; 34 | } 35 | 36 | public override Type SaveType => typeof(VpkAddonSave); 37 | 38 | public override bool RequireFile => true; 39 | 40 | protected override long? GetFileSize() 41 | { 42 | var path = FullVpkFilePath; 43 | if (path == null) 44 | { 45 | return null; 46 | } 47 | 48 | try 49 | { 50 | if (File.Exists(path)) 51 | { 52 | return new FileInfo(path).Length; 53 | } 54 | } 55 | catch (Exception ex) 56 | { 57 | Log.Error(ex, "Exception occurred during VpkAddon.GetFileSize."); 58 | } 59 | 60 | return null; 61 | } 62 | 63 | protected override Task DoGetImageAsync(CancellationToken cancellationToken) 64 | { 65 | string? vpkPath = FullVpkFilePath; 66 | if (vpkPath == null) 67 | { 68 | return Task.FromResult(null); 69 | } 70 | return Task.Run(() => 71 | { 72 | if (TryCreatePackage(vpkPath, out var pak)) 73 | { 74 | using (pak) 75 | { 76 | cancellationToken.ThrowIfCancellationRequested(); 77 | return VpkUtils.GetAddonImage(pak); 78 | } 79 | } 80 | return null; 81 | }, cancellationToken); 82 | } 83 | 84 | public VpkAddonInfo? RetrieveInfo() 85 | { 86 | if (!_addonInfo.TryGetTarget(out var addonInfo)) 87 | { 88 | if (TryCreatePackage(FullVpkFilePath, out var pak)) 89 | { 90 | using (pak) 91 | { 92 | addonInfo = VpkUtils.GetAddonInfo(pak); 93 | if (addonInfo != null) 94 | { 95 | _addonInfo.SetTarget(addonInfo); 96 | } 97 | } 98 | } 99 | } 100 | return addonInfo; 101 | } 102 | 103 | public override void ClearCaches() 104 | { 105 | base.ClearCaches(); 106 | 107 | _addonInfo.SetTarget(null); 108 | } 109 | 110 | protected override void OnCreateSave(AddonNodeSave save) 111 | { 112 | base.OnCreateSave(save); 113 | var save1 = (VpkAddonSave)save; 114 | save1.VpkPriority = VpkPriority; 115 | } 116 | 117 | protected override void OnLoadSave(AddonNodeSave save) 118 | { 119 | base.OnLoadSave(save); 120 | var save1 = (VpkAddonSave)save; 121 | VpkPriority = save1.VpkPriority; 122 | } 123 | 124 | private static bool TryCreatePackage(string? path, [NotNullWhen(true)] out Package? pak) 125 | { 126 | pak = null; 127 | if (path == null) 128 | { 129 | return false; 130 | } 131 | try 132 | { 133 | pak = new Package(); 134 | pak.Read(path); 135 | return true; 136 | } 137 | catch (Exception ex) 138 | { 139 | Log.Warning(ex, "Exception in VpkAddon.TryCreatePackage."); 140 | if (pak != null) 141 | { 142 | pak.Dispose(); 143 | pak = null; 144 | } 145 | return false; 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /FireAxe.Core/VpkAddonInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class VpkAddonInfo 6 | { 7 | public string Version { get; set; } = ""; 8 | 9 | public string Title { get; set; } = ""; 10 | 11 | public string Author { get; set; } = ""; 12 | 13 | public string Description { get; set; } = ""; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /FireAxe.Core/VpkAddonSave.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class VpkAddonSave : AddonNodeSave 6 | { 7 | public override Type TargetType => typeof(VpkAddon); 8 | 9 | public int VpkPriority { get; set; } = 0; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /FireAxe.Core/VpkUtils.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using SteamDatabase.ValvePak; 3 | using System; 4 | using ValveKeyValue; 5 | 6 | namespace FireAxe 7 | { 8 | internal static class VpkUtils 9 | { 10 | public static VpkAddonInfo GetAddonInfo(Package pak) 11 | { 12 | var addonInfo = new VpkAddonInfo(); 13 | 14 | try 15 | { 16 | var addonInfoEntry = pak.FindEntry("addoninfo.txt"); 17 | if (addonInfoEntry != null) 18 | { 19 | pak.ReadEntry(addonInfoEntry, out byte[] data); 20 | var kv = KVSerializer.Create(KVSerializationFormat.KeyValues1Text); 21 | var stream = new MemoryStream(data); 22 | var document = kv.Deserialize(stream); 23 | 24 | var version = document["addonversion"]; 25 | if (version != null) 26 | { 27 | addonInfo.Version = ((string)version).Trim(); 28 | } 29 | 30 | var title = document["addontitle"]; 31 | if (title != null) 32 | { 33 | addonInfo.Title = ((string)title).Trim(); 34 | } 35 | 36 | var author = document["addonauthor"]; 37 | if (author != null) 38 | { 39 | addonInfo.Author = ((string)author).Trim(); 40 | } 41 | 42 | var desc = document["addonDescription"]; 43 | if (desc != null) 44 | { 45 | addonInfo.Description = (string)desc; 46 | } 47 | } 48 | } 49 | catch (Exception ex) 50 | { 51 | Log.Warning(ex, "Exception in VpkUtils.GetAddonInfo."); 52 | } 53 | 54 | return addonInfo; 55 | } 56 | 57 | public static byte[]? GetAddonImage(Package pak) 58 | { 59 | byte[]? image = null; 60 | try 61 | { 62 | var imageEntry = pak.FindEntry("addonimage.jpg"); 63 | if (imageEntry != null) 64 | { 65 | pak.ReadEntry(imageEntry, out image); 66 | } 67 | } 68 | catch (Exception ex) 69 | { 70 | Log.Warning(ex, "Exception in VpkUtils.GetAddonImage."); 71 | } 72 | return image; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /FireAxe.Core/WorkshopCollectionUtils.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using Serilog; 3 | using System; 4 | using System.Text; 5 | 6 | namespace FireAxe 7 | { 8 | public static class WorkshopCollectionUtils 9 | { 10 | public static async Task GetWorkshopCollectionContentAsync(ulong collectionId, bool includeLinkedCollections, HttpClient httpClient, CancellationToken cancellationToken) 11 | { 12 | if (!includeLinkedCollections) 13 | { 14 | var results = await GetRaw(collectionId).ConfigureAwait(false); 15 | if (results == null) 16 | { 17 | return null; 18 | } 19 | return results.Select(obj => obj.Id).ToArray(); 20 | } 21 | 22 | var resultIds = new List(); 23 | var handledCollections = new HashSet(); 24 | var queue = new Queue<(ulong Id, bool IsCollection)>(); 25 | queue.Enqueue((collectionId, true)); 26 | while (queue.Count > 0) 27 | { 28 | var next = queue.Dequeue(); 29 | if (next.IsCollection) 30 | { 31 | if (handledCollections.Add(next.Id)) 32 | { 33 | var results = await GetRaw(next.Id).ConfigureAwait(false); 34 | if (results == null) 35 | { 36 | return null; 37 | } 38 | foreach (var item in results) 39 | { 40 | queue.Enqueue(item); 41 | } 42 | } 43 | } 44 | else 45 | { 46 | resultIds.Add(next.Id); 47 | } 48 | } 49 | 50 | return resultIds.ToArray(); 51 | 52 | async Task?> GetRaw(ulong collectionId) 53 | { 54 | try 55 | { 56 | var postContent = new StringContent($"collectioncount=1&publishedfileids[0]={collectionId}", Encoding.UTF8, "application/x-www-form-urlencoded"); 57 | var response = await httpClient.PostAsync("https://api.steampowered.com/ISteamRemoteStorage/GetCollectionDetails/v1/", postContent, cancellationToken).ConfigureAwait(false); 58 | response.EnsureSuccessStatusCode(); 59 | var responseJson = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); 60 | var jobj = JObject.Parse(responseJson); 61 | var responseToken = jobj["response"]!; 62 | if ((int)responseToken["result"]! == 1) 63 | { 64 | var collectiondetailsToken = responseToken["collectiondetails"]![0]!; 65 | if ((int)collectiondetailsToken["result"]! == 1) 66 | { 67 | var childrenToken = collectiondetailsToken["children"]!; 68 | var results = new List<(ulong Id, bool IsCollection)>(); 69 | foreach (var childToken in childrenToken) 70 | { 71 | cancellationToken.ThrowIfCancellationRequested(); 72 | int fileType = (int)childToken["filetype"]!; 73 | if (fileType != 0 && fileType != 2) 74 | { 75 | continue; 76 | } 77 | bool isCollection = fileType == 2; 78 | if (isCollection && !includeLinkedCollections) 79 | { 80 | continue; 81 | } 82 | ulong id = (ulong)childToken["publishedfileid"]!; 83 | results.Add((id, isCollection)); 84 | } 85 | 86 | return results; 87 | } 88 | } 89 | } 90 | catch (OperationCanceledException) 91 | { 92 | throw; 93 | } 94 | catch (Exception ex) 95 | { 96 | Log.Warning(ex, "Exception occurred during WorkshopCollectionUtils.GetWorkshopCollectionContentAsync"); 97 | } 98 | 99 | return null; 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /FireAxe.Core/WorkshopVpkAddonSave.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class WorkshopVpkAddonSave : VpkAddonSave 6 | { 7 | public override Type TargetType => typeof(WorkshopVpkAddon); 8 | 9 | public ulong? PublishedFileId { get; set; } 10 | 11 | public AutoUpdateStrategy AutoUpdateStrategy { get; set; } 12 | 13 | public bool RequestAutoSetName { get; set; } = false; 14 | 15 | public bool RequestApplyTagsFromWorkshop { get; set; } = true; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /FireAxe.Core/WorkshopVpkFileNotLoadProblem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class WorkshopVpkFileNotLoadProblem : AddonProblem 6 | { 7 | public WorkshopVpkFileNotLoadProblem(WorkshopVpkAddon source) : base(source) 8 | { 9 | 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /FireAxe.Core/WorkshopVpkMetaInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public class WorkshopVpkMetaInfo 6 | { 7 | public required ulong PublishedFileId { get; init; } 8 | 9 | public required ulong TimeUpdated { get; init; } 10 | 11 | public required string CurrentFile { get; init; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /FireAxe.CrashReporter/FireAxe.CrashReporter.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | Debug;Release;release-win-x64 9 | AnyCPU;x64 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /FireAxe.CrashReporter/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Pipes; 3 | 4 | namespace FireAxe.CrashReporter 5 | { 6 | internal class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | Console.WriteLine("====FireAxe CrashReporter====\n"); 11 | 12 | if (args.Length == 0) 13 | { 14 | Console.WriteLine("Error: no args"); 15 | Console.ReadLine(); 16 | return; 17 | } 18 | 19 | var pipeName = args[0]; 20 | try 21 | { 22 | using (var pipeClient = new NamedPipeClientStream(pipeName)) 23 | { 24 | pipeClient.Connect(); 25 | using (var reader = new BinaryReader(pipeClient)) 26 | { 27 | Console.WriteLine(reader.ReadString()); 28 | } 29 | } 30 | } 31 | catch (IOException ex) 32 | { 33 | Console.WriteLine(ex); 34 | } 35 | 36 | Console.ReadLine(); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /FireAxe.GUI/AddonProblemExplanations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FireAxe.Resources; 3 | 4 | namespace FireAxe 5 | { 6 | internal static class AddonProblemExplanations 7 | { 8 | public static void Register(ObjectExplanationManager manager) 9 | { 10 | manager.Register((problem, arg) => string.Format(Texts.AddonFileNotExistProblemExplain, problem.FilePath)); 11 | manager.Register((problem, arg) => Texts.AddonChildProblemExplain); 12 | manager.Register((problem, arg) => Texts.InvalidPublishedFileIdMessage); 13 | manager.Register((problem, arg) => Texts.WorkshopVpkFileNotLoadProblemExplain); 14 | manager.Register((problem, arg) => Texts.AddonGroupEnableStrategyProblemExplain); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /FireAxe.GUI/App.axaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 26 | 27 | 30 | 31 | 40 | 41 | 45 | 46 | 51 | 52 | 57 | 58 | 62 | 63 | 67 | 68 | 71 | 74 | 77 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /FireAxe.GUI/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.ApplicationLifetimes; 4 | using Avalonia.Markup.Xaml; 5 | using Avalonia.Platform.Storage; 6 | using FireAxe.ViewModels; 7 | using FireAxe.Views; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using System; 10 | using System.IO; 11 | using System.Net.Http; 12 | using System.Net.Http.Headers; 13 | 14 | namespace FireAxe 15 | { 16 | public partial class App : Application 17 | { 18 | public const string DocumentDirectoryName = "FireAxe"; 19 | 20 | private string _documentDirectoryPath; 21 | 22 | public event Action? ShutdownRequested = null; 23 | 24 | public App() 25 | { 26 | _documentDirectoryPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), DocumentDirectoryName); 27 | Directory.CreateDirectory(_documentDirectoryPath); 28 | 29 | var httpClient = new HttpClient(); 30 | httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(".NET", "8.0")); 31 | 32 | Services = new ServiceCollection() 33 | .AddSingleton(this) 34 | .AddSingleton() 35 | .AddSingleton() 36 | .AddSingleton() 37 | .AddSingleton() 38 | .AddSingleton() 39 | .AddSingleton() 40 | .AddSingleton() 41 | .AddSingleton(httpClient) 42 | .BuildServiceProvider(); 43 | } 44 | 45 | //public static new App Current => (App?)Application.Current ?? throw new InvalidOperationException("The App.Current is null."); 46 | 47 | public IServiceProvider Services { get; } 48 | 49 | public string DocumentDirectoryPath => _documentDirectoryPath; 50 | 51 | public override void Initialize() 52 | { 53 | AvaloniaXamlLoader.Load(this); 54 | } 55 | 56 | public override void OnFrameworkInitializationCompleted() 57 | { 58 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 59 | { 60 | desktop.ShutdownMode = ShutdownMode.OnMainWindowClose; 61 | 62 | Services.GetRequiredService(); 63 | 64 | desktop.ShutdownRequested += (sender, args) => 65 | { 66 | ShutdownRequested?.Invoke(); 67 | }; 68 | 69 | var mainWindowViewModel = Services.GetRequiredService(); 70 | var mainWindow = Services.GetRequiredService().CreateMainWindow(mainWindowViewModel); 71 | desktop.MainWindow = mainWindow; 72 | mainWindow.Show(); 73 | 74 | var saveManager = Services.GetRequiredService(); 75 | saveManager.Register(Services.GetRequiredService()); 76 | saveManager.Register(Services.GetRequiredService()); 77 | saveManager.Run(); 78 | } 79 | 80 | base.OnFrameworkInitializationCompleted(); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /FireAxe.GUI/AppGlobal.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using Serilog; 3 | using System; 4 | using System.Net.Http; 5 | using System.Reflection; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace FireAxe 10 | { 11 | public static class AppGlobal 12 | { 13 | public const string GitHubApiUrl = "https://api.github.com/repos/ktxiaok/FireAxe"; 14 | 15 | public const string GithubReleasesUrl = "https://github.com/ktxiaok/FireAxe/releases"; 16 | 17 | public const string GithubRepoLink = "https://github.com/ktxiaok/FireAxe"; 18 | 19 | public const string License = "Apache-2.0 license"; 20 | 21 | public static Version Version => typeof(AppGlobal).Assembly.GetName().Version!; 22 | 23 | public static string VersionString => Version.ToString(3); 24 | 25 | public static async Task GetLatestVersionAsync(HttpClient httpClient, CancellationToken cancellationToken) 26 | { 27 | var url = GitHubApiUrl + "/releases/latest"; 28 | try 29 | { 30 | var response = await httpClient.GetAsync(url, cancellationToken); 31 | response.EnsureSuccessStatusCode(); 32 | var responseJson = await response.Content.ReadAsStringAsync(cancellationToken); 33 | var jobj = JObject.Parse(responseJson); 34 | if (jobj.TryGetValue("tag_name", out var tagNameToken)) 35 | { 36 | return (string?)tagNameToken; 37 | } 38 | } 39 | catch (Exception ex) 40 | { 41 | Log.Error(ex, "Exception occurred during AppGlobal.GetLatestVersionAsync"); 42 | } 43 | return null; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /FireAxe.GUI/AppWindowManager.cs: -------------------------------------------------------------------------------- 1 | using FireAxe.ViewModels; 2 | using FireAxe.Views; 3 | using System; 4 | using System.Net.Http; 5 | 6 | namespace FireAxe 7 | { 8 | public class AppWindowManager : IAppWindowManager 9 | { 10 | private AppSettingsViewModel _settingsViewModel; 11 | private DownloadItemListViewModel _downloadItemListViewModel; 12 | private HttpClient _httpClient; 13 | 14 | private MainWindow? _mainWindow = null; 15 | private WindowReference? _settingsWindow = null; 16 | private WindowReference? _downloadItemListWindow = null; 17 | private WindowReference? _aboutWindow = null; 18 | private WindowReference? _flatVpkAddonListWindow = null; 19 | private WindowReference? _tagManagerWindow = null; 20 | 21 | public AppWindowManager(AppSettingsViewModel settingsViewModel, DownloadItemListViewModel downloadItemListViewModel, HttpClient httpClient) 22 | { 23 | ArgumentNullException.ThrowIfNull(settingsViewModel); 24 | ArgumentNullException.ThrowIfNull(downloadItemListViewModel); 25 | ArgumentNullException.ThrowIfNull(httpClient); 26 | _settingsViewModel = settingsViewModel; 27 | _downloadItemListViewModel = downloadItemListViewModel; 28 | _httpClient = httpClient; 29 | } 30 | 31 | public MainWindow? MainWindow => _mainWindow; 32 | 33 | public MainWindow CreateMainWindow(MainWindowViewModel viewModel) 34 | { 35 | _mainWindow = new MainWindow() 36 | { 37 | DataContext = viewModel 38 | }; 39 | return _mainWindow; 40 | } 41 | 42 | public void OpenSettingsWindow() 43 | { 44 | if (_settingsWindow == null || _settingsWindow.Get() == null) 45 | { 46 | _settingsWindow = new(new AppSettingsWindow 47 | { 48 | DataContext = _settingsViewModel 49 | }); 50 | } 51 | var window = _settingsWindow.Get()!; 52 | window.Show(); 53 | window.Activate(); 54 | } 55 | 56 | public void OpenDownloadListWindow() 57 | { 58 | if (_downloadItemListWindow == null || _downloadItemListWindow.Get() == null) 59 | { 60 | _downloadItemListWindow = new(new DownloadItemListWindow() 61 | { 62 | DataContext = _downloadItemListViewModel 63 | }); 64 | } 65 | var window = _downloadItemListWindow.Get()!; 66 | window.Show(); 67 | window.Activate(); 68 | } 69 | 70 | public void OpenAboutWindow() 71 | { 72 | if (_aboutWindow == null || _aboutWindow.Get() == null) 73 | { 74 | _aboutWindow = new(new AboutWindow()); 75 | } 76 | var window = _aboutWindow.Get()!; 77 | window.Show(); 78 | window.Activate(); 79 | } 80 | 81 | public void OpenNewWorkshopCollectionWindow(AddonRoot addonRoot, AddonGroup? addonGroup) 82 | { 83 | var window = new NewWorkshopCollectionWindow() 84 | { 85 | DataContext = new NewWorkshopCollectionViewModel(addonRoot, addonGroup, _httpClient) 86 | }; 87 | window.Show(); 88 | } 89 | 90 | public void OpenFlatVpkAddonListWindow(MainWindowViewModel mainWindowViewModel) 91 | { 92 | if (_flatVpkAddonListWindow == null || _flatVpkAddonListWindow.Get() == null) 93 | { 94 | _flatVpkAddonListWindow = new(new FlatVpkAddonListWindow() 95 | { 96 | DataContext = new FlatVpkAddonListViewModel(mainWindowViewModel) 97 | }); 98 | } 99 | var window = _flatVpkAddonListWindow.Get()!; 100 | window.Show(); 101 | window.Activate(); 102 | } 103 | 104 | public void OpenTagManagerWindow(MainWindowViewModel mainWindowViewModel) 105 | { 106 | if (_tagManagerWindow == null || _tagManagerWindow.Get() == null) 107 | { 108 | _tagManagerWindow = new(new AddonTagManagerWindow() 109 | { 110 | DataContext = new AddonTagManagerViewModel(mainWindowViewModel) 111 | }); 112 | } 113 | var window = _tagManagerWindow.Get()!; 114 | window.Show(); 115 | window.Activate(); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /FireAxe.GUI/Assets/AppLogo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktxiaok/FireAxe/9be8c6e7a08dc1eb49822310e7802bf31e7489b7/FireAxe.GUI/Assets/AppLogo.ico -------------------------------------------------------------------------------- /FireAxe.GUI/Assets/avalonia-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktxiaok/FireAxe/9be8c6e7a08dc1eb49822310e7802bf31e7489b7/FireAxe.GUI/Assets/avalonia-logo.ico -------------------------------------------------------------------------------- /FireAxe.GUI/DataTemplates/ExceptionExplainer.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Controls.Templates; 3 | using FireAxe.Resources; 4 | using System; 5 | 6 | namespace FireAxe.DataTemplates 7 | { 8 | public class ExceptionExplainer : IDataTemplate 9 | { 10 | public ExceptionExplanationScene Scene { get; set; } = ExceptionExplanationScene.Default; 11 | 12 | public Control? Build(object? data) 13 | { 14 | Exception ex = (Exception)data!; 15 | string explain = ObjectExplanationManager.Default.TryGet(ex, Scene) ?? Texts.Error; 16 | return new TextBlock { Text = explain }; 17 | } 18 | 19 | public bool Match(object? data) 20 | { 21 | return data is Exception; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /FireAxe.GUI/DataTemplates/ViewLocator.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Controls.Templates; 3 | using FireAxe.ViewModels; 4 | using System; 5 | 6 | namespace FireAxe.DataTemplates 7 | { 8 | public class ViewLocator : IDataTemplate 9 | { 10 | 11 | public Control? Build(object? data) 12 | { 13 | if (data is null) 14 | return null; 15 | 16 | var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); 17 | var type = Type.GetType(name); 18 | 19 | if (type != null) 20 | { 21 | var control = (Control)Activator.CreateInstance(type)!; 22 | control.DataContext = data; 23 | return control; 24 | } 25 | 26 | return new TextBlock { Text = "Not Found: " + name }; 27 | } 28 | 29 | public bool Match(object? data) 30 | { 31 | return data is ViewModelBase; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /FireAxe.GUI/DescriptiveObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | internal class DescriptiveObject 6 | { 7 | public DescriptiveObject(string description) 8 | { 9 | Description = description; 10 | } 11 | 12 | public string Description { get; set; } 13 | 14 | public override string ToString() => Description; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /FireAxe.GUI/DesignHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace FireAxe 5 | { 6 | internal static class DesignHelper 7 | { 8 | public static AddonRoot CreateEmptyAddonRoot() 9 | { 10 | var root = new AddonRoot(); 11 | root.TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); 12 | return root; 13 | } 14 | 15 | public static AddonNode CreateTestAddonNode() 16 | { 17 | var root = CreateEmptyAddonRoot(); 18 | var node = new AddonNode(root); 19 | node.Name = "test_node"; 20 | return node; 21 | } 22 | 23 | public static AddonGroup CreateTestAddonGroup() 24 | { 25 | var root = CreateEmptyAddonRoot(); 26 | var group = new AddonGroup(root); 27 | group.Name = "test_group"; 28 | return group; 29 | } 30 | 31 | public static AddonRoot CreateTestAddonRoot() 32 | { 33 | var addonRoot = CreateEmptyAddonRoot(); 34 | AddTestAddonNodes(addonRoot); 35 | return addonRoot; 36 | } 37 | 38 | public static void AddTestAddonNodes(AddonRoot root) 39 | { 40 | AddonNode node1 = new(root); 41 | node1.Name = "node_1"; 42 | 43 | AddonNode node2 = new(root); 44 | node2.Name = "node_2_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; 45 | 46 | AddonGroup node3 = new(root); 47 | node3.Name = "node_3_group_single"; 48 | node3.EnableStrategy = AddonGroupEnableStrategy.Single; 49 | 50 | AddonNode node4 = new(root, node3); 51 | node4.Name = "node_4"; 52 | 53 | AddonGroup node5 = new(root, node3); 54 | node5.Name = "node_5_group_all"; 55 | node5.EnableStrategy = AddonGroupEnableStrategy.All; 56 | 57 | AddonNode node6 = new(root, node5); 58 | node6.Name = "node_6"; 59 | 60 | AddonNode node7 = new(root, node3); 61 | node7.Name = "node_7"; 62 | 63 | AddonNode node8 = new(root, node5); 64 | node8.Name = "node_8"; 65 | 66 | for (int i = 100; i <= 150; ++i) 67 | { 68 | var node = new AddonNode(root); 69 | node.Name = "node_" + i; 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /FireAxe.GUI/ErrorOperationReply.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public enum ErrorOperationReply 6 | { 7 | Abort, 8 | Skip, 9 | SkipAll 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /FireAxe.GUI/ExceptionExplanationScene.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe 4 | { 5 | public enum ExceptionExplanationScene 6 | { 7 | Default, 8 | Input 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /FireAxe.GUI/ExceptionExplanations.cs: -------------------------------------------------------------------------------- 1 | using FireAxe.Resources; 2 | using System; 3 | 4 | namespace FireAxe 5 | { 6 | internal static class ExceptionExplanations 7 | { 8 | public static void Register(ObjectExplanationManager manager) 9 | { 10 | manager.Register((exception, arg) => 11 | { 12 | if (arg is ExceptionExplanationScene scene) 13 | { 14 | if (scene == ExceptionExplanationScene.Input) 15 | { 16 | return Texts.InvalidInputMessage; 17 | } 18 | } 19 | return Texts.ExceptionOccurMessage + '\n' + exception.ToString(); 20 | }); 21 | manager.Register((exception, arg) => Texts.ItemNameExists); 22 | manager.Register((exception, arg) => string.Format(Texts.AddonMoveDeniedMessage, exception.AddonNode.FullName)); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /FireAxe.GUI/FireAxe.GUI.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | WinExe 4 | net8.0 5 | enable 6 | true 7 | app.manifest 8 | true 9 | FireAxe 10 | Assets/AppLogo.ico 11 | FireAxe 12 | Debug;Release;release-win-x64 13 | AnyCPU;x64 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Designer 51 | 52 | 53 | 54 | 55 | 56 | True 57 | True 58 | Texts.resx 59 | 60 | 61 | AddonNodeExplorerView.axaml 62 | 63 | 64 | AddonNodeNavBarItemView.axaml 65 | 66 | 67 | AddonNodeNavBarView.axaml 68 | 69 | 70 | FlatVpkAddonListWindow.axaml 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | PublicResXFileCodeGenerator 89 | Texts.Designer.cs 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /FireAxe.GUI/IAppWindowManager.cs: -------------------------------------------------------------------------------- 1 | using FireAxe.ViewModels; 2 | using FireAxe.Views; 3 | using System; 4 | 5 | namespace FireAxe 6 | { 7 | public interface IAppWindowManager 8 | { 9 | MainWindow? MainWindow { get; } 10 | 11 | MainWindow CreateMainWindow(MainWindowViewModel viewModel); 12 | 13 | void OpenSettingsWindow(); 14 | 15 | void OpenDownloadListWindow(); 16 | 17 | void OpenAboutWindow(); 18 | 19 | void OpenNewWorkshopCollectionWindow(AddonRoot addonRoot, AddonGroup? addonGroup); 20 | 21 | void OpenFlatVpkAddonListWindow(MainWindowViewModel mainWindowViewModel); 22 | 23 | void OpenTagManagerWindow(MainWindowViewModel mainWindowViewModel); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /FireAxe.GUI/LanguageManager.cs: -------------------------------------------------------------------------------- 1 | using FireAxe.Resources; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.Immutable; 5 | using System.Globalization; 6 | 7 | namespace FireAxe 8 | { 9 | public static class LanguageManager 10 | { 11 | private static ImmutableHashSet s_supportedLanguages = ["en-US", "zh-Hans"]; 12 | 13 | private static string? s_currentLanguage = null; 14 | 15 | public static IEnumerable SupportedLanguages => s_supportedLanguages; 16 | 17 | public static string? CurrentLanguage 18 | { 19 | get => s_currentLanguage; 20 | set 21 | { 22 | s_currentLanguage = value; 23 | var culture = value == null ? null : new CultureInfo(value); 24 | Texts.Culture = culture; 25 | if (culture != null) 26 | { 27 | CultureInfo.CurrentUICulture = culture; 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /FireAxe.GUI/MarkupExtensions/EnumValues.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Markup.Xaml; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace FireAxe.MarkupExtensions 6 | { 7 | public class EnumValues : MarkupExtension 8 | { 9 | private Type _enumType; 10 | 11 | public EnumValues(Type enumType) 12 | { 13 | ArgumentNullException.ThrowIfNull(enumType); 14 | if (!enumType.IsEnum) 15 | { 16 | throw new ArgumentException("The argument isn't a enum type."); 17 | } 18 | 19 | _enumType = enumType; 20 | } 21 | 22 | public override object ProvideValue(IServiceProvider serviceProvider) 23 | { 24 | return Enum.GetValues(_enumType).Cast().Distinct(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /FireAxe.GUI/ObjectExplanationManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace FireAxe 5 | { 6 | public class ObjectExplanationManager 7 | { 8 | private static ObjectExplanationManager s_default = new(); 9 | 10 | private Dictionary> _dict = new(); 11 | 12 | public static ObjectExplanationManager Default => s_default; 13 | 14 | public void Register(Type type, Func func) 15 | { 16 | ArgumentNullException.ThrowIfNull(type); 17 | ArgumentNullException.ThrowIfNull(func); 18 | 19 | _dict[type] = func; 20 | } 21 | 22 | public void Register(Func func) 23 | { 24 | ArgumentNullException.ThrowIfNull(func); 25 | 26 | Register(typeof(T), (obj, arg) => func((T)obj, arg)); 27 | } 28 | 29 | public string? TryGet(object obj, object? arg = null) 30 | { 31 | ArgumentNullException.ThrowIfNull(obj); 32 | 33 | Type? currentType = obj.GetType(); 34 | while (true) 35 | { 36 | if (_dict.TryGetValue(currentType, out var func)) 37 | { 38 | return func(obj, arg); 39 | } 40 | currentType = currentType.BaseType; 41 | if (currentType == null) 42 | { 43 | return null; 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /FireAxe.GUI/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.ReactiveUI; 3 | using Serilog; 4 | using System; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.IO.Pipes; 8 | using System.Threading.Tasks; 9 | 10 | namespace FireAxe 11 | { 12 | internal sealed class Program 13 | { 14 | // Initialization code. Don't use any Avalonia, third-party APIs or any 15 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 16 | // yet and stuff might break. 17 | [STAThread] 18 | public static void Main(string[] args) 19 | { 20 | SetupLogger(); 21 | TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; 22 | Log.Information("FireAxe Start (Version: {Version})", AppGlobal.VersionString); 23 | try 24 | { 25 | RegisterObjectExplanations(); 26 | 27 | BuildAvaloniaApp() 28 | .StartWithClassicDesktopLifetime(args); 29 | } 30 | catch (Exception ex) 31 | { 32 | Log.Fatal(ex, "Unhandled Exception"); 33 | try 34 | { 35 | OpenCrashReporter(ex); 36 | } 37 | catch (Exception ex2) 38 | { 39 | Log.Error(ex2, "Exception occurred during Program.OpenCrashReporter."); 40 | } 41 | throw; 42 | } 43 | finally 44 | { 45 | Log.CloseAndFlush(); 46 | } 47 | } 48 | 49 | // Avalonia configuration, don't remove; also used by visual designer. 50 | public static AppBuilder BuildAvaloniaApp() 51 | => AppBuilder.Configure() 52 | .UsePlatformDetect() 53 | .WithInterFont() 54 | .LogToTrace() 55 | .UseReactiveUI(); 56 | 57 | private static void SetupLogger() 58 | { 59 | Log.Logger = new LoggerConfiguration() 60 | .WriteTo.Console() 61 | .WriteTo.File("Logs/Log.txt", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 31) 62 | .CreateLogger(); 63 | } 64 | 65 | private static void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) 66 | { 67 | Log.Error(e.Exception, "Unobserved task exception occurred"); 68 | } 69 | 70 | private static void OpenCrashReporter(Exception ex) 71 | { 72 | string pipeName = Guid.NewGuid().ToString(); 73 | using var crashReporterProcess = new Process(); 74 | crashReporterProcess.StartInfo.FileName = "FireAxe.CrashReporter.exe"; 75 | crashReporterProcess.StartInfo.UseShellExecute = true; 76 | crashReporterProcess.StartInfo.Arguments = pipeName; 77 | crashReporterProcess.Start(); 78 | 79 | try 80 | { 81 | using (var pipeServer = new NamedPipeServerStream(pipeName)) 82 | { 83 | pipeServer.WaitForConnection(); 84 | 85 | string message = "FireAxe crashed due to an unhandled exception. The following is the details of the exception.\n" + ex.ToString(); 86 | using (var writer = new BinaryWriter(pipeServer)) 87 | { 88 | writer.Write(message); 89 | } 90 | } 91 | } 92 | catch (IOException ioEx) 93 | { 94 | Log.Warning(ioEx, "IOException occurred during Program.OpenCrashReporter."); 95 | } 96 | 97 | crashReporterProcess.WaitForExit(); 98 | } 99 | 100 | private static void RegisterObjectExplanations() 101 | { 102 | var defaultManager = ObjectExplanationManager.Default; 103 | ExceptionExplanations.Register(defaultManager); 104 | AddonProblemExplanations.Register(defaultManager); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /FireAxe.GUI/SaveManager.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Threading; 2 | using Serilog; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace FireAxe 7 | { 8 | public class SaveManager 9 | { 10 | public static readonly TimeSpan DefaultAutoSaveInterval = TimeSpan.FromSeconds(3); 11 | 12 | private bool _active = false; 13 | 14 | private List _saveables = new(); 15 | 16 | private DispatcherTimer _autoSaveTimer; 17 | 18 | public SaveManager(App app) 19 | { 20 | _autoSaveTimer = new(DefaultAutoSaveInterval, DispatcherPriority.Normal, (sender, e) => 21 | { 22 | SaveAll(); 23 | }); 24 | app.ShutdownRequested += () => 25 | { 26 | SaveAll(); 27 | }; 28 | } 29 | 30 | public IEnumerable Saveables => _saveables; 31 | 32 | public TimeSpan AutoSaveInterval 33 | { 34 | get => _autoSaveTimer.Interval; 35 | set => _autoSaveTimer.Interval = value; 36 | } 37 | 38 | public void Run() 39 | { 40 | if (_active) 41 | { 42 | return; 43 | } 44 | 45 | _autoSaveTimer.Start(); 46 | _active = true; 47 | } 48 | 49 | public void Register(ISaveable saveable) 50 | { 51 | _saveables.Add(saveable); 52 | } 53 | 54 | public void SaveAll(bool forceSave = false) 55 | { 56 | foreach (var saveable in _saveables) 57 | { 58 | if (forceSave || saveable.RequestSave) 59 | { 60 | try 61 | { 62 | saveable.Save(); 63 | saveable.RequestSave = false; 64 | } 65 | catch (Exception ex) 66 | { 67 | Log.Error(ex, "Exception occurred during ISaveable.Save. (ClassName: {ClassName})", saveable.GetType().FullName); 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /FireAxe.GUI/SelectionModelHelper.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls.Selection; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.Immutable; 5 | 6 | namespace FireAxe 7 | { 8 | public static class SelectionModelHelper 9 | { 10 | public static void Select(ISelectionModel selection, IEnumerable? items, Func? sourceConverter = null) 11 | { 12 | ArgumentNullException.ThrowIfNull(selection); 13 | 14 | var source = selection.Source; 15 | if (source == null) 16 | { 17 | return; 18 | } 19 | selection.Clear(); 20 | if (items == null) 21 | { 22 | return; 23 | } 24 | var itemSet = items.ToImmutableHashSet(); 25 | if (itemSet.Count == 0) 26 | { 27 | return; 28 | } 29 | int i = 0; 30 | foreach (var obj in source) 31 | { 32 | var objConverted = sourceConverter == null ? obj : sourceConverter(obj); 33 | if (objConverted != null && itemSet.Contains(objConverted)) 34 | { 35 | selection.Select(i); 36 | } 37 | i++; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /FireAxe.GUI/Utils.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System; 3 | using System.Diagnostics; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace FireAxe 7 | { 8 | internal static class Utils 9 | { 10 | private static readonly string[] s_byteUnits = ["B", "KiB", "MiB", "GiB", "TiB"]; 11 | 12 | public static void GetReadableBytes(double bytes, out double value, out string unit) 13 | { 14 | value = bytes; 15 | int i = 0; 16 | while (value >= 1000 && i < s_byteUnits.Length) 17 | { 18 | value /= 1024; 19 | i++; 20 | } 21 | unit = s_byteUnits[i]; 22 | } 23 | 24 | public static string GetReadableBytes(double bytes) 25 | { 26 | GetReadableBytes(bytes, out double value, out string unit); 27 | return value.ToString("F1") + unit; 28 | } 29 | 30 | public static void OpenWebsite(string url) 31 | { 32 | ArgumentNullException.ThrowIfNull(url); 33 | 34 | try 35 | { 36 | Process.Start(new ProcessStartInfo(url) 37 | { 38 | UseShellExecute = true 39 | }); 40 | } 41 | catch (Exception ex) 42 | { 43 | Log.Error(ex, "Exception occurred during Utils.OpenWebsite"); 44 | } 45 | } 46 | 47 | public static void ShowFileInExplorer(string path) 48 | { 49 | ArgumentNullException.ThrowIfNull(path); 50 | 51 | try 52 | { 53 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 54 | { 55 | Process.Start(new ProcessStartInfo() 56 | { 57 | FileName = "explorer.exe", 58 | Arguments = $" /select, {path}" 59 | }); 60 | } 61 | } 62 | catch (Exception ex) 63 | { 64 | Log.Error(ex, "Exception occurred during Utils.ShowFileInExplorer"); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /FireAxe.GUI/ValueConverters/EnumDescriptionConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Data; 2 | using Avalonia.Data.Converters; 3 | using FireAxe.Resources; 4 | using System; 5 | using System.Globalization; 6 | using System.Linq; 7 | using System.Reflection; 8 | 9 | namespace FireAxe.ValueConverters 10 | { 11 | public class EnumDescriptionConverter : IValueConverter 12 | { 13 | private static EnumDescriptionConverter s_instance = new(); 14 | 15 | public static EnumDescriptionConverter Instance => s_instance; 16 | 17 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 18 | { 19 | if (value == null) 20 | { 21 | return null; 22 | } 23 | var type = value.GetType(); 24 | if (!type.IsEnum) 25 | { 26 | return null; 27 | } 28 | 29 | var targetNames = Enum.GetNames(type).Where((name) => Enum.Parse(type, name).Equals(value)); 30 | string className = type.Name; 31 | string? resultText = null; 32 | foreach (string name in targetNames) 33 | { 34 | string key = $"Enum_{className}_{name}"; 35 | resultText = Texts.ResourceManager.GetString(key, Texts.Culture); 36 | if (resultText != null) 37 | { 38 | break; 39 | } 40 | } 41 | resultText ??= $"[missing text: Enum_{className}_{string.Join('|', targetNames)}]"; 42 | 43 | return resultText; 44 | } 45 | 46 | public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 47 | { 48 | return BindingOperations.DoNothing; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /FireAxe.GUI/ValueConverters/LanguageNativeNameConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Data; 2 | using Avalonia.Data.Converters; 3 | using FireAxe.Resources; 4 | using Serilog; 5 | using System; 6 | using System.Globalization; 7 | 8 | namespace FireAxe.ValueConverters 9 | { 10 | public class LanguageNativeNameConverter : IValueConverter 11 | { 12 | private static LanguageNativeNameConverter s_instance = new(); 13 | 14 | public static LanguageNativeNameConverter Instance => s_instance; 15 | 16 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 17 | { 18 | if (value == null) 19 | { 20 | return Texts.Default; 21 | } 22 | var strValue = value as string; 23 | if (strValue == null) 24 | { 25 | return value; 26 | } 27 | try 28 | { 29 | var languageCulture = new CultureInfo(strValue); 30 | return languageCulture.NativeName; 31 | } 32 | catch (Exception ex) 33 | { 34 | Log.Error(ex, "Exception occurred during LanguageNativeNameConverter.Convert."); 35 | } 36 | return value; 37 | } 38 | 39 | public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 40 | { 41 | return BindingOperations.DoNothing; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /FireAxe.GUI/ValueConverters/ObjectExplanationConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Data; 2 | using Avalonia.Data.Converters; 3 | using System; 4 | using System.Globalization; 5 | 6 | namespace FireAxe.ValueConverters 7 | { 8 | public class ObjectExplanationConverter : IValueConverter 9 | { 10 | private static ObjectExplanationConverter s_default = new(); 11 | 12 | public static ObjectExplanationConverter Default => s_default; 13 | 14 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 15 | { 16 | if (value == null) 17 | { 18 | return null; 19 | } 20 | var result = ObjectExplanationManager.Default.TryGet(value); 21 | if (result != null) 22 | { 23 | return result; 24 | } 25 | return value; 26 | } 27 | 28 | public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 29 | { 30 | return BindingOperations.DoNothing; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AddAddonTagViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace FireAxe.ViewModels 5 | { 6 | public class AddAddonTagViewModel : ViewModelBase 7 | { 8 | private string[] _existingTags; 9 | 10 | public AddAddonTagViewModel(AddonRoot addonRoot) 11 | { 12 | ArgumentNullException.ThrowIfNull(addonRoot); 13 | 14 | _existingTags = [.. AddonTags.BuiltInTags, .. addonRoot.CustomTags]; 15 | } 16 | 17 | public IEnumerable ExistingTags => _existingTags; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AddonGroupViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe.ViewModels 4 | { 5 | public class AddonGroupViewModel : AddonNodeViewModel 6 | { 7 | public AddonGroupViewModel(AddonGroup group) : base(group) 8 | { 9 | 10 | } 11 | 12 | public new AddonGroup AddonNode => (AddonGroup)base.AddonNode; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AddonGroupViewModelDesign.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace FireAxe.ViewModels 3 | { 4 | public class AddonGroupViewModelDesign : AddonGroupViewModel 5 | { 6 | public AddonGroupViewModelDesign() : base(DesignHelper.CreateTestAddonGroup()) 7 | { 8 | 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AddonNodeComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace FireAxe.ViewModels 5 | { 6 | public class AddonNodeComparer : IComparer 7 | { 8 | public AddonNodeComparer(AddonNodeSortMethod sortMethod, bool isAscendingOrder) 9 | { 10 | SortMethod = sortMethod; 11 | IsAscendingOrder = isAscendingOrder; 12 | } 13 | 14 | public AddonNodeSortMethod SortMethod { get; } 15 | 16 | public bool IsAscendingOrder { get; } 17 | 18 | public int Compare(AddonNode? x, AddonNode? y) 19 | { 20 | if (x == null && y == null) 21 | { 22 | return 0; 23 | } 24 | if (x != null && y == null) 25 | { 26 | return 1; 27 | } 28 | if (x == null && y != null) 29 | { 30 | return -1; 31 | } 32 | 33 | int result = SortMethod switch 34 | { 35 | AddonNodeSortMethod.Name => x!.Name.CompareTo(y!.Name), 36 | AddonNodeSortMethod.EnableState => GetEnableState(x!).CompareTo(GetEnableState(y!)), 37 | AddonNodeSortMethod.FileSize => x!.FileSize.GetValueOrDefault(0).CompareTo(y!.FileSize.GetValueOrDefault(0)), 38 | AddonNodeSortMethod.CreationTime => x!.CreationTime.CompareTo(y!.CreationTime), 39 | _ => 0 40 | }; 41 | 42 | if (!IsAscendingOrder) 43 | { 44 | result = -result; 45 | } 46 | 47 | return result; 48 | 49 | int GetEnableState(AddonNode node) 50 | { 51 | if (node.IsEnabled) 52 | { 53 | if (node.IsEnabledInHierarchy) 54 | { 55 | return 0; 56 | } 57 | else 58 | { 59 | return 1; 60 | } 61 | } 62 | else 63 | { 64 | return 2; 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AddonNodeContainerViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using DynamicData; 4 | using DynamicData.Alias; 5 | using DynamicData.Binding; 6 | using ReactiveUI; 7 | using System; 8 | using System.Collections.ObjectModel; 9 | using System.Reactive.Disposables; 10 | using System.Reactive.Linq; 11 | 12 | namespace FireAxe.ViewModels 13 | { 14 | public class AddonNodeContainerViewModel : ViewModelBase, IActivatableViewModel 15 | { 16 | private ReadOnlyObservableCollection? _nodes = null; 17 | private IDisposable? _nodesSubscription = null; 18 | private ReadOnlyObservableCollection? _nodeViewModels = null; 19 | 20 | private AddonNodeListItemViewKind _listItemViewKind = AddonNodeListItemViewKind.MediumTile; 21 | 22 | private readonly ObservableAsPropertyHelper _isGridView; 23 | private readonly ObservableAsPropertyHelper _isTileView; 24 | private readonly ObservableAsPropertyHelper _tileViewSize; 25 | 26 | public AddonNodeContainerViewModel() 27 | { 28 | _isGridView = this.WhenAnyValue(x => x.ListItemViewKind) 29 | .Select((kind) => kind == AddonNodeListItemViewKind.Grid) 30 | .ToProperty(this, nameof(IsGridView)); 31 | _isTileView = this.WhenAnyValue(x => x.ListItemViewKind) 32 | .Select((kind) => kind.IsTile()) 33 | .ToProperty(this, nameof(IsTileView)); 34 | _tileViewSize = this.WhenAnyValue(x => x.ListItemViewKind) 35 | .Select((kind) => (kind switch 36 | { 37 | AddonNodeListItemViewKind.MediumTile => (double?)Application.Current!.FindResource("size_addon_tile"), 38 | AddonNodeListItemViewKind.LargeTile => (double?)Application.Current!.FindResource("size_addon_tile_large"), 39 | AddonNodeListItemViewKind.SmallTile => (double?)Application.Current!.FindResource("size_addon_tile_small"), 40 | _ => null 41 | }).GetValueOrDefault(200)) 42 | .ToProperty(this, nameof(TileViewSize)); 43 | 44 | this.WhenActivated((CompositeDisposable disposables) => 45 | { 46 | this.WhenAnyValue(x => x.Nodes) 47 | .Subscribe(nodes => 48 | { 49 | TryDisposeNodesSubscription(); 50 | if (nodes != null) 51 | { 52 | _nodesSubscription = nodes.ToObservableChangeSet() 53 | .Select(node => new AddonNodeListItemViewModel(node, this)) 54 | .Bind(out var nodeViewModels) 55 | .Subscribe(); 56 | NodeViewModels = nodeViewModels; 57 | } 58 | }) 59 | .DisposeWith(disposables); 60 | 61 | Disposable.Create(() => 62 | { 63 | TryDisposeNodesSubscription(); 64 | }) 65 | .DisposeWith(disposables); 66 | }); 67 | } 68 | 69 | public ViewModelActivator Activator { get; } = new(); 70 | 71 | public ReadOnlyObservableCollection? Nodes 72 | { 73 | get => _nodes; 74 | set => this.RaiseAndSetIfChanged(ref _nodes, value); 75 | } 76 | 77 | public ReadOnlyObservableCollection? NodeViewModels 78 | { 79 | get => _nodeViewModels; 80 | private set => this.RaiseAndSetIfChanged(ref _nodeViewModels, value); 81 | } 82 | 83 | public AddonNodeListItemViewKind ListItemViewKind 84 | { 85 | get => _listItemViewKind; 86 | set => this.RaiseAndSetIfChanged(ref _listItemViewKind, value); 87 | } 88 | 89 | public bool IsGridView => _isGridView.Value; 90 | 91 | public bool IsTileView => _isTileView.Value; 92 | 93 | public double TileViewSize => _tileViewSize.Value; 94 | 95 | private void TryDisposeNodesSubscription() 96 | { 97 | if (_nodesSubscription != null) 98 | { 99 | _nodesSubscription.Dispose(); 100 | _nodesSubscription = null; 101 | _nodeViewModels = null; 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AddonNodeContainerViewModelDesign.cs: -------------------------------------------------------------------------------- 1 | using DynamicData; 2 | using DynamicData.Alias; 3 | using DynamicData.Binding; 4 | using System; 5 | using System.Collections.ObjectModel; 6 | 7 | namespace FireAxe.ViewModels 8 | { 9 | public class AddonNodeContainerViewModelDesign : AddonNodeContainerViewModel 10 | { 11 | private AddonRoot _root; 12 | 13 | public AddonNodeContainerViewModelDesign() 14 | { 15 | _root = DesignHelper.CreateEmptyAddonRoot(); 16 | DesignHelper.AddTestAddonNodes(_root); 17 | Nodes = _root.Nodes; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AddonNodeCustomizeImageViewModel.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | using System; 3 | using System.Reactive; 4 | using System.Reactive.Disposables; 5 | using System.Reactive.Linq; 6 | 7 | namespace FireAxe.ViewModels 8 | { 9 | public class AddonNodeCustomizeImageViewModel : ViewModelBase, IActivatableViewModel 10 | { 11 | private AddonNode _addonNode; 12 | 13 | public AddonNodeCustomizeImageViewModel(AddonNode addonNode) 14 | { 15 | ArgumentNullException.ThrowIfNull(addonNode); 16 | _addonNode = addonNode; 17 | 18 | SelectCustomImagePathCommand = ReactiveCommand.CreateFromTask(async () => 19 | { 20 | var path = await SelectCustomImagePathInteraction.Handle(Unit.Default); 21 | if (path == null) 22 | { 23 | return; 24 | } 25 | CustomImagePath = path; 26 | }); 27 | 28 | this.WhenActivated((CompositeDisposable disposables) => 29 | { 30 | _addonNode.WhenAnyValue(x => x.CustomImagePath) 31 | .Subscribe(_ => this.RaisePropertyChanged(nameof(CustomImagePath))) 32 | .DisposeWith(disposables); 33 | }); 34 | } 35 | 36 | public ViewModelActivator Activator { get; } = new(); 37 | 38 | public AddonNode AddonNode => _addonNode; 39 | 40 | public string CustomImagePath 41 | { 42 | get => _addonNode.CustomImagePath ?? ""; 43 | set 44 | { 45 | if (value.Length == 0) 46 | { 47 | _addonNode.CustomImagePath = null; 48 | } 49 | else 50 | { 51 | _addonNode.CustomImagePath = FileUtils.NormalizePath(value); 52 | } 53 | } 54 | } 55 | 56 | public ReactiveCommand SelectCustomImagePathCommand { get; } 57 | 58 | public Interaction SelectCustomImagePathInteraction { get; } = new(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AddonNodeEnableState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace FireAxe.ViewModels 8 | { 9 | public enum AddonNodeEnableState : byte 10 | { 11 | Disabled, 12 | Enabled, 13 | EnabledSuppressed 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AddonNodeExplorerViewModelDesign.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace FireAxe.ViewModels 8 | { 9 | public class AddonNodeExplorerViewModelDesign : AddonNodeExplorerViewModel 10 | { 11 | public AddonNodeExplorerViewModelDesign() : base(DesignHelper.CreateTestAddonRoot(), null!) 12 | { 13 | 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AddonNodeListItemViewKind.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe.ViewModels 4 | { 5 | public enum AddonNodeListItemViewKind : byte 6 | { 7 | Grid, 8 | MediumTile, 9 | SmallTile, 10 | LargeTile, 11 | TileBegin = MediumTile, 12 | TileEnd = LargeTile, 13 | } 14 | 15 | public static class AddonNodeListItemViewKindExtensions 16 | { 17 | public static bool IsTile(this AddonNodeListItemViewKind kind) 18 | { 19 | return kind >= AddonNodeListItemViewKind.TileBegin && kind <= AddonNodeListItemViewKind.TileEnd; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AddonNodeListItemViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe.ViewModels 4 | { 5 | public class AddonNodeListItemViewModel : AddonNodeSimpleViewModel 6 | { 7 | private readonly AddonNodeContainerViewModel? _containerViewModel; 8 | 9 | public AddonNodeListItemViewModel(AddonNode addonNode, AddonNodeContainerViewModel? containerViewModel) : base(addonNode) 10 | { 11 | _containerViewModel = containerViewModel; 12 | } 13 | 14 | public AddonNodeContainerViewModel? ContainerViewModel => _containerViewModel; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AddonNodeListItemViewModelDesign.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe.ViewModels 4 | { 5 | public class AddonNodeListItemViewModelDesign : AddonNodeListItemViewModel 6 | { 7 | public AddonNodeListItemViewModelDesign() : base(DesignHelper.CreateTestAddonNode(), null) 8 | { 9 | 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AddonNodeNavBarItemViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe.ViewModels 4 | { 5 | public class AddonNodeNavBarItemViewModel : ViewModelBase 6 | { 7 | private AddonNodeExplorerViewModel _explorerViewModel; 8 | 9 | private AddonGroup _addonGroup; 10 | 11 | public AddonNodeNavBarItemViewModel(AddonNodeExplorerViewModel explorerViewModel, AddonGroup addonGroup) 12 | { 13 | ArgumentNullException.ThrowIfNull(explorerViewModel); 14 | ArgumentNullException.ThrowIfNull(addonGroup); 15 | 16 | _explorerViewModel = explorerViewModel; 17 | _addonGroup = addonGroup; 18 | } 19 | 20 | public AddonGroup AddonGroup => _addonGroup; 21 | 22 | public void Goto() 23 | { 24 | _explorerViewModel.GotoGroup(_addonGroup); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AddonNodeSortMethod.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe.ViewModels 4 | { 5 | public enum AddonNodeSortMethod 6 | { 7 | Default, 8 | Name, 9 | EnableState, 10 | CreationTime, 11 | FileSize 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AddonNodeViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe.ViewModels 4 | { 5 | public class AddonNodeViewModel : AddonNodeSimpleViewModel 6 | { 7 | public AddonNodeViewModel(AddonNode addonNode) : base(addonNode) 8 | { 9 | 10 | } 11 | 12 | public static AddonNodeViewModel Create(AddonNode addonNode) 13 | { 14 | if (addonNode is AddonGroup group) 15 | { 16 | return new AddonGroupViewModel(group); 17 | } 18 | else if (addonNode is LocalVpkAddon localVpkAddon) 19 | { 20 | return new LocalVpkAddonViewModel(localVpkAddon); 21 | } 22 | else if (addonNode is WorkshopVpkAddon workshopVpkAddon) 23 | { 24 | return new WorkshopVpkAddonViewModel(workshopVpkAddon); 25 | } 26 | else 27 | { 28 | return new AddonNodeViewModel(addonNode); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AddonNodeViewModelDesign.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe.ViewModels 4 | { 5 | public class AddonNodeViewModelDesign : AddonNodeViewModel 6 | { 7 | public AddonNodeViewModelDesign() : base(DesignHelper.CreateTestAddonNode()) 8 | { 9 | 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/AppSettingsViewModel.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Reactive; 5 | using System.Reactive.Disposables; 6 | using System.Reactive.Linq; 7 | 8 | namespace FireAxe.ViewModels 9 | { 10 | public class AppSettingsViewModel : ViewModelBase, IActivatableViewModel 11 | { 12 | private AppSettings _settings; 13 | 14 | private IEnumerable _languageItemsSource; 15 | 16 | public AppSettingsViewModel(AppSettings settings) 17 | { 18 | ArgumentNullException.ThrowIfNull(settings); 19 | _settings = settings; 20 | 21 | _languageItemsSource = [null, .. LanguageManager.SupportedLanguages]; 22 | 23 | this.WhenActivated((CompositeDisposable disposables) => 24 | { 25 | 26 | }); 27 | 28 | SelectGamePathCommand = ReactiveCommand.CreateFromTask(async () => 29 | { 30 | var path = await ChooseDirectoryInteraction.Handle(Unit.Default); 31 | if (path != null) 32 | { 33 | _settings.GamePath = path; 34 | } 35 | }); 36 | } 37 | 38 | public ViewModelActivator Activator { get; } = new(); 39 | 40 | public AppSettings Settings => _settings; 41 | 42 | public IEnumerable LanguageItemsSource => _languageItemsSource; 43 | 44 | public ReactiveCommand SelectGamePathCommand { get; } 45 | 46 | public Interaction ChooseDirectoryInteraction { get; } = new(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/DownloadItemListViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Threading; 2 | using DynamicData; 3 | using DynamicData.Binding; 4 | using ReactiveUI; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Collections.ObjectModel; 8 | using System.Reactive.Disposables; 9 | using System.Reactive.Linq; 10 | 11 | namespace FireAxe.ViewModels 12 | { 13 | public class DownloadItemListViewModel : ViewModelBase, IActivatableViewModel, IDisposable 14 | { 15 | private static TimeSpan CleanInterval = TimeSpan.FromSeconds(0.5); 16 | 17 | private bool _disposed = false; 18 | 19 | private readonly ObservableCollection _downloadItems = new(); 20 | private readonly ReadOnlyObservableCollection _downloadItemViewModels; 21 | 22 | private IReadOnlyList? _selection = null; 23 | private readonly ObservableAsPropertyHelper _hasSelection; 24 | 25 | private IDisposable _cleanTimer; 26 | 27 | public DownloadItemListViewModel() 28 | { 29 | _downloadItems.ToObservableChangeSet() 30 | .Transform(downloadItem => new DownloadItemViewModel(downloadItem)) 31 | .Bind(out _downloadItemViewModels) 32 | .Subscribe(); 33 | 34 | _hasSelection = this.WhenAnyValue(x => x.Selection) 35 | .Select(selection => 36 | { 37 | if (selection == null) 38 | { 39 | return false; 40 | } 41 | return selection.Count > 0; 42 | }) 43 | .ToProperty(this, nameof(HasSelection)); 44 | 45 | _cleanTimer = DispatcherTimer.Run(() => 46 | { 47 | Clean(); 48 | return true; 49 | }, CleanInterval); 50 | 51 | this.WhenActivated((CompositeDisposable disposables) => 52 | { 53 | Disposable.Create(() => 54 | { 55 | Selection = null; 56 | }) 57 | .DisposeWith(disposables); 58 | }); 59 | } 60 | 61 | public ViewModelActivator Activator { get; } = new(); 62 | 63 | public ReadOnlyObservableCollection DownloadItemViewModels => _downloadItemViewModels; 64 | 65 | public IReadOnlyList? Selection 66 | { 67 | get => _selection; 68 | set 69 | { 70 | _selection = value; 71 | this.RaisePropertyChanged(); 72 | } 73 | } 74 | 75 | public IEnumerable SelectedDownloadItems 76 | { 77 | get 78 | { 79 | var selection = Selection; 80 | if (selection == null) 81 | { 82 | yield break; 83 | } 84 | foreach (var item in selection) 85 | { 86 | yield return item.DownloadItem; 87 | } 88 | } 89 | } 90 | 91 | public bool HasSelection => _hasSelection.Value; 92 | 93 | public void Add(IDownloadItem downloadItem) 94 | { 95 | ArgumentNullException.ThrowIfNull(downloadItem); 96 | 97 | _downloadItems.Add(downloadItem); 98 | } 99 | 100 | public void Remove(IDownloadItem downloadItem) 101 | { 102 | ArgumentNullException.ThrowIfNull(downloadItem); 103 | 104 | _downloadItems.Remove(downloadItem); 105 | } 106 | 107 | public void Clean() 108 | { 109 | int i = 0; 110 | while (i < _downloadItems.Count) 111 | { 112 | var downloadItem = _downloadItems[i]; 113 | if (downloadItem.Status.IsCompleted()) 114 | { 115 | _downloadItems.RemoveAt(i); 116 | } 117 | else 118 | { 119 | i++; 120 | } 121 | } 122 | } 123 | 124 | public void Pause() 125 | { 126 | foreach (var download in SelectedDownloadItems) 127 | { 128 | download.Pause(); 129 | } 130 | } 131 | 132 | public void Resume() 133 | { 134 | foreach (var download in SelectedDownloadItems) 135 | { 136 | download.Resume(); 137 | } 138 | } 139 | 140 | public void Cancel() 141 | { 142 | foreach (var download in SelectedDownloadItems) 143 | { 144 | download.Cancel(); 145 | } 146 | } 147 | 148 | public void Dispose() 149 | { 150 | if (_disposed) 151 | { 152 | return; 153 | } 154 | _disposed = true; 155 | 156 | _cleanTimer.Dispose(); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/FlatVpkAddonViewModel.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | using System; 3 | using System.Reactive.Disposables; 4 | 5 | namespace FireAxe.ViewModels 6 | { 7 | public class FlatVpkAddonViewModel : AddonNodeSimpleViewModel 8 | { 9 | private FlatVpkAddonListViewModel _listViewModel; 10 | 11 | public FlatVpkAddonViewModel(VpkAddon addon, FlatVpkAddonListViewModel listViewModel) : base(addon) 12 | { 13 | ArgumentNullException.ThrowIfNull(listViewModel); 14 | _listViewModel = listViewModel; 15 | 16 | this.WhenActivated((CompositeDisposable disposables) => 17 | { 18 | bool first = true; 19 | addon.WhenAnyValue(x => x.VpkPriority) 20 | .Subscribe(_ => 21 | { 22 | this.RaisePropertyChanged(nameof(Priority)); 23 | if (!first) 24 | { 25 | listViewModel.RequestSoftRefresh(); 26 | } 27 | first = false; 28 | }) 29 | .DisposeWith(disposables); 30 | }); 31 | } 32 | 33 | public new VpkAddon AddonNode => (VpkAddon)base.AddonNode; 34 | 35 | public string Priority 36 | { 37 | get => AddonNode.VpkPriority.ToString(); 38 | set 39 | { 40 | if (int.TryParse(value, out int priority)) 41 | { 42 | AddonNode.VpkPriority = priority; 43 | } 44 | } 45 | } 46 | 47 | public void TurnUpPriority() 48 | { 49 | AddonNode.VpkPriority++; 50 | } 51 | 52 | public void TurnDownPriority() 53 | { 54 | AddonNode.VpkPriority--; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/LocalVpkAddonViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace FireAxe.ViewModels 3 | { 4 | public class LocalVpkAddonViewModel : VpkAddonViewModel 5 | { 6 | public LocalVpkAddonViewModel(VpkAddon addon) : base(addon) 7 | { 8 | 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/NewWorkshopCollectionViewModel.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | using System; 3 | using System.Net.Http; 4 | using System.Reactive; 5 | using System.Reactive.Disposables; 6 | using System.Reactive.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using FireAxe.Resources; 10 | 11 | namespace FireAxe.ViewModels 12 | { 13 | public class NewWorkshopCollectionViewModel : ViewModelBase, IActivatableViewModel 14 | { 15 | private AddonRoot _addonRoot; 16 | private AddonGroup? _addonGroup; 17 | private HttpClient _httpClient; 18 | 19 | private string _collectionId = ""; 20 | private bool _includeLinkedCollections = true; 21 | 22 | private CancellationTokenSource? _createCts = null; 23 | 24 | private bool _created = false; 25 | 26 | private bool _active = false; 27 | 28 | public NewWorkshopCollectionViewModel(AddonRoot addonRoot, AddonGroup? addonGroup, HttpClient httpClient) 29 | { 30 | ArgumentNullException.ThrowIfNull(addonRoot); 31 | ArgumentNullException.ThrowIfNull(httpClient); 32 | _addonRoot = addonRoot; 33 | _addonGroup = addonGroup; 34 | _httpClient = httpClient; 35 | 36 | CreateCommand = ReactiveCommand.CreateFromTask(Create); 37 | 38 | this.WhenActivated((CompositeDisposable disposables) => 39 | { 40 | _active = true; 41 | 42 | Disposable.Create(() => 43 | { 44 | _active = false; 45 | 46 | _createCts?.Cancel(); 47 | }) 48 | .DisposeWith(disposables); 49 | }); 50 | } 51 | 52 | public event Action? Close = null; 53 | 54 | public ViewModelActivator Activator { get; } = new(); 55 | 56 | public string CollectionId 57 | { 58 | get => _collectionId; 59 | set => this.RaiseAndSetIfChanged(ref _collectionId, value); 60 | } 61 | 62 | public bool IncludeLinkedCollections 63 | { 64 | get => _includeLinkedCollections; 65 | set => this.RaiseAndSetIfChanged(ref _includeLinkedCollections, value); 66 | } 67 | 68 | public ReactiveCommand CreateCommand { get; } 69 | 70 | public Interaction ShowInvalidCollectionIdInteraction { get; } = new(); 71 | 72 | public Interaction ShowCreateFailedInteraction { get; } = new(); 73 | 74 | public async Task Create() 75 | { 76 | if (_created) 77 | { 78 | return; 79 | } 80 | ulong collectionId; 81 | if (!WorkshopVpkAddon.TryParsePublishedFileId(CollectionId, out collectionId)) 82 | { 83 | await ShowInvalidCollectionIdInteraction.Handle(Unit.Default); 84 | return; 85 | } 86 | 87 | _createCts = new(); 88 | ulong[]? itemIds = null; 89 | PublishedFileDetails? collectionDetails = null; 90 | try 91 | { 92 | var itemIdsTask = WorkshopCollectionUtils.GetWorkshopCollectionContentAsync(collectionId, _includeLinkedCollections, _httpClient, _createCts.Token); 93 | var collectionDetailsTask = PublishedFileDetailsUtils.GetPublishedFileDetailsAsync(collectionId, _httpClient, _createCts.Token); 94 | itemIds = await itemIdsTask; 95 | var getCollectionDetailsResult = await collectionDetailsTask; 96 | if (getCollectionDetailsResult.IsSucceeded) 97 | { 98 | collectionDetails = getCollectionDetailsResult.Content; 99 | } 100 | } 101 | catch (OperationCanceledException) { } 102 | 103 | _createCts.Dispose(); 104 | _createCts = null; 105 | 106 | if (itemIds == null || collectionDetails == null) 107 | { 108 | if (_active) 109 | { 110 | await ShowCreateFailedInteraction.Handle(Unit.Default); 111 | } 112 | return; 113 | } 114 | 115 | Close?.Invoke(); 116 | if (!_addonRoot.IsValid) 117 | { 118 | return; 119 | } 120 | if (_addonGroup != null && !_addonGroup.IsValid) 121 | { 122 | _addonGroup = null; 123 | } 124 | 125 | var collectionGroup = new AddonGroup(_addonRoot, _addonGroup); 126 | var collectionName = collectionGroup.Parent.GetUniqueNodeName(FileUtils.SanitizeFileName(collectionDetails.Title)); 127 | collectionGroup.Name = collectionName; 128 | foreach (var itemId in itemIds) 129 | { 130 | var addon = new WorkshopVpkAddon(_addonRoot, collectionGroup); 131 | addon.Name = addon.Parent.GetUniqueNodeName(Texts.UnnamedWorkshopAddon); 132 | addon.RequestAutoSetName = true; 133 | addon.PublishedFileId = itemId; 134 | } 135 | _created = true; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/UpdateRequestReply.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireAxe.ViewModels 4 | { 5 | public enum UpdateRequestReply 6 | { 7 | None, 8 | GoToDownload, 9 | Ignore 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | 3 | namespace FireAxe.ViewModels 4 | { 5 | public class ViewModelBase : ReactiveObject 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /FireAxe.GUI/ViewModels/VpkAddonViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Media.Imaging; 2 | using ReactiveUI; 3 | using System; 4 | using System.IO; 5 | using System.Reactive.Disposables; 6 | using System.Reactive.Linq; 7 | 8 | namespace FireAxe.ViewModels 9 | { 10 | public abstract class VpkAddonViewModel : AddonNodeViewModel 11 | { 12 | private VpkAddonInfo? _info = null; 13 | 14 | public VpkAddonViewModel(VpkAddon addon) : base(addon) 15 | { 16 | this.WhenActivated((CompositeDisposable disposables) => 17 | { 18 | addon.WhenAnyValue(x => x.VpkPriority) 19 | .Subscribe(priority => this.RaisePropertyChanged(nameof(VpkPriority))) 20 | .DisposeWith(disposables); 21 | }); 22 | } 23 | 24 | public new VpkAddon AddonNode => (VpkAddon)base.AddonNode; 25 | 26 | public string VpkPriority 27 | { 28 | get => AddonNode.VpkPriority.ToString(); 29 | set 30 | { 31 | if (int.TryParse(value, out int priority)) 32 | { 33 | AddonNode.VpkPriority = priority; 34 | } 35 | else 36 | { 37 | throw new ArgumentException(); 38 | } 39 | } 40 | } 41 | 42 | public VpkAddonInfo? Info 43 | { 44 | get => _info; 45 | private set => this.RaiseAndSetIfChanged(ref _info, value); 46 | } 47 | 48 | protected override void OnRefresh() 49 | { 50 | base.OnRefresh(); 51 | 52 | var addon = AddonNode; 53 | Info = addon.RetrieveInfo(); 54 | } 55 | 56 | protected override void OnClearCaches() 57 | { 58 | base.OnClearCaches(); 59 | 60 | Info = null; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AboutWindow.axaml: -------------------------------------------------------------------------------- 1 | 12 | 16 | 17 | 21 | 22 | 23 | 26 | 29 | 34 | 38 | 39 | 41 | 44 | 47 | 48 | 51 | 52 | 53 | 54 | 56 | 59 | 60 | 61 | 64 | 66 | 68 | 69 | 70 | 73 | 75 | 77 | 78 | 79 | 82 | 84 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AboutWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace FireAxe.Views; 6 | 7 | public partial class AboutWindow : Window 8 | { 9 | public AboutWindow() 10 | { 11 | InitializeComponent(); 12 | } 13 | } -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AddAddonTagWindow.axaml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | 20 | 24 | 27 | 25 | 26 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AddonNodeEnableButton.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Input; 3 | using Avalonia.Controls; 4 | using Avalonia.Media; 5 | using Avalonia.ReactiveUI; 6 | using FireAxe.ViewModels; 7 | using ReactiveUI; 8 | using System; 9 | using System.Reactive.Disposables; 10 | 11 | namespace FireAxe.Views 12 | { 13 | public partial class AddonNodeEnableButton : ReactiveUserControl 14 | { 15 | public AddonNodeEnableButton() 16 | { 17 | InitializeComponent(); 18 | 19 | DoubleTapped += AddonNodeEnableButton_DoubleTapped; 20 | 21 | var app = Application.Current!; 22 | var iconEnabled = (Geometry?)app.FindResource("icon_enabled"); 23 | var iconEnabledSuppressed = (Geometry?)app.FindResource("icon_enabled_suppressed"); 24 | var iconDisabled = (Geometry?)app.FindResource("icon_disabled"); 25 | this.WhenActivated((CompositeDisposable disposables) => 26 | { 27 | this.WhenAnyValue(x => x.ViewModel.EnableState) 28 | .Subscribe(enableState => 29 | { 30 | Geometry? iconData; 31 | Color color; 32 | if (enableState == AddonNodeEnableState.Enabled) 33 | { 34 | iconData = iconEnabled; 35 | color = Colors.Green; 36 | } 37 | else if (enableState == AddonNodeEnableState.EnabledSuppressed) 38 | { 39 | iconData = iconEnabledSuppressed; 40 | color = Colors.Orange; 41 | } 42 | else 43 | { 44 | iconData = iconDisabled; 45 | color = Colors.Red; 46 | } 47 | if (iconData != null) 48 | { 49 | icon.Data = iconData; 50 | } 51 | icon.Foreground = new SolidColorBrush(color); 52 | }) 53 | .DisposeWith(disposables); 54 | }); 55 | } 56 | 57 | private void AddonNodeEnableButton_DoubleTapped(object? sender, TappedEventArgs e) 58 | { 59 | e.Handled = true; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AddonNodeGridRowView.axaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 15 | 18 | 21 | 24 | 27 | 28 | 33 | 37 | 38 | 41 | 42 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 64 | 71 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AddonNodeGridRowView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace FireAxe.Views; 6 | 7 | public partial class AddonNodeGridRowView : AddonNodeListItemView 8 | { 9 | public AddonNodeGridRowView() 10 | { 11 | InitializeComponent(); 12 | } 13 | } -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AddonNodeListItemView.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Input; 3 | using Avalonia.ReactiveUI; 4 | using FireAxe.ViewModels; 5 | using ReactiveUI; 6 | using System; 7 | using System.Reactive.Disposables; 8 | 9 | namespace FireAxe.Views 10 | { 11 | public class AddonNodeListItemView : ReactiveUserControl 12 | { 13 | public AddonNodeListItemView() 14 | { 15 | this.WhenActivated((CompositeDisposable disposables) => 16 | { 17 | 18 | }); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AddonNodeNavBarItemView.axaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AddonNodeNavBarItemView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace FireAxe.Views; 6 | 7 | public partial class AddonNodeNavBarItemView : UserControl 8 | { 9 | public AddonNodeNavBarItemView() 10 | { 11 | InitializeComponent(); 12 | } 13 | } -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AddonNodeNavBarView.axaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AddonNodeNavBarView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace FireAxe.Views; 6 | 7 | public partial class AddonNodeNavBarView : UserControl 8 | { 9 | public AddonNodeNavBarView() 10 | { 11 | InitializeComponent(); 12 | } 13 | } -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AddonNodeSectionViewDecorator.axaml: -------------------------------------------------------------------------------- 1 | 7 | 14 | Content 15 | 16 | 17 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AddonNodeSectionViewDecorator.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace FireAxe.Views; 6 | 7 | public partial class AddonNodeSectionViewDecorator : UserControl 8 | { 9 | public AddonNodeSectionViewDecorator() 10 | { 11 | InitializeComponent(); 12 | } 13 | 14 | public AddonNodeSectionViewDecorator(Control control) : this() 15 | { 16 | decorator.Child = control; 17 | } 18 | } -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AddonNodeTileView.axaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 16 | 19 | 23 | 28 | 33 | 40 | 41 | 42 | 52 | 53 | 54 | 57 | 58 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AddonNodeTileView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace FireAxe.Views 4 | { 5 | public partial class AddonNodeTileView : AddonNodeListItemView 6 | { 7 | public AddonNodeTileView() 8 | { 9 | InitializeComponent(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AddonTagCheckBox.axaml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 16 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AddonTagCheckBox.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace FireAxe.Views; 6 | 7 | public partial class AddonTagCheckBox : UserControl 8 | { 9 | public AddonTagCheckBox() 10 | { 11 | InitializeComponent(); 12 | } 13 | 14 | public bool IsChecked 15 | { 16 | get => checkBox.IsChecked ?? false; 17 | set => checkBox.IsChecked = value; 18 | } 19 | } -------------------------------------------------------------------------------- /FireAxe.GUI/Views/AddonTagCheckBoxList.axaml: -------------------------------------------------------------------------------- 1 | 11 | 13 | 15 | 81 | 87 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/DownloadItemView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | using Avalonia.ReactiveUI; 5 | using FireAxe.ViewModels; 6 | using ReactiveUI; 7 | using System.Reactive.Disposables; 8 | 9 | namespace FireAxe.Views; 10 | 11 | public partial class DownloadItemView : ReactiveUserControl 12 | { 13 | public DownloadItemView() 14 | { 15 | InitializeComponent(); 16 | 17 | this.WhenActivated((CompositeDisposable disposables) => 18 | { 19 | 20 | }); 21 | } 22 | } -------------------------------------------------------------------------------- /FireAxe.GUI/Views/EditableTextBlock.axaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 29 | 36 | 37 | 41 | 46 | 53 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/FlatVpkAddonListItemView.axaml: -------------------------------------------------------------------------------- 1 | 11 | 14 | 17 | 20 | 23 | 24 | 25 | 31 | 32 | 35 | 40 | 45 | 51 | 52 | 53 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/FlatVpkAddonListItemView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | using Avalonia.ReactiveUI; 5 | using FireAxe.ViewModels; 6 | using ReactiveUI; 7 | using System.Reactive.Disposables; 8 | 9 | namespace FireAxe.Views; 10 | 11 | public partial class FlatVpkAddonListItemView : ReactiveUserControl 12 | { 13 | public FlatVpkAddonListItemView() 14 | { 15 | InitializeComponent(); 16 | 17 | this.WhenActivated((CompositeDisposable disposables) => 18 | { 19 | 20 | }); 21 | } 22 | } -------------------------------------------------------------------------------- /FireAxe.GUI/Views/FlatVpkAddonListWindow.axaml: -------------------------------------------------------------------------------- 1 | 14 | 16 | 18 | 21 | 47 | 52 | 53 | 54 | 55 | 58 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/FlatVpkAddonListWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.LogicalTree; 4 | using Avalonia.Markup.Xaml; 5 | using Avalonia.ReactiveUI; 6 | using FireAxe.ViewModels; 7 | using ReactiveUI; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Reactive.Disposables; 11 | 12 | namespace FireAxe.Views; 13 | 14 | public partial class FlatVpkAddonListWindow : ReactiveWindow 15 | { 16 | public FlatVpkAddonListWindow() 17 | { 18 | this.WhenAnyValue(x => x.ViewModel) 19 | .WhereNotNull() 20 | .Subscribe(viewModel => 21 | { 22 | viewModel.Select += ViewModel_Select; 23 | }); 24 | 25 | this.WhenActivated((CompositeDisposable disposables) => 26 | { 27 | 28 | }); 29 | 30 | InitializeComponent(); 31 | } 32 | 33 | private void ViewModel_Select(IEnumerable addons) 34 | { 35 | var listBox = FindListBox(); 36 | if (listBox == null) 37 | { 38 | return; 39 | } 40 | 41 | SelectionModelHelper.Select(listBox.Selection, addons, obj => 42 | { 43 | if (obj is FlatVpkAddonViewModel viewModel) 44 | { 45 | return viewModel.AddonNode; 46 | } 47 | return null; 48 | }); 49 | } 50 | 51 | private ListBox? FindListBox() 52 | { 53 | foreach (var obj in this.GetLogicalDescendants()) 54 | { 55 | if (obj is ListBox listBox) 56 | { 57 | return listBox; 58 | } 59 | } 60 | return null; 61 | } 62 | } -------------------------------------------------------------------------------- /FireAxe.GUI/Views/MainWindow.axaml: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /FireAxe.GUI/Views/NewWorkshopCollectionWindow.axaml: -------------------------------------------------------------------------------- 1 | 13 | 17 | 21 | 27 | 28 | 32 | 36 | 37 |