├── .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