diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 53b53ad..9267718 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,27 +8,49 @@ on: - main jobs: build: + strategy: + matrix: + configuration: [Release] + platform: [x64] runs-on: windows-latest permissions: packages: read steps: - - uses: actions/checkout@v3 - - uses: actions/setup-dotnet@v2 - with: - dotnet-version: '6.0.x' - - if: ${{ github.ref == 'refs/heads/main' }} - run: | - dotnet nuget add source --username USERNAME --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/OpenSimTools/index.json" - dotnet publish -c Release - - if: ${{ github.ref == 'refs/heads/main' }} - uses: actions/upload-artifact@v3 - with: - name: AMS2CM - path: src/CLI/bin/Release/net6.0-windows/publish/** - if-no-files-found: error - - if: ${{ github.ref == 'refs/heads/main' }} - uses: actions/upload-artifact@v3 - with: - name: AMS2CM - path: LICENSE - if-no-files-found: error + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Install .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v1.0.2 + - name: Configure NuGet repository + run: dotnet nuget add source --username USERNAME --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/OpenSimTools/index.json" + + - name: Build + run: | + msbuild /t:Restore /p:Configuration=${{ matrix.configuration }} + msbuild /t:Publish /p:Configuration=${{ matrix.configuration }} /p:Platform=${{ matrix.platform }} + + - name: Upload license + uses: actions/upload-artifact@v3 + with: + name: AMS2CM + path: LICENSE + if-no-files-found: error + - name: Upload CLI + uses: actions/upload-artifact@v3 + with: + name: AMS2CM + path: src/CLI/bin/${{ matrix.configuration }}/net6.0-windows/publish/** + if-no-files-found: error + # Separate package until stable, to be able to release CLI independently + - name: Upload GUI + uses: actions/upload-artifact@v3 + with: + name: AMS2CM GUI + path: src/GUI/bin/${{ matrix.platform }}/${{ matrix.configuration }}/net6.0-windows10.0.19041.0/** + if-no-files-found: error diff --git a/AMS2CM.sln b/AMS2CM.sln index d72fddf..8397dc8 100644 --- a/AMS2CM.sln +++ b/AMS2CM.sln @@ -1,22 +1,87 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLI", "src\CLI\CLI.csproj", "{77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}" +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33530.505 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CLI", "src\CLI\CLI.csproj", "{77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "src\Core\Core.csproj", "{A20048F0-D212-499D-8CCF-5E0B989E21F7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "src\Core\Core.csproj", "{A20048F0-D212-499D-8CCF-5E0B989E21F7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GUI", "src\GUI\GUI.csproj", "{2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Debug|ARM64.Build.0 = Debug|Any CPU + {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Debug|x64.ActiveCfg = Debug|Any CPU + {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Debug|x64.Build.0 = Debug|Any CPU + {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Debug|x86.ActiveCfg = Debug|Any CPU + {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Debug|x86.Build.0 = Debug|Any CPU {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Release|Any CPU.ActiveCfg = Release|Any CPU {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Release|Any CPU.Build.0 = Release|Any CPU + {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Release|ARM64.ActiveCfg = Release|Any CPU + {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Release|ARM64.Build.0 = Release|Any CPU + {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Release|x64.ActiveCfg = Release|Any CPU + {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Release|x64.Build.0 = Release|Any CPU + {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Release|x86.ActiveCfg = Release|Any CPU + {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Release|x86.Build.0 = Release|Any CPU {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Debug|ARM64.Build.0 = Debug|Any CPU + {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Debug|x64.ActiveCfg = Debug|Any CPU + {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Debug|x64.Build.0 = Debug|Any CPU + {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Debug|x86.ActiveCfg = Debug|Any CPU + {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Debug|x86.Build.0 = Debug|Any CPU {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Release|Any CPU.ActiveCfg = Release|Any CPU {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Release|Any CPU.Build.0 = Release|Any CPU + {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Release|ARM64.ActiveCfg = Release|Any CPU + {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Release|ARM64.Build.0 = Release|Any CPU + {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Release|x64.ActiveCfg = Release|Any CPU + {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Release|x64.Build.0 = Release|Any CPU + {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Release|x86.ActiveCfg = Release|Any CPU + {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Release|x86.Build.0 = Release|Any CPU + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|Any CPU.ActiveCfg = Debug|x64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|Any CPU.Build.0 = Debug|x64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|Any CPU.Deploy.0 = Debug|x64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|ARM64.Build.0 = Debug|ARM64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|x64.ActiveCfg = Debug|x64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|x64.Build.0 = Debug|x64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|x64.Deploy.0 = Debug|x64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|x86.ActiveCfg = Debug|x86 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|x86.Build.0 = Debug|x86 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|x86.Deploy.0 = Debug|x86 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|Any CPU.ActiveCfg = Release|x64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|Any CPU.Build.0 = Release|x64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|Any CPU.Deploy.0 = Release|x64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|ARM64.ActiveCfg = Release|ARM64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|ARM64.Build.0 = Release|ARM64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|ARM64.Deploy.0 = Release|ARM64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|x64.ActiveCfg = Release|x64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|x64.Build.0 = Release|x64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|x64.Deploy.0 = Release|x64 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|x86.ActiveCfg = Release|x86 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|x86.Build.0 = Release|x86 + {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|x86.Deploy.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6C69475D-BCFD-4B42-A90D-845A72BFB1F8} EndGlobalSection EndGlobal diff --git a/LICENSE b/LICENSE index 653deb4..7eb47e5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2023 OpenSimTools Contributors +Copyright (c) 2023 Open Sim Tools Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/CLI/CLI.csproj b/src/CLI/CLI.csproj index e70b46c..6c0c32a 100644 --- a/src/CLI/CLI.csproj +++ b/src/CLI/CLI.csproj @@ -1,31 +1,23 @@  + + Exe + net6.0-windows + enable + enable + OpenSimTools + OpenSimTools Contributors + AMS2CM + AMS2CM.CLI + ..\Shared\AMS2CM.ico + - - Exe - net6.0-windows - enable - enable - OpenSimTools - OpenSimTools Contributors - AMS2CM - AMS2CM.CLI - AMS2CM.ico - - - - - - - - - + - + - + Always - diff --git a/src/CLI/Config.cs b/src/Core/Config.cs similarity index 100% rename from src/CLI/Config.cs rename to src/Core/Config.cs diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index fb34204..754be5b 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -1,5 +1,4 @@  - net6.0-windows enable @@ -17,4 +16,9 @@ + + + + + diff --git a/src/Core/ModManager.cs b/src/Core/ModManager.cs index 4e6eadf..e0f15d4 100644 --- a/src/Core/ModManager.cs +++ b/src/Core/ModManager.cs @@ -1,4 +1,4 @@ -using Core.Games; +using Core.Games; using Core.Mods; using Newtonsoft.Json; using SevenZipExtractor; @@ -8,7 +8,8 @@ namespace Core; public class ModManager { private record WorkPaths( - string ModArchivesDir, + string EnabledModArchivesDir, + string DisabledModArchivesDir, string TempDir, string CurrentStateFile ); @@ -20,6 +21,7 @@ string CurrentStateFile private const string ModsDirName = "Mods"; private const string EnabledModsDirName = "Enabled"; + private const string DisabledModsSubdir = "Disabled"; private const string TempDirName = "Temp"; private const string CurrentStateFileName = "installed.json"; @@ -33,11 +35,13 @@ string CurrentStateFile public ModManager(IGame game, IModFactory modFactory) { + this.game = game; this.modFactory = modFactory; var modsDir = Path.Combine(game.InstallationDirectory, ModsDirName); workPaths = new WorkPaths( - ModArchivesDir: Path.Combine(modsDir, EnabledModsDirName), + EnabledModArchivesDir: Path.Combine(modsDir, EnabledModsDirName), + DisabledModArchivesDir: Path.Combine(modsDir, DisabledModsSubdir), TempDir: Path.Combine(modsDir, TempDirName), CurrentStateFile: Path.Combine(modsDir, CurrentStateFileName) ); @@ -54,6 +58,75 @@ private static void AddToEnvionmentPath(string additionalPath) Environment.SetEnvironmentVariable(pathEnvVar, $"{env};{additionalPath}"); } + public List FetchState() + { + var installedPackageNames = ReadPreviouslyInstalledFiles().Keys.Where(_ => !IsBootFiles(_)).ToHashSet(); + var enabledTuples = ListEnabledModPackages().Select(_ => (_, true)); + var disabledTuples = ListDisabledModPackages().Select(_ => (_, false)); + var availableTuples = enabledTuples.Concat(disabledTuples); + var availableModsState = availableTuples.Select(_ => + { + var packagePath = _._; + var isEnabled = _.Item2; + var packageName = PackageName(packagePath); + return new ModState( + PackageName: packageName, + PackagePath: packagePath, + IsEnabled: isEnabled, + IsInstalled: installedPackageNames.Contains(packageName) + ); + }); + var availablePackageNames = availableModsState.Select(_ => _.PackageName); + var unavailablePackageNames = installedPackageNames.Except(availablePackageNames); + var unavailableModsState = unavailablePackageNames.Select(packageName => new ModState( + PackageName: packageName, + PackagePath: null, + IsEnabled: null, + IsInstalled: true + )); + return unavailableModsState.Concat(availableModsState).ToList(); + } + + public ModState EnableNewMod(string packagePath) + { + var destinationDirectoryPath = workPaths.EnabledModArchivesDir; + ExistingDirectoryOrCreate(destinationDirectoryPath); + var destinationFilePath = Path.Combine(destinationDirectoryPath, Path.GetFileName(packagePath)); + File.Copy(packagePath, destinationFilePath); + return new ModState( + PackageName: PackageName(destinationFilePath), + PackagePath: destinationFilePath, + IsEnabled: true, + IsInstalled: false + ); + } + + public string EnableMod(string packagePath) + { + return MoveMod(packagePath, workPaths.EnabledModArchivesDir); + } + + public string DisableMod(string packagePath) + { + return MoveMod(packagePath, workPaths.DisabledModArchivesDir); + } + + private string MoveMod(string packagePath, string destinationDirectoryPath) + { + ExistingDirectoryOrCreate(destinationDirectoryPath); + var destinationFilePath = Path.Combine(destinationDirectoryPath, Path.GetFileName(packagePath)); + File.Move(packagePath, destinationFilePath); + return destinationFilePath; + } + + private static void ExistingDirectoryOrCreate(string directoryPath) + { + if (!Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + } + public void InstallEnabledMods() { // It shoulnd't be needed, but some systems seem to want to load oo2core @@ -112,27 +185,18 @@ private void CheckNoBootfilesInstalled() private void InstallAllModFiles() { - if (!Directory.Exists(workPaths.ModArchivesDir)) - { - Console.WriteLine($"No mod archives found in {workPaths.ModArchivesDir}"); - return; - } + var modPackages = ListEnabledModPackages(); var modConfigs = new List(); - var modArchives = Directory.EnumerateFiles(workPaths.ModArchivesDir).ToList(); var installedFilesByMod = new Dictionary>(); try { - if (!modArchives.Any()) - { - Console.WriteLine($"No mod archives found in {workPaths.ModArchivesDir}"); - } - else + if (modPackages.Any()) { Console.WriteLine("Installing mods:"); - foreach (var archivePath in modArchives) + foreach (var packagePath in modPackages) { - var packageName = Path.GetFileNameWithoutExtension(archivePath); - if (packageName.StartsWith(BootfilesPrefix)) + var packageName = Path.GetFileNameWithoutExtension(packagePath); + if (IsBootFiles(packageName)) { Console.WriteLine($"- {packageName} (skipped)"); continue; @@ -140,7 +204,7 @@ private void InstallAllModFiles() Console.WriteLine($"- {packageName}"); - var mod = ExtractMod(packageName, archivePath); + var mod = ExtractMod(packageName, packagePath); try { mod.Install(game.InstallationDirectory); @@ -173,6 +237,10 @@ private void InstallAllModFiles() Console.WriteLine("Post-processing not required"); } } + else + { + Console.WriteLine($"No mod archives found in {workPaths.EnabledModArchivesDir}"); + } } finally { @@ -180,6 +248,8 @@ private void InstallAllModFiles() } } + private bool IsBootFiles(string packageName) => packageName.StartsWith(BootfilesPrefix); + private IMod ExtractMod(string packageName, string archivePath) { var extractionDir = Path.Combine(workPaths.TempDir, packageName); @@ -191,7 +261,7 @@ private IMod ExtractMod(string packageName, string archivePath) private IMod BootfilesMod() { - var bootfilesArchives = Directory.EnumerateFiles(workPaths.ModArchivesDir, $"{BootfilesPrefix}*.*"); + var bootfilesArchives = Directory.EnumerateFiles(workPaths.EnabledModArchivesDir, $"{BootfilesPrefix}*.*"); switch (bootfilesArchives.Count()) { case 0: @@ -212,6 +282,24 @@ private IMod BootfilesMod() } } + private IReadOnlyCollection ListEnabledModPackages() => ListModPackages(workPaths.EnabledModArchivesDir); + + private IReadOnlyCollection ListDisabledModPackages() => ListModPackages(workPaths.DisabledModArchivesDir); + + private IReadOnlyCollection ListModPackages(string path) + { + if (Directory.Exists(path)) + { + return Directory.EnumerateFiles(path).ToList(); + } + else + { + return Array.Empty(); + } + } + + private string PackageName(string archivePath) => Path.GetFileNameWithoutExtension(archivePath); + private Dictionary> ReadPreviouslyInstalledFiles() { if (!File.Exists(workPaths.CurrentStateFile)) { @@ -224,6 +312,9 @@ private Dictionary> ReadPreviouslyInstalledF private void WriteInstalledFiles(Dictionary> filesByMod) { + if (!filesByMod.Any() && !File.Exists(workPaths.CurrentStateFile)) { + return; + } File.WriteAllText(workPaths.CurrentStateFile, JsonConvert.SerializeObject(filesByMod, JsonSerializerSettings)); } } \ No newline at end of file diff --git a/src/Core/ModState.cs b/src/Core/ModState.cs new file mode 100644 index 0000000..ead7eb6 --- /dev/null +++ b/src/Core/ModState.cs @@ -0,0 +1,8 @@ +namespace Core; + +public record ModState( + string PackageName, + string? PackagePath, + bool IsInstalled, + bool? IsEnabled +); diff --git a/src/GUI/App.xaml b/src/GUI/App.xaml new file mode 100644 index 0000000..5f4c61b --- /dev/null +++ b/src/GUI/App.xaml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/GUI/App.xaml.cs b/src/GUI/App.xaml.cs new file mode 100644 index 0000000..9657da3 --- /dev/null +++ b/src/GUI/App.xaml.cs @@ -0,0 +1,19 @@ +using Microsoft.UI.Xaml; + +namespace AMS2CM.GUI; + +public partial class App : Application +{ + public App() + { + InitializeComponent(); + } + + protected override void OnLaunched(LaunchActivatedEventArgs args) + { + window = new MainWindow(); + window.Activate(); + } + + private Window window; +} diff --git a/src/GUI/GUI.csproj b/src/GUI/GUI.csproj new file mode 100644 index 0000000..136d05a --- /dev/null +++ b/src/GUI/GUI.csproj @@ -0,0 +1,34 @@ + + + WinExe + net6.0-windows10.0.19041.0 + 10.0.17763.0 + AMS2CM.GUI + AMS2CM.GUI + app.manifest + x86;x64 + win10-x86;win10-x64 + true + true + ..\Shared\AMS2CM.ico + + + + + + + + + + + + + + + Always + + + Always + + + diff --git a/src/GUI/MainWindow.xaml b/src/GUI/MainWindow.xaml new file mode 100644 index 0000000..d7b0994 --- /dev/null +++ b/src/GUI/MainWindow.xaml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GUI/MainWindow.xaml.cs b/src/GUI/MainWindow.xaml.cs new file mode 100644 index 0000000..d32720e --- /dev/null +++ b/src/GUI/MainWindow.xaml.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Microsoft.VisualBasic.FileIO; +using System.Linq; +using Core; +using Core.Games; +using Microsoft.UI.Xaml; +using Windows.ApplicationModel.DataTransfer; +using Windows.Storage; +using WinUIEx; + +namespace AMS2CM.GUI; + +public sealed partial class MainWindow : WindowEx +{ + private readonly ObservableCollection modList; + private readonly ModManager modManager; + + public MainWindow() + { + InitializeComponent(); + modManager = CreateModManager(); + modList = new ObservableCollection(); + ModListView.ItemsSource = modList; + SyncModListView(); + } + + private static ModManager CreateModManager() + { + var args = Environment.GetCommandLineArgs(); + var config = Config.Load(args); + var game = new Game(config.Game); + var modFactory = new ModFactory(config.ModInstall, game); + return new ModManager(game, modFactory); + } + + private void SyncButton_Click(object sender, RoutedEventArgs e) + { + SyncButton.IsEnabled = false; + modManager.InstallEnabledMods(); + SyncModListView(); + SyncButton.IsEnabled = true; + } + + private void SyncModListView() + { + modList.Clear(); + foreach (var modState in modManager.FetchState().OrderBy(_ => _.PackageName)) + { + modList.Add(new ModVM(modState, modManager)); + } + } + + private void ModListView_DragOver(object sender, DragEventArgs e) + { + e.AcceptedOperation = DataPackageOperation.Copy; + } + + private async void ModListView_Drop(object sender, DragEventArgs e) + { + if (e.DataView.Contains(StandardDataFormats.StorageItems)) + { + var items = await e.DataView.GetStorageItemsAsync(); + if (items.Count > 0) + { + foreach (var storageFile in items.OfType()) + { + var filePath = storageFile.Path; + modManager.EnableNewMod(filePath); + } + // Refresh list after adding mods with drag and drop + SyncModListView(); + } + } + } + + private async void ModListView_DragItemsStarting(object sender, Microsoft.UI.Xaml.Controls.DragItemsStartingEventArgs e) + { + var storageItems = new List(); + foreach (var o in e.Items) + { + var mvm = (ModVM)o; + var si = await StorageFile.GetFileFromPathAsync(mvm.PackagePath); + storageItems.Add(si); + } + e.Data.SetStorageItems(storageItems); + + e.Data.RequestedOperation = DataPackageOperation.Move; + } + + private void ModListView_DragItemsCompleted(Microsoft.UI.Xaml.Controls.ListViewBase sender, Microsoft.UI.Xaml.Controls.DragItemsCompletedEventArgs args) + { + // Refresh list after removing mods with drag and drop + SyncModListView(); + } + + private void ModListMenuToInstall_Click(object sender, RoutedEventArgs e) + { + foreach (var o in ModListView.SelectedItems) + { + var mvm = (ModVM)o; + mvm.IsEnabled = true; + } + } + + private void ModListMenuDelete_Click(object sender, RoutedEventArgs e) + { + foreach (var o in ModListView.SelectedItems) + { + var mvm = (ModVM)o; + if (mvm.IsAvailable) + { + FileSystem.DeleteFile(mvm.PackagePath, UIOption.AllDialogs, RecycleOption.SendToRecycleBin); + } + } + // Refresh list after removing mods with context menu + SyncModListView(); + } + + private void ModListView_RightTapped(object sender, Microsoft.UI.Xaml.Input.RightTappedRoutedEventArgs e) + { + // Select the mod if right click outside of selection + var mvm = (e.OriginalSource as FrameworkElement).DataContext as ModVM; + if (!ModListView.SelectedItems.Contains(mvm)) + { + ModListView.SelectedItem = mvm; + } + } +} diff --git a/src/GUI/ModVM.cs b/src/GUI/ModVM.cs new file mode 100644 index 0000000..1ea0256 --- /dev/null +++ b/src/GUI/ModVM.cs @@ -0,0 +1,61 @@ +using System.ComponentModel; +using Core; + +namespace AMS2CM.GUI; + +internal class ModVM : INotifyPropertyChanged +{ + private readonly ModState modState; + private readonly ModManager modManager; + private bool isEnabled; + private string currentPackagePath; + + public event PropertyChangedEventHandler PropertyChanged; + + public ModVM(ModState modState, ModManager modManager) + { + this.modState = modState; + this.modManager = modManager; + isEnabled = modState.IsEnabled ?? false; + currentPackagePath = modState.PackagePath; + } + + public string PackageName => modState.PackageName; + + public string PackagePath => currentPackagePath; + + public bool IsInstalled => modState.IsInstalled; + + public bool IsEnabled + { + get => isEnabled; + set => EnableOrDisable(value); + } + + public bool IsAvailable + { + get => modState.IsEnabled is not null; + set => DoNothing(); + } + + private void EnableOrDisable(bool shouldEnable) + { + if (!IsAvailable || shouldEnable == isEnabled) + return; + + if (shouldEnable) + { + currentPackagePath = modManager.EnableMod(currentPackagePath); + } + else + { + currentPackagePath = modManager.DisableMod(currentPackagePath); + } + isEnabled = shouldEnable; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsEnabled))); + } + + private void DoNothing() + { + } +} diff --git a/src/GUI/app.manifest b/src/GUI/app.manifest new file mode 100644 index 0000000..51325a1 --- /dev/null +++ b/src/GUI/app.manifest @@ -0,0 +1,17 @@ + + + + + + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + \ No newline at end of file diff --git a/src/CLI/AMS2CM.ico b/src/Shared/AMS2CM.ico similarity index 100% rename from src/CLI/AMS2CM.ico rename to src/Shared/AMS2CM.ico diff --git a/src/CLI/Config.yaml b/src/Shared/Config.yaml similarity index 100% rename from src/CLI/Config.yaml rename to src/Shared/Config.yaml