├── .gitmodules
├── BeatSaberModManager
├── Resources
│ ├── Icons
│ │ └── Icon.ico
│ ├── Images
│ │ └── bsmg.jpg
│ ├── Localization
│ │ ├── Localizations.axaml
│ │ ├── en.axaml
│ │ └── de.axaml
│ └── Styles
│ │ ├── Brushes.axaml
│ │ ├── HyperlinkButton.axaml
│ │ ├── FluentTheme.axaml
│ │ ├── Controls.axaml
│ │ ├── IconHeader.axaml
│ │ └── ProgressRing.axaml
├── ViewModels
│ ├── ViewModelBase.cs
│ ├── ModGridItemViewModel.cs
│ ├── MainWindowViewModel.cs
│ ├── AssetInstallWindowViewModel.cs
│ └── DashboardViewModel.cs
├── Services
│ ├── Interfaces
│ │ ├── IStatusProgress.cs
│ │ ├── IGameLauncher.cs
│ │ ├── IGameVersionProvider.cs
│ │ ├── IInstallDirValidator.cs
│ │ ├── IUpdater.cs
│ │ ├── IGamePathsProvider.cs
│ │ ├── IHashProvider.cs
│ │ ├── IInstallDirLocator.cs
│ │ ├── IAssetProvider.cs
│ │ ├── IProtocolHandlerRegistrar.cs
│ │ ├── IModInstaller.cs
│ │ ├── IDependencyResolver.cs
│ │ └── IModProvider.cs
│ └── Implementations
│ │ ├── BeatSaber
│ │ ├── BeatSaberInstallDirValidator.cs
│ │ ├── ModelSaber
│ │ │ ├── ModelSaberAssetProvider.cs
│ │ │ └── ModelSaberModelInstaller.cs
│ │ ├── Playlists
│ │ │ ├── PlaylistAssetProvider.cs
│ │ │ └── PlaylistInstaller.cs
│ │ ├── BeatSaver
│ │ │ └── BeatSaverAssetProvider.cs
│ │ ├── BeatSaberGamePathsProvider.cs
│ │ ├── BeatMods
│ │ │ └── MD5HashProvider.cs
│ │ ├── BeatSaberGameLauncher.cs
│ │ ├── BeatSaberGameVersionProvider.cs
│ │ └── BeatSaberInstallDirLocator.cs
│ │ ├── Progress
│ │ └── StatusProgress.cs
│ │ ├── ProtocolHandlerRegistrars
│ │ ├── WindowsProtocolHandlerRegistrar.cs
│ │ └── LinuxProtocolHandlerRegistrar.cs
│ │ ├── DependencyManagement
│ │ └── SimpleDependencyResolver.cs
│ │ ├── Observables
│ │ └── DirectoryExistsObservable.cs
│ │ ├── Settings
│ │ └── JsonSettingsProvider.cs
│ │ ├── Updater
│ │ └── GitHubUpdater.cs
│ │ └── Http
│ │ └── HttpProgressClient.cs
├── Models
│ ├── Implementations
│ │ ├── Json
│ │ │ ├── PlaylistJsonSerializerContext.cs
│ │ │ ├── BeatSaverJsonSerializerContext.cs
│ │ │ ├── CommonJsonSerializerContext.cs
│ │ │ ├── GitHubJsonSerializerContext.cs
│ │ │ ├── BeatModsJsonSerializerContext.cs
│ │ │ ├── SettingsJsonSerializerContext.cs
│ │ │ └── VersionConverter.cs
│ │ ├── PlatformType.cs
│ │ ├── BeatSaber
│ │ │ ├── BeatSaver
│ │ │ │ ├── BeatSaverMapVersion.cs
│ │ │ │ ├── BeatSaverMapMetaData.cs
│ │ │ │ └── BeatSaverMap.cs
│ │ │ ├── Playlists
│ │ │ │ ├── PlaylistSong.cs
│ │ │ │ └── Playlist.cs
│ │ │ └── BeatMods
│ │ │ │ ├── BeatModsHash.cs
│ │ │ │ ├── BeatModsDependency.cs
│ │ │ │ ├── BeatModsDownload.cs
│ │ │ │ └── BeatModsMod.cs
│ │ ├── GitHub
│ │ │ ├── Asset.cs
│ │ │ └── Release.cs
│ │ ├── Progress
│ │ │ ├── StatusType.cs
│ │ │ └── ProgressInfo.cs
│ │ └── Settings
│ │ │ └── AppSettings.cs
│ └── Interfaces
│ │ ├── ISettings.cs
│ │ └── IMod.cs
├── Views
│ ├── Pages
│ │ ├── IntroPage.axaml.cs
│ │ ├── IntroPage.axaml
│ │ ├── ModsPage.axaml.cs
│ │ ├── SettingsPage.axaml.cs
│ │ ├── DashboardPage.axaml.cs
│ │ └── ModsPage.axaml
│ ├── App.axaml
│ ├── Converters
│ │ ├── IsUpToDateColorConverter.cs
│ │ ├── TopOnlyThicknessConverter.cs
│ │ └── StatusTypeEnumConverter.cs
│ ├── Theming
│ │ ├── Theme.cs
│ │ └── ThemeManager.cs
│ ├── Localization
│ │ ├── Language.cs
│ │ └── LocalizationManager.cs
│ ├── Windows
│ │ ├── ExceptionWindow.axaml.cs
│ │ ├── MainWindow.axaml.cs
│ │ ├── InstallFolderDialogWindow.axaml.cs
│ │ ├── ExceptionWindow.axaml
│ │ ├── AssetInstallWindow.axaml.cs
│ │ ├── InstallFolderDialogWindow.axaml
│ │ ├── AssetInstallWindow.axaml
│ │ └── MainWindow.axaml
│ ├── Controls
│ │ ├── HyperlinkButton.cs
│ │ ├── DataGridFuncGroupDescription.cs
│ │ ├── IconHeader.cs
│ │ ├── ProgressRing.cs
│ │ ├── SearchableDataGrid.cs
│ │ └── HamburgerMenu.cs
│ ├── Helpers
│ │ └── ResourceKeyBindingHelper.cs
│ └── App.axaml.cs
├── Utils
│ ├── PlatformUtils.cs
│ └── IOUtils.cs
├── TrimmerRoots.xml
├── Startup.cs
└── BeatSaberModManager.csproj
├── LICENSE
├── PKGBUILD
├── BeatSaberModManager.sln
├── README.md
└── .github
└── workflows
└── main.yml
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "BSIPA-Linux"]
2 | path = BSIPA-Linux
3 | url = https://github.com/geefr/BSIPA-Linux.git
4 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Resources/Icons/Icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/affederaffe/BeatSaberModManager/HEAD/BeatSaberModManager/Resources/Icons/Icon.ico
--------------------------------------------------------------------------------
/BeatSaberModManager/Resources/Images/bsmg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/affederaffe/BeatSaberModManager/HEAD/BeatSaberModManager/Resources/Images/bsmg.jpg
--------------------------------------------------------------------------------
/BeatSaberModManager/ViewModels/ViewModelBase.cs:
--------------------------------------------------------------------------------
1 | using ReactiveUI;
2 |
3 |
4 | namespace BeatSaberModManager.ViewModels
5 | {
6 | ///
7 | /// Defines a ViewModel.
8 | ///
9 | public class ViewModelBase : ReactiveObject;
10 | }
11 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Resources/Localization/Localizations.axaml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Resources/Styles/Brushes.axaml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 | #FF8B0000
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Interfaces/IStatusProgress.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | using BeatSaberModManager.Models.Implementations.Progress;
4 |
5 |
6 | namespace BeatSaberModManager.Services.Interfaces
7 | {
8 | ///
9 | /// Defines a provider for progress updates.
10 | ///
11 | public interface IStatusProgress : IProgress, IProgress;
12 | }
13 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/Json/PlaylistJsonSerializerContext.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | using BeatSaberModManager.Models.Implementations.BeatSaber.Playlists;
4 |
5 |
6 | namespace BeatSaberModManager.Models.Implementations.Json
7 | {
8 | [JsonSerializable(typeof(Playlist))]
9 | internal sealed partial class PlaylistJsonSerializerContext : JsonSerializerContext;
10 | }
11 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/Json/BeatSaverJsonSerializerContext.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | using BeatSaberModManager.Models.Implementations.BeatSaber.BeatSaver;
4 |
5 |
6 | namespace BeatSaberModManager.Models.Implementations.Json
7 | {
8 | [JsonSerializable(typeof(BeatSaverMap))]
9 | internal sealed partial class BeatSaverJsonSerializerContext : JsonSerializerContext;
10 | }
11 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Resources/Styles/HyperlinkButton.axaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/Json/CommonJsonSerializerContext.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text.Json.Serialization;
3 |
4 |
5 | namespace BeatSaberModManager.Models.Implementations.Json
6 | {
7 | [JsonSerializable(typeof(string[]))]
8 | [JsonSerializable(typeof(Dictionary))]
9 | internal sealed partial class CommonJsonSerializerContext : JsonSerializerContext;
10 | }
11 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/Json/GitHubJsonSerializerContext.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | using BeatSaberModManager.Models.Implementations.GitHub;
4 |
5 |
6 | namespace BeatSaberModManager.Models.Implementations.Json
7 | {
8 | [JsonSerializable(typeof(Asset))]
9 | [JsonSerializable(typeof(Release))]
10 | internal sealed partial class GitHubJsonSerializerContext : JsonSerializerContext;
11 | }
12 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/Json/BeatModsJsonSerializerContext.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text.Json.Serialization;
3 |
4 | using BeatSaberModManager.Models.Implementations.BeatSaber.BeatMods;
5 |
6 |
7 | namespace BeatSaberModManager.Models.Implementations.Json
8 | {
9 | [JsonSerializable(typeof(HashSet))]
10 | internal sealed partial class BeatModsModJsonSerializerContext : JsonSerializerContext;
11 | }
12 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Resources/Styles/FluentTheme.axaml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Interfaces/IGameLauncher.cs:
--------------------------------------------------------------------------------
1 | namespace BeatSaberModManager.Services.Interfaces
2 | {
3 | ///
4 | /// Provides a method to launch the game.
5 | ///
6 | public interface IGameLauncher
7 | {
8 | ///
9 | /// Launches the game.
10 | ///
11 | /// The game's installation directory.
12 | void LaunchGame(string installDir);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/Json/SettingsJsonSerializerContext.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | using BeatSaberModManager.Models.Implementations.Settings;
4 |
5 |
6 | namespace BeatSaberModManager.Models.Implementations.Json
7 | {
8 | [JsonSerializable(typeof(AppSettings))]
9 | [JsonSourceGenerationOptions(WriteIndented = true, IgnoreReadOnlyProperties = true)]
10 | internal sealed partial class SettingsJsonSerializerContext : JsonSerializerContext;
11 | }
12 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/Pages/IntroPage.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 |
3 |
4 | namespace BeatSaberModManager.Views.Pages
5 | {
6 | ///
7 | /// View for informational purposes.
8 | ///
9 | public partial class IntroPage : UserControl
10 | {
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | public IntroPage()
15 | {
16 | InitializeComponent();
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/PlatformType.cs:
--------------------------------------------------------------------------------
1 | namespace BeatSaberModManager.Models.Implementations
2 | {
3 | ///
4 | /// Indicates from which store the game has been installed.
5 | ///
6 | public enum PlatformType
7 | {
8 | ///
9 | /// The game was installed through Steam.
10 | ///
11 | Steam,
12 |
13 | ///
14 | /// The game was installed through the Oculus store.
15 | ///
16 | Oculus
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/BeatSaber/BeatSaver/BeatSaverMapVersion.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.Json.Serialization;
3 |
4 |
5 | namespace BeatSaberModManager.Models.Implementations.BeatSaber.BeatSaver
6 | {
7 | ///
8 | /// A version of a .
9 | ///
10 | public class BeatSaverMapVersion
11 | {
12 | ///
13 | /// The url to download the map from.
14 | ///
15 | [JsonPropertyName("downloadURL")]
16 | public required Uri DownloadUrl { get; init; }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Implementations/BeatSaber/BeatSaberInstallDirValidator.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.IO;
3 |
4 | using BeatSaberModManager.Services.Interfaces;
5 |
6 |
7 | namespace BeatSaberModManager.Services.Implementations.BeatSaber
8 | {
9 | ///
10 | public class BeatSaberInstallDirValidator : IInstallDirValidator
11 | {
12 | ///
13 | public bool ValidateInstallDir([NotNullWhen(true)] string? path) =>
14 | !string.IsNullOrEmpty(path) && File.Exists(Path.Join(path, "Beat Saber.exe"));
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Interfaces/IGameVersionProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 |
4 | namespace BeatSaberModManager.Services.Interfaces
5 | {
6 | ///
7 | /// Provides a method to detect a game's version.
8 | ///
9 | public interface IGameVersionProvider
10 | {
11 | ///
12 | /// Asynchronously detects a game's version.
13 | ///
14 | /// The game's installation directory.
15 | /// The game's version, or null when failed.
16 | Task DetectGameVersionAsync(string installDir);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Interfaces/IInstallDirValidator.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 |
3 |
4 | namespace BeatSaberModManager.Services.Interfaces
5 | {
6 | ///
7 | /// Provides a method to validate a game's installation.
8 | ///
9 | public interface IInstallDirValidator
10 | {
11 | ///
12 | /// Validates the game's installation.
13 | ///
14 | /// The path of the directory.
15 | /// True if the installation is valid, false otherwise.
16 | bool ValidateInstallDir([NotNullWhen(true)] string? path);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/Pages/IntroPage.axaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/BeatSaber/Playlists/PlaylistSong.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 |
4 | namespace BeatSaberModManager.Models.Implementations.BeatSaber.Playlists
5 | {
6 | ///
7 | /// A song of a .
8 | ///
9 | public class PlaylistSong
10 | {
11 | ///
12 | /// The map's unique identifier on https://beatsaver.com.
13 | ///
14 | [JsonPropertyName("key")]
15 | public string? Id { get; init; }
16 |
17 | ///
18 | /// The map's hash.
19 | ///
20 | [JsonPropertyName("hash")]
21 | public string? Hash { get; init; }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/GitHub/Asset.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.Json.Serialization;
3 |
4 |
5 | namespace BeatSaberModManager.Models.Implementations.GitHub
6 | {
7 | ///
8 | /// An asset of a .
9 | ///
10 | public class Asset
11 | {
12 | ///
13 | /// The name of the asset.
14 | ///
15 | [JsonPropertyName("name")]
16 | public required string Name { get; init; }
17 |
18 | ///
19 | /// The url to download the asset from.
20 | ///
21 | [JsonPropertyName("browser_download_url")]
22 | public required Uri DownloadUrl { get; init; }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/BeatSaber/BeatMods/BeatModsHash.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 |
4 | namespace BeatSaberModManager.Models.Implementations.BeatSaber.BeatMods
5 | {
6 | ///
7 | /// A file of a and it's MD5 hash.
8 | ///
9 | public class BeatModsHash
10 | {
11 | ///
12 | /// The MD5 hash for the .
13 | ///
14 | [JsonPropertyName("hash")]
15 | public required string Hash { get; init; }
16 |
17 | ///
18 | /// The relative file path.
19 | ///
20 | [JsonPropertyName("file")]
21 | public required string File { get; init; }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Interfaces/ISettings.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 |
4 | namespace BeatSaberModManager.Models.Interfaces
5 | {
6 | ///
7 | /// Provides a generic getter for settings.
8 | ///
9 | /// The type of the settings class.
10 | public interface ISettings
11 | {
12 | ///
13 | /// Gets the loaded settings instance.
14 | ///
15 | T Value { get; }
16 |
17 | ///
18 | /// Asynchronously loads the config.
19 | ///
20 | Task LoadAsync();
21 |
22 | ///
23 | /// Save the config to disk.
24 | ///
25 | Task SaveAsync();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/GitHub/Release.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text.Json.Serialization;
3 |
4 |
5 | namespace BeatSaberModManager.Models.Implementations.GitHub
6 | {
7 | ///
8 | /// A release on https://github.com.
9 | ///
10 | public class Release
11 | {
12 | ///
13 | /// The tag of the .
14 | ///
15 | [JsonPropertyName("tag_name")]
16 | public required string TagName { get; init; }
17 |
18 | ///
19 | /// All assets of the .
20 | ///
21 | [JsonPropertyName("assets")]
22 | public required IReadOnlyList Assets { get; init; }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/BeatSaber/BeatSaver/BeatSaverMapMetaData.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 |
4 | namespace BeatSaberModManager.Models.Implementations.BeatSaber.BeatSaver
5 | {
6 | ///
7 | /// Additional metadata for a .
8 | ///
9 | public class BeatSaverMapMetaData
10 | {
11 | ///
12 | /// The name of the mapper.
13 | ///
14 | [JsonPropertyName("levelAuthorName")]
15 | public required string LevelAuthorName { get; init; }
16 |
17 | ///
18 | /// The name of the song.
19 | ///
20 | [JsonPropertyName("songName")]
21 | public required string SongName { get; init; }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/App.axaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Interfaces/IUpdater.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 |
4 | namespace BeatSaberModManager.Services.Interfaces
5 | {
6 | ///
7 | /// Defines a method for updating the application.
8 | ///
9 | public interface IUpdater
10 | {
11 | ///
12 | /// Asynchronously checks for an update of the application.
13 | ///
14 | /// True when a newer version is available, false otherwise.
15 | ValueTask NeedsUpdateAsync();
16 |
17 | ///
18 | /// Asynchronously updates the application.
19 | ///
20 | /// 0 if the update succeeds, -1 otherwise.
21 | Task UpdateAsync();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/Converters/IsUpToDateColorConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 |
4 | using Avalonia.Data.Converters;
5 | using Avalonia.Media;
6 |
7 |
8 | namespace BeatSaberModManager.Views.Converters
9 | {
10 | ///
11 | /// Converts a boolean value to an .
12 | ///
13 | public class IsUpToDateColorConverter : IValueConverter
14 | {
15 | ///
16 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
17 | value is bool b ? b ? Brushes.Green : Brushes.Red : null;
18 |
19 | ///
20 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
21 | throw new NotSupportedException();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/BeatSaber/BeatMods/BeatModsDependency.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | using BeatSaberModManager.Models.Interfaces;
4 |
5 |
6 | namespace BeatSaberModManager.Models.Implementations.BeatSaber.BeatMods
7 | {
8 | ///
9 | /// Defines a dependency for a .
10 | ///
11 | public class BeatModsDependency
12 | {
13 | ///
14 | /// The name of the dependency.
15 | ///
16 | [JsonPropertyName("name")]
17 | public required string Name { get; init; }
18 |
19 | ///
20 | /// Cached to avoid finding it again by .
21 | ///
22 | [JsonIgnore]
23 | public IMod? DependingMod { get; set; }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Resources/Styles/Controls.axaml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/BeatSaber/BeatMods/BeatModsDownload.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text.Json.Serialization;
4 |
5 |
6 | namespace BeatSaberModManager.Models.Implementations.BeatSaber.BeatMods
7 | {
8 | ///
9 | /// Provides a download link as well as the mod's files and their hashes.
10 | ///
11 | public class BeatModsDownload
12 | {
13 | ///
14 | /// The url to download the mod from.
15 | ///
16 | [JsonPropertyName("url")]
17 | public required Uri Url { get; init; }
18 |
19 | ///
20 | /// The mod's files and their corresponding hashes.
21 | ///
22 | [JsonPropertyName("hashMd5")]
23 | public required IReadOnlyList Hashes { get; init; }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/Theming/Theme.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Styling;
2 |
3 |
4 | namespace BeatSaberModManager.Views.Theming
5 | {
6 | ///
7 | /// Represents a theme for Avalonia.
8 | ///
9 | public class Theme
10 | {
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | public Theme(string name, ThemeVariant themeVariant)
15 | {
16 | Name = name;
17 | ThemeVariant = themeVariant;
18 | }
19 |
20 | ///
21 | /// The name of the theme.
22 | ///
23 | public string Name { get; }
24 |
25 | ///
26 | /// The associated of the theme.
27 | ///
28 | public ThemeVariant ThemeVariant { get; }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Interfaces/IGamePathsProvider.cs:
--------------------------------------------------------------------------------
1 | namespace BeatSaberModManager.Services.Interfaces
2 | {
3 | ///
4 | /// Defines a method to get a game's AppData directory.
5 | ///
6 | public interface IGamePathsProvider
7 | {
8 | ///
9 | /// Gets the AppData directory of a game.
10 | ///
11 | /// The game's installation directory.
12 | /// The path of the game's AppData directory.
13 | string GetAppDataPath(string installDir);
14 |
15 | ///
16 | /// Gets the Logs directory of a game.
17 | ///
18 | /// The game's installation directory.
19 | /// The path of the game's Logs directory.
20 | string GetLogsPath(string installDir);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/Json/VersionConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 |
5 |
6 | namespace BeatSaberModManager.Models.Implementations.Json
7 | {
8 | ///
9 | public class VersionConverter : JsonConverter
10 | {
11 | ///
12 | public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
13 | Version.TryParse(reader.GetString() ?? string.Empty, out Version? version) ? version : new Version(0, 0, 0);
14 |
15 | ///
16 | public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options)
17 | {
18 | ArgumentNullException.ThrowIfNull(writer);
19 | ArgumentNullException.ThrowIfNull(value);
20 | writer.WriteStringValue(value.ToString());
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/Progress/StatusType.cs:
--------------------------------------------------------------------------------
1 | namespace BeatSaberModManager.Models.Implementations.Progress
2 | {
3 | ///
4 | /// Represents the status of an operation.
5 | ///
6 | public enum StatusType
7 | {
8 | ///
9 | /// There is no running operation.
10 | ///
11 | None,
12 |
13 | ///
14 | /// The operation is currently installing a resource.
15 | ///
16 | Installing,
17 |
18 | ///
19 | /// The operation is currently uninstalling a resource.
20 | ///
21 | Uninstalling,
22 |
23 | ///
24 | /// The operation ran to completion successfully.
25 | ///
26 | Completed,
27 |
28 | ///
29 | /// The operation failed.
30 | ///
31 | Failed
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/BeatSaber/Playlists/Playlist.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text.Json.Serialization;
3 |
4 |
5 | namespace BeatSaberModManager.Models.Implementations.BeatSaber.Playlists
6 | {
7 | ///
8 | /// A playlist of multiple songs.
9 | ///
10 | public class Playlist
11 | {
12 | ///
13 | /// The title of the playlist.
14 | ///
15 | [JsonPropertyName("playlistTitle")]
16 | public required string PlaylistTitle { get; init; }
17 |
18 | ///
19 | /// The author of the playlist.
20 | ///
21 | [JsonPropertyName("playlistAuthor")]
22 | public required string PlaylistAuthor { get; init; }
23 |
24 | ///
25 | /// The songs included in the playlist.
26 | ///
27 | [JsonPropertyName("songs")]
28 | public required IReadOnlyList Songs { get; init; }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/Localization/Language.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 |
3 | using Avalonia.Controls;
4 |
5 |
6 | namespace BeatSaberModManager.Views.Localization
7 | {
8 | ///
9 | /// Represents a language used for localization.
10 | ///
11 | public class Language
12 | {
13 | ///
14 | /// Initializes a new instance of the class.
15 | ///
16 | public Language(CultureInfo cultureInfo, IResourceProvider resourceProvider)
17 | {
18 | CultureInfo = cultureInfo;
19 | ResourceProvider = resourceProvider;
20 | }
21 |
22 | ///
23 | /// The of the language.
24 | ///
25 | public CultureInfo CultureInfo { get; }
26 |
27 | ///
28 | /// The resources used for localization.
29 | ///
30 | public IResourceProvider ResourceProvider { get; }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/Converters/TopOnlyThicknessConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 |
4 | using Avalonia;
5 | using Avalonia.Data.Converters;
6 |
7 |
8 | namespace BeatSaberModManager.Views.Converters
9 | {
10 | ///
11 | /// Converts a to only use its value.
12 | ///
13 | public class TopOnlyThicknessConverter : IValueConverter
14 | {
15 | ///
16 | public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
17 | value switch
18 | {
19 | Thickness thickness => new Thickness(0, thickness.Top, 0, 0),
20 | double d => new Thickness(0, d, 0, 0),
21 | _ => new Thickness()
22 | };
23 |
24 | ///
25 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new InvalidOperationException();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Interfaces/IHashProvider.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Threading.Tasks;
3 |
4 |
5 | namespace BeatSaberModManager.Services.Interfaces
6 | {
7 | ///
8 | /// Provides methods to calculate hashes.
9 | ///
10 | public interface IHashProvider
11 | {
12 | ///
13 | /// Asynchronously calculates the hash for a file.
14 | ///
15 | /// The path of the file.
16 | /// The string representation of the hash, or null when failed to read the file.
17 | Task CalculateHashForFileAsync(string path);
18 |
19 | ///
20 | /// Asynchronously calculates the hash for a .
21 | ///
22 | /// The to calculate the hash from.
23 | /// The string representation of the hash.
24 | Task CalculateHashForStreamAsync(Stream stream);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Interfaces/IInstallDirLocator.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | using BeatSaberModManager.Models.Implementations;
4 |
5 |
6 | namespace BeatSaberModManager.Services.Interfaces
7 | {
8 | ///
9 | /// Provides methods to locate an game's installation and detect the .
10 | ///
11 | public interface IInstallDirLocator
12 | {
13 | ///
14 | /// Asynchronously locates a game's installation directory, optionally asynchronously.
15 | ///
16 | /// The installation directory of the game if found, null otherwise.
17 | ValueTask LocateInstallDirAsync();
18 |
19 | ///
20 | /// Detects the of an installation.
21 | ///
22 | /// The game's installation directory.
23 | /// The detected .
24 | PlatformType DetectPlatform(string installDir);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Interfaces/IMod.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 |
4 | namespace BeatSaberModManager.Models.Interfaces
5 | {
6 | ///
7 | /// Defines a mod.
8 | ///
9 | public interface IMod
10 | {
11 | ///
12 | /// The mod's display name.
13 | ///
14 | string Name { get; }
15 |
16 | ///
17 | /// The mod's version.
18 | ///
19 | Version Version { get; }
20 |
21 | ///
22 | /// The description or summary of the mod.
23 | ///
24 | string Description { get; }
25 |
26 | ///
27 | /// The category of the mod.
28 | ///
29 | string Category { get; }
30 |
31 | ///
32 | /// A link which provides more resources about the mod.
33 | ///
34 | Uri MoreInfoLink { get; }
35 |
36 | ///
37 | /// Indicates if the mod must be installed.
38 | ///
39 | bool IsRequired { get; }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 affederaffe
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Implementations/BeatSaber/ModelSaber/ModelSaberAssetProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 |
4 | using BeatSaberModManager.Services.Interfaces;
5 |
6 |
7 | namespace BeatSaberModManager.Services.Implementations.BeatSaber.ModelSaber
8 | {
9 | ///
10 | public class ModelSaberAssetProvider : IAssetProvider
11 | {
12 | private readonly ModelSaberModelInstaller _modelSaberModelInstaller;
13 |
14 | ///
15 | /// Initializes a new instance of the class.
16 | ///
17 | public ModelSaberAssetProvider(ModelSaberModelInstaller modelSaberModelInstaller)
18 | {
19 | _modelSaberModelInstaller = modelSaberModelInstaller;
20 | }
21 |
22 | ///
23 | public string Protocol => "modelsaber";
24 |
25 | ///
26 | public Task InstallAssetAsync(string installDir, Uri uri, IStatusProgress? progress = null)
27 | => _modelSaberModelInstaller.InstallModelAsync(installDir, uri, progress);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Interfaces/IAssetProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 |
4 |
5 | namespace BeatSaberModManager.Services.Interfaces
6 | {
7 | ///
8 | /// Defines a method to install additional assets like maps, models or playlists.
9 | ///
10 | public interface IAssetProvider
11 | {
12 | ///
13 | /// The protocol that the specific implementation handles.
14 | ///
15 | string Protocol { get; }
16 |
17 | ///
18 | /// Asynchronously downloads and installs an asset.
19 | ///
20 | /// The game's installation directory.
21 | /// The to download the asset from.
22 | /// The has to match .
23 | /// Optionally track the progress of the operation.
24 | /// True when the installation succeeded, false otherwise.
25 | Task InstallAssetAsync(string installDir, Uri uri, IStatusProgress? progress = null);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Implementations/BeatSaber/Playlists/PlaylistAssetProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 |
4 | using BeatSaberModManager.Services.Interfaces;
5 |
6 |
7 | namespace BeatSaberModManager.Services.Implementations.BeatSaber.Playlists
8 | {
9 | ///
10 | public class PlaylistAssetProvider : IAssetProvider
11 | {
12 | private readonly PlaylistInstaller _playlistInstaller;
13 |
14 | ///
15 | public string Protocol => "bsplaylist";
16 |
17 | ///
18 | /// Initializes a new instance of the class.
19 | ///
20 | public PlaylistAssetProvider(PlaylistInstaller playlistInstaller)
21 | {
22 | _playlistInstaller = playlistInstaller;
23 | }
24 |
25 | ///
26 | public Task InstallAssetAsync(string installDir, Uri uri, IStatusProgress? progress = null)
27 | {
28 | ArgumentNullException.ThrowIfNull(uri);
29 | return _playlistInstaller.InstallPlaylistAsync(installDir, new Uri(uri.PathAndQuery[1..]), progress);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Implementations/BeatSaber/BeatSaver/BeatSaverAssetProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 |
4 | using BeatSaberModManager.Services.Interfaces;
5 |
6 |
7 | namespace BeatSaberModManager.Services.Implementations.BeatSaber.BeatSaver
8 | {
9 | ///
10 | public class BeatSaverAssetProvider : IAssetProvider
11 | {
12 | private readonly BeatSaverMapInstaller _beatSaverMapInstaller;
13 |
14 | ///
15 | /// Initializes a new instance of the class.
16 | ///
17 | public BeatSaverAssetProvider(BeatSaverMapInstaller beatSaverMapInstaller)
18 | {
19 | _beatSaverMapInstaller = beatSaverMapInstaller;
20 | }
21 |
22 | ///
23 | public string Protocol => "beatsaver";
24 |
25 | ///
26 | public Task InstallAssetAsync(string installDir, Uri uri, IStatusProgress? progress = null)
27 | {
28 | ArgumentNullException.ThrowIfNull(uri);
29 | return _beatSaverMapInstaller.InstallBeatSaverMapByKeyAsync(installDir, uri.Host, progress);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/PKGBUILD:
--------------------------------------------------------------------------------
1 | pkgname="beatsabermodmanager"
2 | _pkgname="BeatSaberModManager"
3 | pkgver="0.0.1"
4 | pkgrel="1"
5 | pkgdesc="Yet another mod installer for Beat Saber, heavily inspired by ModAssistant"
6 | arch=("x86_64")
7 | url="https://github.com/affederaffe/BeatSaberModManager"
8 | license=("MIT")
9 | depends=("dotnet-runtime")
10 | makedepends=("dotnet-sdk" "git" "imagemagick" "gendesk")
11 | options=("!strip")
12 | source=("$url/archive/v$pkgver.tar.gz")
13 | sha256sums=("e83160d6d64ebf9ca8516ce44de09a74a640b8725f5ffe3d08774655f96d2c6a")
14 |
15 | build() {
16 | cd "$_pkgname-$pkgver"
17 | git clone https://github.com/geefr/BSIPA-Linux.git
18 | dotnet publish -c Release -r linux-x64 --no-self-contained -p:EnableSingleFileAnalyzer=false --output ../$_pkgname
19 | }
20 |
21 | package() {
22 | convert "$_pkgname/Resources/Icons/Icon.ico" "$pkgname.png"
23 | gendesk -n --pkgname "$pkgname" --name "$_pkgname" --pkgdesc "$pkgdesc" --comment "$pkgdesc" --categories "Game;Utility" --icon "$pkgname.png"
24 | install -Dm755 "$_pkgname/$_pkgname" "$pkgdir/usr/bin/$pkgname"
25 | install -Dm644 "$pkgname.png" "$pkgdir/usr/share/pixmaps/$pkgname.png"
26 | install -Dm644 "$pkgname.desktop" "$pkgdir/usr/share/applications/$pkgname.desktop"
27 | }
28 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/Windows/ExceptionWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | using Avalonia;
4 | using Avalonia.Controls;
5 | using Avalonia.Interactivity;
6 |
7 |
8 | namespace BeatSaberModManager.Views.Windows
9 | {
10 | ///
11 | /// Dialog that displays an .
12 | ///
13 | public partial class ExceptionWindow : Window
14 | {
15 | ///
16 | /// [Required by Avalonia]
17 | ///
18 | public ExceptionWindow() { }
19 |
20 | ///
21 | /// Initializes a new instance of the class.
22 | ///
23 | /// The to display.
24 | public ExceptionWindow(Exception e)
25 | {
26 | ArgumentNullException.ThrowIfNull(e);
27 | InitializeComponent();
28 | ExtendClientAreaToDecorationsHint = !OperatingSystem.IsLinux();
29 | Margin = ExtendClientAreaToDecorationsHint ? WindowDecorationMargin : new Thickness();
30 | ExceptionTextBlock.Text = e.ToString();
31 | }
32 |
33 | private void OkButtonClicked(object? sender, RoutedEventArgs e) => Close();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Interfaces/IProtocolHandlerRegistrar.cs:
--------------------------------------------------------------------------------
1 | namespace BeatSaberModManager.Services.Interfaces
2 | {
3 | ///
4 | /// Registers and unregister the application to handle protocols.
5 | ///
6 | public interface IProtocolHandlerRegistrar
7 | {
8 | ///
9 | /// Checks if the application is already registered as a protocol handler for the specified protocol.
10 | ///
11 | /// The protocol to check for.
12 | /// True if the application is registered as a protocol handler, false otherwise.
13 | bool IsProtocolHandlerRegistered(string protocol);
14 |
15 | ///
16 | /// Registers the application as a handler for the specified protocol.
17 | ///
18 | /// The protocol or scheme.
19 | void RegisterProtocolHandler(string protocol);
20 |
21 | ///
22 | /// Unregisters the application as a handler for the specified protocol.
23 | ///
24 | /// The protocol or scheme.
25 | void UnregisterProtocolHandler(string protocol);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/Pages/ModsPage.axaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | using Avalonia.Collections;
4 | using Avalonia.ReactiveUI;
5 |
6 | using BeatSaberModManager.ViewModels;
7 | using BeatSaberModManager.Views.Controls;
8 |
9 |
10 | namespace BeatSaberModManager.Views.Pages
11 | {
12 | ///
13 | /// View for installing and uninstalling mods.
14 | ///
15 | public partial class ModsPage : ReactiveUserControl
16 | {
17 | ///
18 | /// [Required by Avalonia]
19 | ///
20 | public ModsPage() { }
21 |
22 | ///
23 | /// Initializes a new instance of the class.
24 | ///
25 | public ModsPage(ModsViewModel viewModel)
26 | {
27 | ArgumentNullException.ThrowIfNull(viewModel);
28 | InitializeComponent();
29 | ViewModel = viewModel;
30 | ModsDataGrid.ItemsSource = new DataGridCollectionView(viewModel.GridItems)
31 | {
32 | GroupDescriptions =
33 | {
34 | new DataGridFuncGroupDescription(static x => x.AvailableMod.Category)
35 | }
36 | };
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/BeatSaber/BeatSaver/BeatSaverMap.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text.Json.Serialization;
3 |
4 |
5 | namespace BeatSaberModManager.Models.Implementations.BeatSaber.BeatSaver
6 | {
7 | ///
8 | /// A Beatmap from https://beatsaver.com.
9 | ///
10 | public class BeatSaverMap
11 | {
12 | ///
13 | /// The map's unique identifier on https://beatsaver.com.
14 | ///
15 | [JsonPropertyName("id")]
16 | public required string Id { get; init; }
17 |
18 | ///
19 | /// The name of the map.
20 | ///
21 | [JsonPropertyName("name")]
22 | public required string Name { get; init; }
23 |
24 | ///
25 | /// Additional metadata.
26 | ///
27 | [JsonPropertyName("metadata")]
28 | public required BeatSaverMapMetaData MetaData { get; init; }
29 |
30 | ///
31 | /// All versions of the map.
32 | /// The last entry corresponds to the latest version.
33 | ///
34 | [JsonPropertyName("versions")]
35 | public required IReadOnlyList Versions { get; init; }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Implementations/BeatSaber/BeatSaberGamePathsProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Runtime.Versioning;
4 |
5 | using BeatSaberModManager.Services.Interfaces;
6 |
7 |
8 | namespace BeatSaberModManager.Services.Implementations.BeatSaber
9 | {
10 | ///
11 | public class BeatSaberGamePathsProvider : IGamePathsProvider
12 | {
13 | ///
14 | public string GetAppDataPath(string installDir) =>
15 | OperatingSystem.IsWindows() ? GetWindowsAppDataPath()
16 | : OperatingSystem.IsLinux() ? GetLinuxAppDataPath(installDir)
17 | : throw new PlatformNotSupportedException();
18 |
19 | ///
20 | public string GetLogsPath(string installDir) => Path.Join(installDir, "Logs");
21 |
22 | [SupportedOSPlatform("windows")]
23 | private static string GetWindowsAppDataPath() => Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData", "LocalLow", "Hyperbolic Magnetism");
24 |
25 | [SupportedOSPlatform("linux")]
26 | private static string GetLinuxAppDataPath(string installDir) => Path.Join(installDir, "../../compatdata/620980/pfx/drive_c/users/steamuser/AppData/LocalLow/Hyperbolic Magnetism");
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/Controls/HyperlinkButton.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | using Avalonia;
4 | using Avalonia.Controls;
5 |
6 | using BeatSaberModManager.Utils;
7 |
8 |
9 | namespace BeatSaberModManager.Views.Controls
10 | {
11 | ///
12 | /// A button that opens the specified when clicked.
13 | ///
14 | public class HyperlinkButton : Button
15 | {
16 | ///
17 | /// Defines the property.
18 | ///
19 | public static readonly DirectProperty UriProperty = AvaloniaProperty.RegisterDirect(nameof(Uri), static o => o.Uri, static (o, v) => o.Uri = v);
20 |
21 | ///
22 | /// The uri to open.
23 | ///
24 | public Uri? Uri
25 | {
26 | get => _uri;
27 | set => SetAndRaise(UriProperty, ref _uri, value);
28 | }
29 |
30 | private Uri? _uri;
31 |
32 | ///
33 | /// Opens the when it's valid and the is enabled.
34 | ///
35 | protected override void OnClick()
36 | {
37 | if (IsEffectivelyEnabled && Uri is not null)
38 | PlatformUtils.TryOpenUri(Uri);
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/BeatSaberModManager.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BeatSaberModManager", "BeatSaberModManager\BeatSaberModManager.csproj", "{BFAAAF7F-EFD8-425A-A1EC-E48E08095EEA}"
4 | EndProject
5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IPA-lib", "BSIPA-Linux\IPA-lib\IPA-lib.csproj", "{5203504D-A4DE-49CB-8C4F-D74BEA4384BF}"
6 | EndProject
7 | Global
8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
9 | Debug|Any CPU = Debug|Any CPU
10 | Release|Any CPU = Release|Any CPU
11 | EndGlobalSection
12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
13 | {BFAAAF7F-EFD8-425A-A1EC-E48E08095EEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
14 | {BFAAAF7F-EFD8-425A-A1EC-E48E08095EEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
15 | {BFAAAF7F-EFD8-425A-A1EC-E48E08095EEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
16 | {BFAAAF7F-EFD8-425A-A1EC-E48E08095EEA}.Release|Any CPU.Build.0 = Release|Any CPU
17 | {5203504D-A4DE-49CB-8C4F-D74BEA4384BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
18 | {5203504D-A4DE-49CB-8C4F-D74BEA4384BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
19 | {5203504D-A4DE-49CB-8C4F-D74BEA4384BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
20 | {5203504D-A4DE-49CB-8C4F-D74BEA4384BF}.Release|Any CPU.Build.0 = Release|Any CPU
21 | EndGlobalSection
22 | EndGlobal
23 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Implementations/BeatSaber/BeatMods/MD5HashProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using System.IO;
4 | using System.Security.Cryptography;
5 | using System.Threading.Tasks;
6 |
7 | using BeatSaberModManager.Services.Interfaces;
8 | using BeatSaberModManager.Utils;
9 |
10 |
11 | namespace BeatSaberModManager.Services.Implementations.BeatSaber.BeatMods
12 | {
13 | ///
14 | public class MD5HashProvider : IHashProvider
15 | {
16 | ///
17 | public async Task CalculateHashForFileAsync(string path)
18 | {
19 | #pragma warning disable CA2007
20 | await using FileStream? fileStream = IOUtils.TryOpenFile(path, FileMode.Open, FileAccess.Read);
21 | #pragma warning restore CA2007
22 | return fileStream is null ? null : await CalculateHashForStreamAsync(fileStream).ConfigureAwait(false);
23 | }
24 |
25 | ///
26 | [SuppressMessage("Security", "CA5351:Do Not Use Broken Cryptographic Algorithms")]
27 | public async Task CalculateHashForStreamAsync(Stream stream)
28 | {
29 | using MD5 md5 = MD5.Create();
30 | byte[] hash = await md5.ComputeHashAsync(stream).ConfigureAwait(false);
31 | return Convert.ToHexString(hash);
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Interfaces/IModInstaller.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | using BeatSaberModManager.Models.Interfaces;
4 |
5 |
6 | namespace BeatSaberModManager.Services.Interfaces
7 | {
8 | ///
9 | /// Provides methods to install and uninstall mods.
10 | ///
11 | public interface IModInstaller
12 | {
13 | ///
14 | /// Asynchronously installs multiple mods.
15 | ///
16 | /// The game's installation directory.
17 | /// The mod to install.
18 | /// True if the operation succeeds, false otherwise.
19 | Task InstallModAsync(string installDir, IMod modification);
20 |
21 | ///
22 | /// Asynchronously uninstalls multiple mods.
23 | ///
24 | /// The game's installation directory.
25 | /// The mod to uninstall.
26 | Task UninstallModAsync(string installDir, IMod modification);
27 |
28 | ///
29 | /// Removes all installed mods.
30 | ///
31 | /// The game's installation directory.
32 | void RemoveAllModFiles(string installDir);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/Windows/MainWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Reactive.Linq;
4 |
5 | using Avalonia;
6 | using Avalonia.Controls;
7 | using Avalonia.Controls.Chrome;
8 | using Avalonia.Controls.Primitives;
9 | using Avalonia.ReactiveUI;
10 | using Avalonia.VisualTree;
11 |
12 | using BeatSaberModManager.ViewModels;
13 |
14 | using ReactiveUI;
15 |
16 |
17 | namespace BeatSaberModManager.Views.Windows
18 | {
19 | ///
20 | /// Standard top-level view of the application.
21 | ///
22 | public partial class MainWindow : ReactiveWindow
23 | {
24 | ///
25 | /// [Required by Avalonia]
26 | ///
27 | public MainWindow() { }
28 |
29 | ///
30 | /// Initializes a new instance of the class.
31 | ///
32 | public MainWindow(MainWindowViewModel viewModel)
33 | {
34 | ArgumentNullException.ThrowIfNull(viewModel);
35 | InitializeComponent();
36 | ViewModel = viewModel;
37 | ExtendClientAreaToDecorationsHint = !OperatingSystem.IsLinux();
38 | viewModel.PickInstallDirInteraction.RegisterHandler(async context => context.SetOutput(await new InstallFolderDialogWindow().ShowDialog(this).ConfigureAwait(false)));
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Implementations/Progress/StatusProgress.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reactive.Subjects;
3 |
4 | using BeatSaberModManager.Models.Implementations.Progress;
5 | using BeatSaberModManager.Services.Interfaces;
6 |
7 |
8 | namespace BeatSaberModManager.Services.Implementations.Progress
9 | {
10 | ///
11 | public sealed class StatusProgress : IStatusProgress, IDisposable
12 | {
13 | private readonly Subject _progressValue = new();
14 | private readonly Subject _progressInfo = new();
15 |
16 | ///
17 | /// Signals when the progress value changes.
18 | ///
19 | public IObservable ProgressValue => _progressValue;
20 |
21 | ///
22 | /// Signals when the progress info changes.
23 | ///
24 | public IObservable ProgressInfo => _progressInfo;
25 |
26 | ///
27 | public void Report(double value) => _progressValue.OnNext(value * 100);
28 |
29 | ///
30 | public void Report(ProgressInfo value) => _progressInfo.OnNext(value);
31 |
32 | ///
33 | public void Dispose()
34 | {
35 | _progressValue.Dispose();
36 | _progressInfo.Dispose();
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/Windows/InstallFolderDialogWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reactive.Linq;
3 |
4 | using Avalonia;
5 | using Avalonia.Controls;
6 | using Avalonia.Interactivity;
7 | using Avalonia.Platform.Storage;
8 |
9 | using ReactiveUI;
10 |
11 |
12 | namespace BeatSaberModManager.Views.Windows
13 | {
14 | ///
15 | /// Dialog that asks the user to manually select the game's installation directory.
16 | ///
17 | public partial class InstallFolderDialogWindow : Window
18 | {
19 | ///
20 | /// Initializes a new instance of the class.
21 | ///
22 | public InstallFolderDialogWindow()
23 | {
24 | InitializeComponent();
25 | ExtendClientAreaToDecorationsHint = !OperatingSystem.IsLinux();
26 | Margin = ExtendClientAreaToDecorationsHint ? WindowDecorationMargin : new Thickness();
27 | ContinueButton.GetObservable(Button.ClickEvent)
28 | .SelectMany(_ => StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions()))
29 | .Where(static x => x.Count == 1)
30 | .Select(static x => x[0].Path.LocalPath)
31 | .ObserveOn(RxApp.MainThreadScheduler)
32 | .Subscribe(Close);
33 | }
34 |
35 | private void OnCancelButtonClicked(object? sender, RoutedEventArgs e) => Close(null);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/Controls/DataGridFuncGroupDescription.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Globalization;
4 |
5 | using Avalonia.Collections;
6 |
7 |
8 | namespace BeatSaberModManager.Views.Controls
9 | {
10 | ///
11 | /// A reflection-free .
12 | ///
13 | public class DataGridFuncGroupDescription : DataGridGroupDescription
14 | {
15 | private readonly Func _selector;
16 |
17 | ///
18 | /// Initializes a new instance of the class.
19 | ///
20 | /// A that returns the group key for an item.
21 | public DataGridFuncGroupDescription(Func selector)
22 | {
23 | _selector = selector;
24 | }
25 |
26 | ///
27 | public override object GroupKeyFromItem(object item, int level, CultureInfo culture)
28 | {
29 | object? result = null;
30 | if (item is TItem tItem)
31 | result = _selector.Invoke(tItem);
32 | return result ?? item;
33 | }
34 |
35 | ///
36 | public override bool KeysMatch(object groupKey, object itemKey) =>
37 | groupKey is TKey tGroupKey && itemKey is TKey tItemKey && EqualityComparer.Default.Equals(tGroupKey, tItemKey);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Implementations/BeatSaber/BeatSaberGameLauncher.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 |
4 | using BeatSaberModManager.Models.Implementations;
5 | using BeatSaberModManager.Services.Interfaces;
6 | using BeatSaberModManager.Utils;
7 |
8 |
9 | namespace BeatSaberModManager.Services.Implementations.BeatSaber
10 | {
11 | ///
12 | public class BeatSaberGameLauncher : IGameLauncher
13 | {
14 | private readonly IInstallDirLocator _installDirLocator;
15 |
16 | ///
17 | /// Initializes a new instance.
18 | ///
19 | public BeatSaberGameLauncher(IInstallDirLocator installDirLocator)
20 | {
21 | _installDirLocator = installDirLocator;
22 | }
23 |
24 | ///
25 | public void LaunchGame(string installDir)
26 | {
27 | switch (_installDirLocator.DetectPlatform(installDir))
28 | {
29 | case PlatformType.Steam:
30 | PlatformUtils.TryOpenUri(new Uri("steam://rungameid/620980"));
31 | break;
32 | case PlatformType.Oculus:
33 | PlatformUtils.TryStartProcess(new ProcessStartInfo("Beat Saber.exe") { WorkingDirectory = installDir }, out _);
34 | break;
35 | default:
36 | throw new InvalidOperationException("Could not detect platform.");
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/Windows/ExceptionWindow.axaml:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
21 |
22 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Services/Interfaces/IDependencyResolver.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | using BeatSaberModManager.Models.Interfaces;
4 |
5 |
6 | namespace BeatSaberModManager.Services.Interfaces
7 | {
8 | ///
9 | /// Provides methods to manage dependencies of s.
10 | ///
11 | public interface IDependencyResolver
12 | {
13 | ///
14 | /// Checks if other s depend on .
15 | ///
16 | /// The to check.
17 | /// True if is a dependency, false otherwise.
18 | bool IsDependency(IMod modification);
19 |
20 | ///
21 | /// Resolves all dependencies for and adds it as an dependent.
22 | ///
23 | /// The to resolve the dependencies for.
24 | /// All affected dependencies of .
25 | IEnumerable ResolveDependencies(IMod modification);
26 |
27 | ///
28 | /// Resolves all dependencies for and removes it as an dependent.
29 | ///
30 | /// The to unresolve the dependencies for.
31 | /// All affected dependencies of .
32 | IEnumerable UnresolveDependencies(IMod modification);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/Converters/StatusTypeEnumConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 |
4 | using Avalonia.Data.Converters;
5 |
6 | using BeatSaberModManager.Models.Implementations.Progress;
7 |
8 |
9 | namespace BeatSaberModManager.Views.Converters
10 | {
11 | ///
12 | /// Converts the of a to a localized string.
13 | ///
14 | public class StatusTypeEnumConverter : IValueConverter
15 | {
16 |
17 | ///
18 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
19 | value is StatusType progressInfo ? Convert(progressInfo) : null;
20 |
21 | ///
22 | /// Converts the of a to a localized string
23 | ///
24 | /// The to convert
25 | /// The localized string
26 | public static string Convert(StatusType statusType) =>
27 | statusType switch
28 | {
29 | StatusType.Installing => "Status:Installing",
30 | StatusType.Uninstalling => "Status:Uninstalling",
31 | StatusType.Completed => "Status:Completed",
32 | StatusType.Failed => "Status:Failed",
33 | _ => string.Empty
34 | };
35 |
36 | ///
37 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotSupportedException();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Resources/Styles/IconHeader.axaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
14 |
15 |
19 |
20 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Utils/PlatformUtils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Diagnostics.CodeAnalysis;
4 |
5 |
6 | namespace BeatSaberModManager.Utils
7 | {
8 | ///
9 | /// Utilities for platform specific operations.
10 | ///
11 | internal static class PlatformUtils
12 | {
13 | ///
14 | /// Attempts to use the standard program to open the .
15 | ///
16 | /// The uri to open.
17 | /// True if the operation succeeds, false otherwise.
18 | public static bool TryOpenUri(Uri uri) =>
19 | OperatingSystem.IsWindows()
20 | ? TryStartProcess(new ProcessStartInfo(uri.LocalPath) { UseShellExecute = true }, out _)
21 | : OperatingSystem.IsLinux() && TryStartProcess(new ProcessStartInfo("xdg-open", $"\"{uri}\""), out _);
22 |
23 | ///
24 | /// Attempts to start a new process.
25 | ///
26 | /// The information used to start the process.
27 | /// The when the operation succeeds.
28 | /// True if the operation succeeds, false otherwise.
29 | public static bool TryStartProcess(ProcessStartInfo startInfo, [NotNullWhen(true)] out Process? process)
30 | {
31 | try
32 | {
33 | process = Process.Start(startInfo);
34 | return process is not null;
35 | }
36 | catch (InvalidOperationException) { }
37 | catch (PlatformNotSupportedException) { }
38 |
39 | process = null;
40 | return false;
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/BeatSaberModManager/TrimmerRoots.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/Pages/SettingsPage.axaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 |
5 | using Avalonia.Controls;
6 | using Avalonia.Platform.Storage;
7 | using Avalonia.ReactiveUI;
8 |
9 | using BeatSaberModManager.ViewModels;
10 | using BeatSaberModManager.Views.Localization;
11 | using BeatSaberModManager.Views.Theming;
12 |
13 |
14 | namespace BeatSaberModManager.Views.Pages
15 | {
16 | ///
17 | /// View for user settings.
18 | ///
19 | public partial class SettingsPage : ReactiveUserControl
20 | {
21 | ///
22 | /// [Required by Avalonia]
23 | ///
24 | public SettingsPage() { }
25 |
26 | ///
27 | /// Initializes a new instance of the class.
28 | ///
29 | public SettingsPage(SettingsViewModel viewModel, Window window, LocalizationManager localizationManager, ThemeManager themeManager)
30 | {
31 | ArgumentNullException.ThrowIfNull(viewModel);
32 | InitializeComponent();
33 | LanguagesComboBox.DataContext = localizationManager;
34 | ThemesComboBox.DataContext = themeManager;
35 | ViewModel = viewModel;
36 | viewModel.PickInstallDirInteraction.RegisterHandler(async context => context.SetOutput(await SelectInstallDirAsync(window).ConfigureAwait(false)));
37 | }
38 |
39 | private static async Task SelectInstallDirAsync(TopLevel window)
40 | {
41 | IReadOnlyList folders = await window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions()).ConfigureAwait(false);
42 | return folders.Count == 1 ? folders[0].TryGetLocalPath() : null;
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Models/Implementations/Settings/AppSettings.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Globalization;
3 |
4 |
5 | namespace BeatSaberModManager.Models.Implementations.Settings
6 | {
7 | ///
8 | /// Application-wide settings.
9 | ///
10 | public sealed class AppSettings
11 | {
12 | ///
13 | /// The index of the tab that was last open.
14 | ///
15 | public int TabIndex { get; set; }
16 |
17 | ///
18 | /// The name of the theme used.
19 | ///
20 | public string? ThemeName { get; set; }
21 |
22 | ///
23 | /// The of the language used.
24 | ///
25 | public string? LanguageCode { get; set; }
26 |
27 | ///
28 | /// True if already installed mods should be reinstalled, false otherwise.
29 | ///
30 | public bool ForceReinstallMods { get; set; }
31 |
32 | ///
33 | /// True if the OneClick installation window should automatically close, false otherwise.
34 | ///
35 | public bool CloseOneClickWindow { get; set; } = true;
36 |
37 | ///
38 | /// True if selected mods should be saved and restored on restart, false otherwise.
39 | ///
40 | public bool SaveSelectedMods { get; set; } = true;
41 |
42 | ///
43 | /// The game's installation directory.
44 | ///
45 | public string? InstallDir { get; set; }
46 |
47 | ///
48 | /// A collection of all selected mods.
49 | ///
50 | public HashSet SelectedMods => _selectedMods ??= new HashSet();
51 | private HashSet? _selectedMods;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/BeatSaberModManager/Views/Controls/IconHeader.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Controls.Primitives;
4 | using Avalonia.Media;
5 |
6 |
7 | namespace BeatSaberModManager.Views.Controls
8 | {
9 | ///
10 | /// A Header with a dockable icon.
11 | ///
12 | public class IconHeader : TemplatedControl
13 | {
14 | ///
15 | public static readonly StyledProperty