diff --git a/src/GIMI-ModManager.Core/Contracts/Entities/ICharacterModList.cs b/src/GIMI-ModManager.Core/Contracts/Entities/ICharacterModList.cs index 48d3e4a7..5b048f87 100644 --- a/src/GIMI-ModManager.Core/Contracts/Entities/ICharacterModList.cs +++ b/src/GIMI-ModManager.Core/Contracts/Entities/ICharacterModList.cs @@ -28,7 +28,7 @@ public interface ICharacterModList /// Remove a mod from the mod list. Stops tracking the mod. /// /// - internal void UnTrackMod(IMod mod); + internal void UnTrackMod(ISkinMod mod); /// /// Enable a mod. This enables the mod. @@ -40,9 +40,8 @@ public interface ICharacterModList /// public void DisableMod(Guid modId); - public bool IsModEnabled(IMod mod); + public bool IsModEnabled(ISkinMod mod); - public void SetCustomModName(Guid modId, string newName); public bool IsMultipleModsActive(bool perSkin = false); @@ -66,9 +65,7 @@ public interface ICharacterModList public bool FolderAlreadyExists(string folderName); /// - /// Permanently deletes a mod from the mod list. This deletes entire mod from the mod folder. + /// Deletes a mod from the mod list. This deletes entire mod from the mod folder. /// - public void DeleteMod(Guid modId, bool moveToRecycleBin = true); - public void DeleteModBySkinEntryId(Guid skinEntryId, bool moveToRecycleBin = true); } \ No newline at end of file diff --git a/src/GIMI-ModManager.Core/Contracts/Entities/IMod.cs b/src/GIMI-ModManager.Core/Contracts/Entities/IMod.cs index f65a3f4a..cffca55d 100644 --- a/src/GIMI-ModManager.Core/Contracts/Entities/IMod.cs +++ b/src/GIMI-ModManager.Core/Contracts/Entities/IMod.cs @@ -1,6 +1,4 @@ -using GIMI_ModManager.Core.Entities; - -namespace GIMI_ModManager.Core.Contracts.Entities; +namespace GIMI_ModManager.Core.Contracts.Entities; /// /// The Idea behind this interface was that mods might not be folders but could also be archives or some other format @@ -22,17 +20,6 @@ public interface IMod : IEqualityComparer /// public string OnlyPath { get; } - /// - /// Custom name of the mod. - /// - public string CustomName { get; } - - /// - /// Set the custom name of the mod. - /// - /// - public void SetCustomName(string customName); - /// /// Move the mod to the specified folder. This does not change the folder name. /// If the drive is different, the mod will be copied and then deleted. @@ -42,6 +29,7 @@ public interface IMod : IEqualityComparer /// This needs to be an absolute path to the folder you want to move the mod to. /// public void MoveTo(string absPath); + /// /// Copies the mod to the specified folder. This does not change the folder name. /// @@ -60,12 +48,12 @@ public interface IMod : IEqualityComparer public bool Exists(); public bool IsEmpty(); + /// /// This uses the contents of the mods to compare them /// /// public bool DeepEquals(IMod? x, IMod? y); - - public byte[] GetContentsHash(); + public byte[] GetContentsHash(); } \ No newline at end of file diff --git a/src/GIMI-ModManager.Core/Contracts/Entities/ISkinMod.cs b/src/GIMI-ModManager.Core/Contracts/Entities/ISkinMod.cs index 138eca57..d458fa78 100644 --- a/src/GIMI-ModManager.Core/Contracts/Entities/ISkinMod.cs +++ b/src/GIMI-ModManager.Core/Contracts/Entities/ISkinMod.cs @@ -1,25 +1,14 @@ -using GIMI_ModManager.Core.Entities; +using GIMI_ModManager.Core.Entities.Mods.SkinMod; namespace GIMI_ModManager.Core.Contracts.Entities; public interface ISkinMod : IMod, IEqualityComparer { - public IReadOnlyCollection ImagePaths { get; } // Support multiple images at some point ??? - public SkinModSettings? CachedSkinModSettings { get; } - public IReadOnlyCollection? CachedKeySwaps { get; } + Guid Id { get; } public bool HasMergedInI { get; } - public void ClearCache(); + public SkinModSettingsManager Settings { get; } + public SkinModKeySwapManager? KeySwaps { get; } - public Task> ReadKeySwapConfiguration(bool forceReload = false, - CancellationToken cancellationToken = default); - - public Task SaveKeySwapConfiguration(ICollection updatedKeySwaps, - CancellationToken cancellationToken = default); - - - public Task ReadSkinModSettings(bool forceReload = false, CancellationToken cancellationToken = default); - - public Task SaveSkinModSettings(SkinModSettings skinModSettings, - CancellationToken cancellationToken = default); + public bool ContainsOnlyJasmFiles(); } \ No newline at end of file diff --git a/src/GIMI-ModManager.Core/Contracts/Services/ISkinManagerService.cs b/src/GIMI-ModManager.Core/Contracts/Services/ISkinManagerService.cs index 827c6585..c7e76ed0 100644 --- a/src/GIMI-ModManager.Core/Contracts/Services/ISkinManagerService.cs +++ b/src/GIMI-ModManager.Core/Contracts/Services/ISkinManagerService.cs @@ -1,6 +1,8 @@ using GIMI_ModManager.Core.Contracts.Entities; using GIMI_ModManager.Core.Entities.Genshin; using GIMI_ModManager.Core.Services; +using OneOf; +using OneOf.Types; namespace GIMI_ModManager.Core.Contracts.Services; @@ -19,15 +21,18 @@ public Task Initialize(string activeModsFolderPath, string? unloadedModsFolderPa /// /// /// If null, reorganize all mods outside of characters mod folders + /// If true will also disable the mods /// Mods moved - public int ReorganizeMods(GenshinCharacter? characterFolderToReorganize = null); + public Task ReorganizeModsAsync(GenshinCharacter? characterFolderToReorganize = null, + bool disableMods = false); /// /// This looks for mods in characters mod folder that are not tracked by the mod manager and adds them to the mod manager. /// public Task RefreshModsAsync(GenshinCharacter? refreshForCharacter = null); - public Task TransferMods(ICharacterModList source, ICharacterModList destination, IEnumerable modsEntryIds); + public Task[]>> TransferMods(ICharacterModList source, ICharacterModList destination, + IEnumerable modsEntryIds); public Task GetCurrentSwapVariationAsync(Guid characterSkinEntryId); @@ -46,6 +51,8 @@ public void ExportMods(ICollection characterModLists, string SetModStatus setModStatus = SetModStatus.KeepCurrent); public event EventHandler? ModExportProgress; + + public ISkinMod? GetModById(Guid id); } public enum SetModStatus @@ -58,16 +65,19 @@ public enum SetModStatus public readonly struct RefreshResult { public RefreshResult(IReadOnlyCollection modsUntracked, IReadOnlyCollection modsTracked, - IReadOnlyCollection modsDuplicate) + IReadOnlyCollection modsDuplicate, IReadOnlyCollection errors) { ModsUntracked = modsUntracked; ModsTracked = modsTracked; ModsDuplicate = modsDuplicate; + Errors = errors; } public IReadOnlyCollection ModsUntracked { get; } public IReadOnlyCollection ModsTracked { get; } + public IReadOnlyCollection Errors { get; } + public IReadOnlyCollection ModsDuplicate { get; } public readonly struct DuplicateMods diff --git a/src/GIMI-ModManager.Core/Entities/CharacterModList.cs b/src/GIMI-ModManager.Core/Entities/CharacterModList.cs index fe5707ed..45c67731 100644 --- a/src/GIMI-ModManager.Core/Entities/CharacterModList.cs +++ b/src/GIMI-ModManager.Core/Entities/CharacterModList.cs @@ -1,6 +1,8 @@ #nullable enable +using System.Diagnostics; using GIMI_ModManager.Core.Contracts.Entities; using GIMI_ModManager.Core.Entities.Genshin; +using GIMI_ModManager.Core.Entities.Mods.SkinMod; using GIMI_ModManager.Core.Helpers; using Serilog; @@ -18,6 +20,8 @@ public sealed class CharacterModList : ICharacterModList, IDisposable private readonly FileSystemWatcher _watcher; public GenshinCharacter Character { get; } + private readonly object _modsLock = new(); + internal CharacterModList(GenshinCharacter character, string absPath, ILogger? logger = null) { _logger = logger?.ForContext(); @@ -34,17 +38,6 @@ internal CharacterModList(GenshinCharacter character, string absPath, ILogger? l _watcher.EnableRaisingEvents = true; } - public void SetCustomModName(Guid modId, string newName = "") - { - if (_mods.FirstOrDefault(mod => mod.Id == modId) is { } modEntry) - { - } - else - { - _logger?.Warning("Renamed mod {ModId} was not tracked in mod list", modId); - } - } - public event EventHandler? ModsChanged; private void OnWatcherError(object sender, ErrorEventArgs e) @@ -55,74 +48,120 @@ private void OnWatcherError(object sender, ErrorEventArgs e) private void OnModDeleted(object sender, FileSystemEventArgs e) { _logger?.Information("Mod {ModName} in {characterFolder} folder was deleted", e.Name, Character.DisplayName); - if (_mods.Any(mod => mod.Mod.FullPath == e.FullPath)) + Task.Run(() => { - var mod = _mods.First(mod => mod.Mod.FullPath == e.FullPath); - _mods.Remove(mod); - } - else - { - _logger?.Warning("Deleted folder {Folder} was not tracked in mod list", e.FullPath); - } - - ModsChanged?.Invoke(this, new ModFolderChangedArgs(e.FullPath, ModFolderChangeType.Deleted)); + if (_mods.Any(mod => mod.Mod.FullPath == e.FullPath)) + { + var mod = _mods.First(mod => mod.Mod.FullPath == e.FullPath); + + UnTrackMod(mod.Mod); + } + else + { + _logger?.Warning("Deleted folder {Folder} was not tracked in mod list", e.FullPath); + } + + ModsChanged?.Invoke(this, new ModFolderChangedArgs(e.FullPath, ModFolderChangeType.Deleted)); + }); } private void OnModCreated(object sender, FileSystemEventArgs e) { _logger?.Information("Mod {ModName} was created in {characterFolder} created", e.Name, Character.DisplayName); - var mod = new SkinMod(new DirectoryInfo(e.FullPath)); - if (ModAlreadyAdded(mod)) - _logger?.Warning("Created folder {Folder} was already tracked in {characterFolder} mod list", e.Name, - Character.DisplayName); - else - TrackMod(mod); - ModsChanged?.Invoke(this, new ModFolderChangedArgs(e.FullPath, ModFolderChangeType.Created)); + + Task.Run(async () => + { + ISkinMod newSkinMod = null!; + try + { + newSkinMod = await SkinMod.CreateModAsync(new DirectoryInfo(e.FullPath)); + } + catch (Exception exception) + { + _logger?.Error(exception, "Error initializing mod"); + return; + } + + + if (ModAlreadyAdded(newSkinMod)) + _logger?.Warning("Created folder {Folder} was already tracked in {characterFolder} mod list", e.Name, + Character.DisplayName); + else + TrackMod(newSkinMod); + ModsChanged?.Invoke(this, new ModFolderChangedArgs(e.FullPath, ModFolderChangeType.Created)); + }); } private void OnModRenamed(object sender, RenamedEventArgs e) { _logger?.Information("Mod {ModName} renamed to {NewName}", e.OldFullPath, e.FullPath); - if (_mods.FirstOrDefault(mod => mod.Mod.FullPath == e.OldFullPath) is var oldModEntry && - oldModEntry is not null) - { - var newMod = new SkinMod(new DirectoryInfo(e.FullPath), oldModEntry.Mod.CustomName); - var modEntry = new CharacterSkinEntry(newMod, this, !newMod.Name.StartsWith(DISABLED_PREFIX)); - _mods.Remove(oldModEntry); - _mods.Add(modEntry); - } - else - { - _logger?.Warning("Renamed folder {Folder} was not tracked in mod list", e.OldFullPath); - } - ModsChanged?.Invoke(this, new ModFolderChangedArgs(e.FullPath, ModFolderChangeType.Renamed, e.OldFullPath)); + Task.Run(async () => + { + if (_mods.FirstOrDefault(mod => mod.Mod.FullPath == e.OldFullPath) is { } oldModEntry) + { + ISkinMod newSkinMod = null!; + try + { + newSkinMod = await SkinMod.CreateModAsync(new DirectoryInfo(e.FullPath)); + await newSkinMod.Settings.ReadSettingsAsync(); + } + catch (Exception exception) + { + _logger?.Error(exception, "Error initializing mod"); + return; + } + + var modEntry = new CharacterSkinEntry(newSkinMod, this, IsModFolderEnabled(newSkinMod.Name)); + UnTrackMod(oldModEntry.Mod); + TrackMod(modEntry.Mod); + } + else + { + _logger?.Warning("Renamed folder {Folder} was not tracked in mod list", e.OldFullPath); + } + + ModsChanged?.Invoke(this, new ModFolderChangedArgs(e.FullPath, ModFolderChangeType.Renamed, e.OldFullPath)); + }); } public void TrackMod(ISkinMod mod) { - if (ModAlreadyAdded(mod)) - throw new InvalidOperationException("Mod already added"); - - _mods.Add(mod.Name.StartsWith(DISABLED_PREFIX) - ? new CharacterSkinEntry(mod, this, false) - : new CharacterSkinEntry(mod, this, true)); - _logger?.Debug("Tracking {ModName} in {CharacterName} modList", mod.Name, Character.DisplayName); + lock (_modsLock) + { + if (ModAlreadyAdded(mod)) + { + _logger?.Warning("Mod {ModName} was already tracked in {CharacterName} modList", mod.Name, + Character.DisplayName); + return; + } + + _mods.Add(ModFolderHelpers.FolderHasDisabledPrefix(mod.Name) + ? new CharacterSkinEntry(mod, this, false) + : new CharacterSkinEntry(mod, this, true)); + _logger?.Debug("Tracking {ModName} in {CharacterName} modList", mod.Name, Character.DisplayName); + Debug.Assert(_mods.DistinctBy(m => m.Id).Count() == _mods.Count); + } } // Untrack - public void UnTrackMod(IMod mod) + public void UnTrackMod(ISkinMod mod) { - if (!ModAlreadyAdded(mod)) + lock (_modsLock) { - _logger?.Warning("Mod {ModName} was not tracked in {CharacterName} modList", mod.Name, - Character.DisplayName); - return; - } + if (!ModAlreadyAdded(mod)) + { + _logger?.Warning("Mod {ModName} was not tracked in {CharacterName} modList", mod.Name, + Character.DisplayName); + return; + } + + _mods.Remove(_mods.First(m => m.Mod.Equals(mod))); - _mods.Remove(_mods.First(m => m.Mod == mod)); - _logger?.Debug("Stopped tracking {ModName} in {CharacterName} modList", mod.Name, Character.DisplayName); + _logger?.Debug("Stopped tracking {ModName} in {CharacterName} modList", mod.Name, Character.DisplayName); + Debug.Assert(_mods.DistinctBy(m => m.Id).Count() == _mods.Count); + } } public void EnableMod(Guid modId) @@ -136,7 +175,7 @@ public void EnableMod(Guid modId) if (!ModAlreadyAdded(mod)) throw new InvalidOperationException("Mod not added"); - if (!mod.Name.StartsWith(DISABLED_PREFIX)) + if (!ModFolderHelpers.FolderHasDisabledPrefix(mod.Name)) throw new InvalidOperationException("Cannot enable a enabled mod"); var newName = ModFolderHelpers.GetFolderNameWithoutDisabledPrefix(mod.Name); @@ -165,7 +204,7 @@ public void DisableMod(Guid modId) if (!ModAlreadyAdded(mod)) throw new InvalidOperationException("Mod not added"); - if (mod.Name.StartsWith(DISABLED_PREFIX)) + if (ModFolderHelpers.FolderHasDisabledPrefix(mod.Name)) throw new InvalidOperationException("Cannot disable a disabled mod"); var newName = ModFolderHelpers.GetFolderNameWithDisabledPrefix(mod.Name); @@ -174,7 +213,7 @@ public void DisableMod(Guid modId) throw new InvalidOperationException("Cannot disable a mod with the same name as a disabled mod"); mod.Rename(newName); - _mods.First(m => m.Mod == mod).IsEnabled = false; + _mods.First(m => m.Mod.Equals(mod)).IsEnabled = false; } finally { @@ -182,17 +221,17 @@ public void DisableMod(Guid modId) } } - public bool IsModEnabled(IMod mod) + public bool IsModEnabled(ISkinMod mod) { if (!ModAlreadyAdded(mod)) throw new InvalidOperationException("Mod not added"); - return _mods.First(m => m.Mod == mod).IsEnabled; + return _mods.First(m => m.Mod.Equals(mod)).IsEnabled; } - private bool ModAlreadyAdded(IMod mod) + private bool ModAlreadyAdded(ISkinMod mod) { - return _mods.Any(m => m.Mod == mod); + return _mods.Any(m => m.Mod.Equals(mod)); } public void Dispose() @@ -223,23 +262,22 @@ public bool FolderAlreadyExists(string folderName) return Directory.Exists(enabledFolderNamePath) || Directory.Exists(disabledFolderNamePath); } - public void DeleteMod(Guid modId, bool moveToRecycleBin = true) - { - throw new NotImplementedException(); - } - public void DeleteModBySkinEntryId(Guid skinEntryId, bool moveToRecycleBin = true) { - var skinEntry = _mods.FirstOrDefault(modEntry => modEntry.Id == skinEntryId); - if (skinEntry is null) - throw new InvalidOperationException("Skin entry not found"); - using var disableWatcher = DisableWatcher(); - var mod = skinEntry.Mod; - _mods.Remove(skinEntry); - mod.Delete(moveToRecycleBin); - _logger?.Information("{Operation} mod {ModName} from {CharacterName} modList", - moveToRecycleBin ? "Recycled" : "Deleted", mod.Name, Character.DisplayName); + lock (_modsLock) + { + var skinEntry = _mods.FirstOrDefault(modEntry => modEntry.Id == skinEntryId); + if (skinEntry is null) + throw new InvalidOperationException("Skin entry not found"); + using var disableWatcher = DisableWatcher(); + var mod = skinEntry.Mod; + + UnTrackMod(skinEntry.Mod); + mod.Delete(moveToRecycleBin); + _logger?.Information("{Operation} mod {ModName} from {CharacterName} modList", + moveToRecycleBin ? "Recycled" : "Deleted", mod.Name, Character.DisplayName); + } } public bool IsMultipleModsActive(bool perSkin = false) diff --git a/src/GIMI-ModManager.Core/Entities/CharacterSkinEntry.cs b/src/GIMI-ModManager.Core/Entities/CharacterSkinEntry.cs index fd8465a4..64b817f8 100644 --- a/src/GIMI-ModManager.Core/Entities/CharacterSkinEntry.cs +++ b/src/GIMI-ModManager.Core/Entities/CharacterSkinEntry.cs @@ -4,16 +4,17 @@ namespace GIMI_ModManager.Core.Entities; public class CharacterSkinEntry : IEqualityComparer { - internal CharacterSkinEntry(ISkinMod mod, CharacterModList modList, bool isEnabled) + internal CharacterSkinEntry(ISkinMod mod, ICharacterModList modList, bool isEnabled) { + Id = mod.Id; Mod = mod; ModList = modList; IsEnabled = isEnabled; } - public Guid Id { get; } = Guid.NewGuid(); + public Guid Id { get; } public ISkinMod Mod { get; } - public CharacterModList ModList { get; } + public ICharacterModList ModList { get; } public bool IsEnabled { get; internal set; } public bool Equals(CharacterSkinEntry? x, CharacterSkinEntry? y) diff --git a/src/GIMI-ModManager.Core/Entities/Mod.cs b/src/GIMI-ModManager.Core/Entities/Mod.cs index bfeba2e1..c25627de 100644 --- a/src/GIMI-ModManager.Core/Entities/Mod.cs +++ b/src/GIMI-ModManager.Core/Entities/Mod.cs @@ -14,12 +14,10 @@ public class Mod : IMod public string FullPath => _modDirectory.FullName; public string Name => _modDirectory.Name; public string OnlyPath => _modDirectory.Parent!.FullName; - public string CustomName { get; private set; } - public Mod(DirectoryInfo modDirectory, string customName = "") + public Mod(DirectoryInfo modDirectory) { _modDirectory = modDirectory; - CustomName = customName; } public bool Exists() @@ -32,10 +30,6 @@ public bool IsEmpty() return !_modDirectory.EnumerateFiles().Any(); } - public void SetCustomName(string customName) - { - CustomName = customName; - } public void MoveTo(string absPath) { @@ -63,7 +57,7 @@ public virtual IMod CopyTo(string absPath) var newModDirectory = new DirectoryInfo(Path.Combine(absPath, Name)); RecursiveCopyTo(_modDirectory, newModDirectory); - return new Mod(newModDirectory, CustomName); + return new Mod(newModDirectory); } public void Rename(string newName) @@ -146,6 +140,6 @@ public int GetHashCode(IMod obj) public override string ToString() { - return $"FolderName: {Name} | CustomName: {CustomName} | FullPath: {FullPath}"; + return $"FolderName: {Name} | FullPath: {FullPath}"; } } \ No newline at end of file diff --git a/src/GIMI-ModManager.Core/Entities/Mods/Contract/KeySwapSection.cs b/src/GIMI-ModManager.Core/Entities/Mods/Contract/KeySwapSection.cs new file mode 100644 index 00000000..ccaacd68 --- /dev/null +++ b/src/GIMI-ModManager.Core/Entities/Mods/Contract/KeySwapSection.cs @@ -0,0 +1,29 @@ +using GIMI_ModManager.Core.Entities.Mods.FileModels; + +namespace GIMI_ModManager.Core.Entities.Mods.Contract; + +public record KeySwapSection +{ + public string SectionName { get; init; } = "Unknown"; + + public string? ForwardKey { get; init; } + + public string? BackwardKey { get; init; } + + public int? Variants { get; init; } + + public string Type { get; init; } = "Unknown"; + + + internal static KeySwapSection FromIniKeySwapSection(IniKeySwapSection iniKeySwapSection) + { + return new KeySwapSection + { + SectionName = iniKeySwapSection.SectionKey, + ForwardKey = iniKeySwapSection.ForwardHotkey, + BackwardKey = iniKeySwapSection.BackwardHotkey, + Variants = iniKeySwapSection.SwapVar?.Length, + Type = iniKeySwapSection.Type ?? "Unknown" + }; + } +} \ No newline at end of file diff --git a/src/GIMI-ModManager.Core/Entities/Mods/Contract/ModSettings.cs b/src/GIMI-ModManager.Core/Entities/Mods/Contract/ModSettings.cs new file mode 100644 index 00000000..efe83e51 --- /dev/null +++ b/src/GIMI-ModManager.Core/Entities/Mods/Contract/ModSettings.cs @@ -0,0 +1,97 @@ +using GIMI_ModManager.Core.Contracts.Entities; +using GIMI_ModManager.Core.Entities.Mods.FileModels; +using GIMI_ModManager.Core.Entities.Mods.Helpers; + +namespace GIMI_ModManager.Core.Entities.Mods.Contract; + +public record ModSettings +{ + public ModSettings(Guid id, string? customName = null, string? author = null, string? version = null, + Uri? modUrl = null, Uri? imagePath = null, string? characterSkinOverride = null) + { + Id = id; + CustomName = customName; + Author = author; + Version = version; + ModUrl = modUrl; + ImagePath = imagePath; + CharacterSkinOverride = characterSkinOverride; + } + + public ModSettings DeepCopyWithProperties(string? characterSkinOverride = null) + { + return new ModSettings( + Id, + CustomName, + Author, + Version, + ModUrl, + ImagePath, + characterSkinOverride ?? CharacterSkinOverride + ); + } + + internal ModSettings() + { + } + + public Guid Id { get; internal set; } + + public string? CustomName { get; internal set; } + + public string? Author { get; internal set; } + + public string? Version { get; internal set; } + + public Uri? ModUrl { get; internal set; } + + public Uri? ImagePath { get; internal set; } + + public string? CharacterSkinOverride { get; internal set; } + + + internal static ModSettings FromJsonSkinSettings(ISkinMod skinMod, JsonModSettings settings) + { + return new ModSettings + { + Id = ModsHelpers.StringToGuid(settings.Id), + CustomName = settings.CustomName, + Author = settings.Author, + Version = settings.Version, + ModUrl = ModsHelpers.StringUrlToUri(settings.ModUrl), + ImagePath = ModsHelpers.RelativeModPathToAbsPath(skinMod.FullPath, settings.ImagePath), + CharacterSkinOverride = settings.CharacterSkinOverride + }; + } + + internal JsonModSettings ToJsonSkinSettings(ISkinMod skinMod) + { + return new JsonModSettings + { + Id = Id.ToString(), + CustomName = CustomName, + Author = Author, + Version = Version, + ModUrl = ModUrl?.ToString(), + ImagePath = ModsHelpers.UriPathToModRelativePath(skinMod, ImagePath?.LocalPath), + CharacterSkinOverride = CharacterSkinOverride + }; + } + + + public bool SettingsEquals(ModSettings? other) + { + if (ReferenceEquals(this, other)) return true; + if (ReferenceEquals(other, null)) return false; + + if (Id != other.Id) return false; + if (CustomName != other.CustomName) return false; + if (Author != other.Author) return false; + if (Version != other.Version) return false; + if (ModUrl != other.ModUrl) return false; + if (ImagePath != other.ImagePath) return false; + if (CharacterSkinOverride != other.CharacterSkinOverride) return false; + + return true; + } +} \ No newline at end of file diff --git a/src/GIMI-ModManager.Core/Entities/Mods/FileModels/IniKeySwapSection.cs b/src/GIMI-ModManager.Core/Entities/Mods/FileModels/IniKeySwapSection.cs new file mode 100644 index 00000000..428700bb --- /dev/null +++ b/src/GIMI-ModManager.Core/Entities/Mods/FileModels/IniKeySwapSection.cs @@ -0,0 +1,41 @@ +namespace GIMI_ModManager.Core.Entities.Mods.FileModels; + +public class IniKeySwapSection +{ + public Dictionary IniKeyValues { get; } = new(); + + public const string KeySwapIniSection = "KeySwap"; + public string SectionKey { get; set; } = KeySwapIniSection; + + public const string ForwardIniKey = "key"; + + public string? ForwardHotkey + { + get => IniKeyValues.TryGetValue(ForwardIniKey, out var value) ? value : null; + set => IniKeyValues[ForwardIniKey] = value ?? string.Empty; + } + + public const string BackwardIniKey = "back"; + + public string? BackwardHotkey + { + get => IniKeyValues.TryGetValue(BackwardIniKey, out var value) ? value : null; + set => IniKeyValues[BackwardIniKey] = value ?? string.Empty; + } + + public const string TypeIniKey = "type"; + + public string? Type + { + get => IniKeyValues.TryGetValue(TypeIniKey, out var value) ? value : null; + set => IniKeyValues[TypeIniKey] = value ?? string.Empty; + } + + public const string SwapVarIniKey = "$swapvar"; + public string[]? SwapVar { get; set; } + + public bool AnyValues() + { + return ForwardHotkey is not null || BackwardHotkey is not null; + } +} \ No newline at end of file diff --git a/src/GIMI-ModManager.Core/Entities/Mods/FileModels/JsonModSettings.cs b/src/GIMI-ModManager.Core/Entities/Mods/FileModels/JsonModSettings.cs new file mode 100644 index 00000000..463114d9 --- /dev/null +++ b/src/GIMI-ModManager.Core/Entities/Mods/FileModels/JsonModSettings.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace GIMI_ModManager.Core.Entities.Mods.FileModels; + +internal class JsonModSettings +{ + public string Id { get; set; } = string.Empty; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? CustomName { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Author { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Version { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ModUrl { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + + public string? ImagePath { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? CharacterSkinOverride { get; set; } +} \ No newline at end of file diff --git a/src/GIMI-ModManager.Core/Entities/Mods/Helpers/ModsHelpers.cs b/src/GIMI-ModManager.Core/Entities/Mods/Helpers/ModsHelpers.cs new file mode 100644 index 00000000..4d5cacb8 --- /dev/null +++ b/src/GIMI-ModManager.Core/Entities/Mods/Helpers/ModsHelpers.cs @@ -0,0 +1,91 @@ +using GIMI_ModManager.Core.Contracts.Entities; + +namespace GIMI_ModManager.Core.Entities.Mods.Helpers; + +internal static class ModsHelpers +{ + public static string? UriPathToModRelativePath(ISkinMod mod, string? uriPath) + { + if (string.IsNullOrWhiteSpace(uriPath)) + return null; + + var modPath = mod.FullPath; + + var modUri = Uri.TryCreate(modPath, UriKind.Absolute, out var result) && + result.Scheme == Uri.UriSchemeFile + ? result + : null; + + var uri = Uri.TryCreate(uriPath, UriKind.Absolute, out var uriResult) && + uriResult.Scheme == Uri.UriSchemeFile + ? uriResult + : null; + + // This is technically the only path that should be used. + if (modUri is not null && uri is not null) + { + var relativeUri = modUri.MakeRelativeUri(uri); + + var modName = modUri.Segments.LastOrDefault(); + if (string.IsNullOrWhiteSpace(modName)) + modName = mod.Name; + + var relativePath = relativeUri.ToString().Replace($"{modName}/", ""); + + return relativePath; + } + + + if (Uri.IsWellFormedUriString(uriPath, UriKind.Absolute)) + { + var filename = Path.GetFileName(uriPath); + return string.IsNullOrWhiteSpace(filename) ? null : filename; + } + + var absPath = Path.GetFileName(uriPath); + + var file = Path.GetFileName(absPath); + return string.IsNullOrWhiteSpace(file) ? null : file; + } + + public static Uri? RelativeModPathToAbsPath(string modPath, string? relativeModPath) + { + if (string.IsNullOrWhiteSpace(relativeModPath)) + return null; + + var uri = Uri.TryCreate(Path.Combine(modPath, relativeModPath), UriKind.Absolute, out var result) && + result.Scheme == Uri.UriSchemeFile + ? result + : null; + + return uri; + } + + public static bool IsInModFolder(ISkinMod mod, Uri path) + { + if (path.Scheme != Uri.UriSchemeFile) + return false; + + var fsPath = path.LocalPath; + + + return fsPath.StartsWith(mod.FullPath, StringComparison.OrdinalIgnoreCase); + } + + + public static Uri? StringUrlToUri(string? url) + { + if (string.IsNullOrWhiteSpace(url)) + return null; + + return Uri.IsWellFormedUriString(url, UriKind.Absolute) ? new Uri(url) : null; + } + + public static Guid StringToGuid(string? guid) + { + if (string.IsNullOrWhiteSpace(guid)) + return Guid.NewGuid(); + + return Guid.TryParse(guid, out var result) ? result : Guid.NewGuid(); + } +} \ No newline at end of file diff --git a/src/GIMI-ModManager.Core/Entities/Mods/SkinMod/SkinMod.cs b/src/GIMI-ModManager.Core/Entities/Mods/SkinMod/SkinMod.cs new file mode 100644 index 00000000..648383d8 --- /dev/null +++ b/src/GIMI-ModManager.Core/Entities/Mods/SkinMod/SkinMod.cs @@ -0,0 +1,138 @@ +using GIMI_ModManager.Core.Contracts.Entities; + +namespace GIMI_ModManager.Core.Entities.Mods.SkinMod; + +public class SkinMod : Mod, ISkinMod +{ + private const string ModIniName = "merged.ini"; + private string _modIniPath = string.Empty; + private const string configFileName = ".JASM_ModConfig.json"; + private string _configFilePath = string.Empty; + + public Guid Id { get; private set; } + + public SkinModSettingsManager Settings { get; private set; } = null!; + public SkinModKeySwapManager? KeySwaps { get; private set; } + + + public bool HasMergedInI => KeySwaps is not null; + + + public void ClearCache() + { + Settings.ClearSettings(); + KeySwaps?.ClearKeySwaps(); + } + + + private SkinMod(DirectoryInfo modDirectory) : base(modDirectory) + { + Init(); + } + + + public static Task CreateModAsync(string fullPath, bool forceGenerateNewId = false) + { + if (!Path.IsPathFullyQualified(fullPath)) + throw new ArgumentException("Path must be absolute.", nameof(fullPath)); + + var modDirectory = new DirectoryInfo(fullPath); + + return CreateModAsync(modDirectory, forceGenerateNewId); + } + + public static async Task CreateModAsync(DirectoryInfo modFolder, bool forceGenerateNewId = false) + { + if (!modFolder.Exists) + throw new DirectoryNotFoundException($"Directory not found at path: {modFolder.FullName}"); + + + var skinMod = new SkinMod(modFolder); + skinMod.Settings = new SkinModSettingsManager(skinMod); + + if (HasMergedInIFile(modFolder) is { } merged) + skinMod.KeySwaps = new SkinModKeySwapManager(skinMod, merged); + + + skinMod.Id = await skinMod.Settings.InitializeAsync(); + + if (!forceGenerateNewId) return skinMod; + + + var settings = await skinMod.Settings.ReadSettingsAsync().ConfigureAwait(false); + settings.Id = Guid.NewGuid(); + await skinMod.Settings.SaveSettingsAsync(settings).ConfigureAwait(false); + + + return skinMod; + } + + private void Init() + { + var modFolderAttributes = File.GetAttributes(_modDirectory.FullName); + if (!modFolderAttributes.HasFlag(FileAttributes.Directory)) + throw new ArgumentException("Mod must be a folder.", nameof(_modDirectory.FullName)); + Refresh(); + } + + private void Refresh() + { + _modDirectory.Refresh(); + + _configFilePath = Path.Combine(FullPath, configFileName); + _modIniPath = Path.Combine(FullPath, ModIniName); + } + + public bool ContainsOnlyJasmFiles() + { + return _modDirectory.EnumerateFiles() + .All(file => file.Name.StartsWith(".JASM_", StringComparison.CurrentCultureIgnoreCase)); + } + + private static string? HasMergedInIFile(DirectoryInfo modDirectory) + { + return modDirectory.EnumerateFiles("*.ini", SearchOption.TopDirectoryOnly) + .FirstOrDefault(iniFiles => iniFiles.Name.Equals(ModIniName, StringComparison.CurrentCultureIgnoreCase)) + ?.FullName; + } + + public static bool operator ==(SkinMod? left, SkinMod? right) + { + if (ReferenceEquals(left, right)) return true; + if (ReferenceEquals(left, null)) return false; + if (ReferenceEquals(right, null)) return false; + return left.Id.Equals(right.Id); + } + + public static bool operator !=(SkinMod? left, SkinMod? right) + { + return !(left == right); + } + + public bool Equals(ISkinMod? x, ISkinMod? y) + { + if (ReferenceEquals(x, y)) return true; + if (ReferenceEquals(x, null)) return false; + if (ReferenceEquals(y, null)) return false; + return x.Id.Equals(y.Id); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(this, (SkinMod)obj); + } + + public override int GetHashCode() + { + // ReSharper disable once NonReadonlyMemberInGetHashCode + return Id.GetHashCode(); + } + + public int GetHashCode(ISkinMod obj) + { + return obj.Id.GetHashCode(); + } +} \ No newline at end of file diff --git a/src/GIMI-ModManager.Core/Entities/Mods/SkinMod/SkinModKeySwapManager.cs b/src/GIMI-ModManager.Core/Entities/Mods/SkinMod/SkinModKeySwapManager.cs new file mode 100644 index 00000000..1aad850a --- /dev/null +++ b/src/GIMI-ModManager.Core/Entities/Mods/SkinMod/SkinModKeySwapManager.cs @@ -0,0 +1,235 @@ +using GIMI_ModManager.Core.Contracts.Entities; +using GIMI_ModManager.Core.Entities.Mods.Contract; +using GIMI_ModManager.Core.Entities.Mods.FileModels; +using GIMI_ModManager.Core.Helpers; +using OneOf; + +namespace GIMI_ModManager.Core.Entities.Mods.SkinMod; + +public class SkinModKeySwapManager +{ + private readonly ISkinMod _skinMod; + private readonly string _modIniPath; + private List? _keySwaps; + + public SkinModKeySwapManager(ISkinMod skinMod, string iniPath) + { + _skinMod = skinMod; + _modIniPath = iniPath; + } + + public void ClearKeySwaps() + { + _keySwaps?.Clear(); + } + + public async Task ReadKeySwapConfiguration(CancellationToken cancellationToken = default) + { + List keySwapLines = new(); + List keySwaps = new(); + var keySwapBlockStarted = false; + var currentLine = -1; + var sectionLine = string.Empty; + await foreach (var line in File.ReadLinesAsync(_modIniPath, cancellationToken)) + { + currentLine++; + if (line.Trim().StartsWith(";") || string.IsNullOrWhiteSpace(line)) + continue; + + if (IniConfigHelpers.IsSection(line) && keySwapBlockStarted || + keySwapBlockStarted && keySwapLines.Count > 9) + { + keySwapBlockStarted = false; + + var keySwap = IniConfigHelpers.ParseKeySwap(keySwapLines, sectionLine); + if (keySwap is not null) + keySwaps.Add(keySwap); + keySwapLines.Clear(); + + if (IniConfigHelpers.IsSection(line)) + { + sectionLine = line; + keySwapBlockStarted = true; + } + else + { + sectionLine = string.Empty; + } + + + continue; + } + + if (IniConfigHelpers.IsSection(line)) + { + keySwapBlockStarted = true; + sectionLine = line; + continue; + } + + if (keySwapLines.Count > 10 && !IniConfigHelpers.IsSection(line) && + keySwapBlockStarted) + { + keySwapBlockStarted = false; + sectionLine = string.Empty; + keySwapLines.Clear(); + continue; + } + + if (keySwapBlockStarted) + keySwapLines.Add(line); + } + + _keySwaps = new List(keySwaps.Count); + + foreach (var keySwap in keySwaps) + { + _keySwaps.Add(KeySwapSection.FromIniKeySwapSection(keySwap)); + } + } + + + public async Task SaveKeySwapConfiguration(ICollection updatedKeySwaps, + CancellationToken cancellationToken = default) + { + if (updatedKeySwaps.Count == 0) + throw new ArgumentException("No key swaps to save.", nameof(updatedKeySwaps)); + + if (updatedKeySwaps.Count != _keySwaps?.Count) + throw new ArgumentException("Key swap count mismatch.", nameof(updatedKeySwaps)); + + + var fileLines = new List(); + + await using var fileStream = new FileStream(_modIniPath, FileMode.Open, FileAccess.Read, FileShare.None); + using (var reader = new StreamReader(fileStream)) + { + while (await reader.ReadLineAsync(cancellationToken) is { } line) + fileLines.Add(line); + } + + var sectionStartIndexes = new List(); + for (var i = 0; i < fileLines.Count; i++) + { + var currentLine = fileLines[i]; + if (updatedKeySwaps.Any(keySwap => IniConfigHelpers.IsSection(currentLine, keySwap.SectionName))) + sectionStartIndexes.Add(i); + } + + if (sectionStartIndexes.Count != updatedKeySwaps.Count) + throw new InvalidOperationException("Key swap count mismatch."); + + if (sectionStartIndexes.Count == 0) + throw new InvalidOperationException("No key swaps found."); + + // Line numbers where the key swap sections starts + // We loop from the beginning to the end so we can remove or add lines without messing up the indexes + for (var i = sectionStartIndexes.Count - 1; i >= 0; i--) + { + var keySwap = updatedKeySwaps.ElementAt(i); + var sectionStartIndex = sectionStartIndexes[i] + 1; + + var newForwardKeyWrittenIndex = -1; + var newBackwardKeyWrittenIndex = -1; + + var oldForwardKeyIndex = -1; + var oldBackwardKeyIndex = -1; + + // When iterating a section go downwards instead of upwards + // 8 as the limit is just an arbitrary number so it doesn't loop forever + for (var lineIndex = sectionStartIndex; lineIndex < sectionStartIndex + 8; lineIndex++) + { + var line = fileLines[lineIndex]; + + if (newForwardKeyWrittenIndex == -1 && IniConfigHelpers.IsIniKey(line, IniKeySwapSection.ForwardIniKey)) + { + var value = IniConfigHelpers.FormatIniKey(IniKeySwapSection.ForwardIniKey, keySwap.ForwardKey); + if (value is null) + continue; + fileLines[lineIndex] = value; + + // If forwardkey is defined also set the backward key + if (keySwap.BackwardKey is null) continue; + + var backwardValue = + IniConfigHelpers.FormatIniKey(IniKeySwapSection.BackwardIniKey, keySwap.BackwardKey); + if (backwardValue is null) + continue; + newBackwardKeyWrittenIndex = lineIndex + 1; + fileLines.Insert(newBackwardKeyWrittenIndex, backwardValue); + } + + // Remove old forward key + else if (newForwardKeyWrittenIndex != -1 && newForwardKeyWrittenIndex != lineIndex && + IniConfigHelpers.IsIniKey(line, IniKeySwapSection.ForwardIniKey)) + { + oldForwardKeyIndex = lineIndex; + } + + else if (newBackwardKeyWrittenIndex == -1 && + IniConfigHelpers.IsIniKey(line, IniKeySwapSection.BackwardIniKey)) + { + var value = IniConfigHelpers.FormatIniKey(IniKeySwapSection.BackwardIniKey, keySwap.BackwardKey); + if (value is null) + continue; + fileLines[lineIndex] = value; + + // If backwardkey is defined also set the forward key + if (keySwap.ForwardKey is null) continue; + var forwardValue = + IniConfigHelpers.FormatIniKey(IniKeySwapSection.ForwardIniKey, keySwap.ForwardKey); + if (forwardValue is null) + continue; + newForwardKeyWrittenIndex = lineIndex + 1; + fileLines.Insert(newForwardKeyWrittenIndex, forwardValue); + } + + // Remove old backward key + else if (newBackwardKeyWrittenIndex != -1 && newBackwardKeyWrittenIndex != lineIndex && + IniConfigHelpers.IsIniKey(line, IniKeySwapSection.BackwardIniKey)) + { + oldBackwardKeyIndex = lineIndex; + } + + else if (IniConfigHelpers.IsSection(line)) + { + break; + } + } + + if (newBackwardKeyWrittenIndex != -1 && newForwardKeyWrittenIndex != -1) + throw new InvalidOperationException("Key bind writing error"); + + if (oldBackwardKeyIndex != -1 && oldForwardKeyIndex != -1) + throw new InvalidOperationException("key bind writing error"); + + if (oldBackwardKeyIndex != -1) + fileLines.RemoveAt(oldBackwardKeyIndex); + else if (oldForwardKeyIndex != -1) + fileLines.RemoveAt(oldForwardKeyIndex); + } + + cancellationToken.ThrowIfCancellationRequested(); + await using var writeStream = new FileStream(_modIniPath, FileMode.Truncate, FileAccess.Write, FileShare.None); + + await using (var writer = new StreamWriter(writeStream)) + { + foreach (var line in fileLines) + await writer.WriteLineAsync(line); + } + + await ReadKeySwapConfiguration(CancellationToken.None); + } + + public OneOf GetKeySwaps() + { + if (_keySwaps is null) + return new KeySwapsNotLoaded(); + + return _keySwaps.ToArray(); + } +} + +public struct KeySwapsNotLoaded +{ +} \ No newline at end of file diff --git a/src/GIMI-ModManager.Core/Entities/Mods/SkinMod/SkinModSettingsManager.cs b/src/GIMI-ModManager.Core/Entities/Mods/SkinMod/SkinModSettingsManager.cs new file mode 100644 index 00000000..025cd5ee --- /dev/null +++ b/src/GIMI-ModManager.Core/Entities/Mods/SkinMod/SkinModSettingsManager.cs @@ -0,0 +1,173 @@ +using System.Text.Json; +using GIMI_ModManager.Core.Contracts.Entities; +using GIMI_ModManager.Core.Entities.Mods.Contract; +using GIMI_ModManager.Core.Entities.Mods.FileModels; +using GIMI_ModManager.Core.Entities.Mods.Helpers; +using Newtonsoft.Json; +using OneOf; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace GIMI_ModManager.Core.Entities.Mods.SkinMod; + +public class SkinModSettingsManager +{ + private readonly ISkinMod _skinMod; + private const string configFileName = ".JASM_ModConfig.json"; + private const string ImageName = ".JASM_Cover"; + + private string _settingsFilePath => Path.Combine(_skinMod.FullPath, configFileName); + + private ModSettings? _settings; + + private readonly JsonSerializerOptions _serializerOptions = new() + { + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + WriteIndented = true + }; + + + internal SkinModSettingsManager(ISkinMod skinMod) + { + _skinMod = skinMod; + } + + + internal async Task InitializeAsync() + { + // Check if the settings file exists + + if (File.Exists(_settingsFilePath)) + { + var modSettings = await ReadSettingsAsync().ConfigureAwait(false); + + if (modSettings.Id == Guid.Empty) + { + modSettings.Id = Guid.NewGuid(); + await SaveSettingsAsync(modSettings).ConfigureAwait(false); + _settings = modSettings; + } + + + return modSettings.Id; + } + + var newId = Guid.NewGuid(); + var settings = new JsonModSettings() { Id = newId.ToString() }; + var json = JsonSerializer.Serialize(settings, _serializerOptions); + + await File.WriteAllTextAsync(_settingsFilePath, json).ConfigureAwait(false); + await ReadSettingsAsync().ConfigureAwait(false); + + return newId; + } + + internal void ClearSettings() => _settings = null; + + + public async Task ReadSettingsAsync() + { + if (!File.Exists(_settingsFilePath)) + throw new ModSettingsNotFoundException($"Settings file not found. Path: {_settingsFilePath}"); + + var json = await File.ReadAllTextAsync(_settingsFilePath); + + + var settings = JsonSerializer.Deserialize(json, _serializerOptions); + + + if (settings is null) + throw new JsonSerializationException("Failed to deserialize settings file. Return value is null"); + + var modSettings = ModSettings.FromJsonSkinSettings(_skinMod, settings); + _settings = modSettings; + return modSettings; + } + + private Task SaveSettingsAsync(JsonModSettings settings) + { + var json = JsonSerializer.Serialize(settings, _serializerOptions); + return File.WriteAllTextAsync(_settingsFilePath, json); + } + + + public async Task SaveSettingsAsync(ModSettings modSettings) + { + if (modSettings.ImagePath is not null && !ModsHelpers.IsInModFolder(_skinMod, modSettings.ImagePath)) + await CopyAndSetModImage(modSettings, modSettings.ImagePath); + + + if (modSettings.ImagePath is null) + await ClearModImage(); + + + var jsonSkinSettings = modSettings.ToJsonSkinSettings(_skinMod); + await SaveSettingsAsync(jsonSkinSettings); + _settings = modSettings; + } + + public OneOf GetSettings() + { + if (_settings is null) + return new SettingsNotLoaded(); + + return _settings; + } + + private Task CopyAndSetModImage(ModSettings modSettings, string imagePath) + { + var uri = Uri.TryCreate(imagePath, UriKind.Absolute, out var result) && + result.Scheme == Uri.UriSchemeFile + ? result + : null; + + if (uri is null) + throw new ArgumentException("Invalid image path.", nameof(imagePath)); + + return CopyAndSetModImage(modSettings, uri); + } + + private async Task CopyAndSetModImage(ModSettings modSettings, Uri imagePath) + { + var oldModSettings = _settings ?? await ReadSettingsAsync(); + if (!File.Exists(imagePath.LocalPath)) + throw new FileNotFoundException("Image file not found.", imagePath.LocalPath); + + + DeleteOldImage(oldModSettings.ImagePath); + + + var newImageFileName = ImageName + Path.GetExtension(imagePath.LocalPath); + var newImagePath = Path.Combine(_skinMod.FullPath, newImageFileName); + + File.Copy(imagePath.LocalPath, newImagePath, true); + modSettings.ImagePath = new Uri(newImagePath); + } + + + public async Task ClearModImage() + { + var modSettings = _settings ?? await ReadSettingsAsync(); + + DeleteOldImage(modSettings.ImagePath); + } + + private static void DeleteOldImage(Uri? oldImageUri) + { + if (oldImageUri is null || !File.Exists(oldImageUri.LocalPath)) + return; + + File.Delete(oldImageUri.LocalPath); + } +} + +public class ModSettingsNotFoundException : Exception +{ + public ModSettingsNotFoundException(string message) : base(message) + { + } +} + +public struct SettingsNotLoaded +{ +} \ No newline at end of file diff --git a/src/GIMI-ModManager.Core/Entities/SkinMod.cs b/src/GIMI-ModManager.Core/Entities/SkinMod.cs deleted file mode 100644 index bfc084b8..00000000 --- a/src/GIMI-ModManager.Core/Entities/SkinMod.cs +++ /dev/null @@ -1,576 +0,0 @@ -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using GIMI_ModManager.Core.Contracts.Entities; -using GIMI_ModManager.Core.Helpers; - -namespace GIMI_ModManager.Core.Entities; - -public class SkinMod : Mod, ISkinMod -{ - private const string ImageName = ".JASM_Cover"; - private const string ModIniName = "merged.ini"; - private string _modIniPath = string.Empty; - private const string configFileName = ".JASM_ModConfig.json"; - private string _configFilePath = string.Empty; - private readonly List _imagePaths = new(); - - private List? _keySwaps = new(); - - public IReadOnlyCollection ImagePaths => _imagePaths.AsReadOnly(); - public SkinModSettings? CachedSkinModSettings { get; private set; } = null; - public IReadOnlyCollection? CachedKeySwaps => _keySwaps?.AsReadOnly(); - public bool HasMergedInI { get; private set; } - - - public SkinMod(IMod mod) : base(new DirectoryInfo(mod.FullPath), mod.CustomName) - { - Init(); - } - - public SkinMod(string modPath, string customName = "") : base(new DirectoryInfo(Path.GetFullPath(modPath)), - customName) - { - Init(); - } - - public SkinMod(DirectoryInfo modDirectory, string customName = "") : base(modDirectory, customName) - { - Init(); - } - - public static async Task CreateWithSettingsAsync(DirectoryInfo modDirectory, - CancellationToken cancellationToken = default) - { - var skinMod = new SkinMod(modDirectory); - if (skinMod.HasMergedInI) - await skinMod.ReadKeySwapConfiguration(cancellationToken: cancellationToken); - await skinMod.ReadSkinModSettings(cancellationToken: cancellationToken); - return skinMod; - } - - private void Init() - { - var modFolderAttributes = File.GetAttributes(_modDirectory.FullName); - if (!modFolderAttributes.HasFlag(FileAttributes.Directory)) - throw new ArgumentException("Mod must be a folder.", nameof(_modDirectory.FullName)); - _imagePaths.Add(".JASM_Cover"); - Refresh(); - } - - public void Refresh() - { - _modDirectory.Refresh(); - - _configFilePath = Path.Combine(FullPath, configFileName); - _modIniPath = Path.Combine(FullPath, ModIniName); - - HasMergedInI = HasMergedInIFile(); - } - - private bool HasMergedInIFile() - { - return _modDirectory.EnumerateFiles("*.ini", SearchOption.TopDirectoryOnly) - .Any(iniFiles => iniFiles.Name.Equals(ModIniName, StringComparison.CurrentCultureIgnoreCase)); - } - - public bool IsValidFolder() - { - return Exists() && !IsEmpty(); - } - - public void ClearCache() - { - CachedSkinModSettings = null; - _keySwaps = null; - } - - public async Task> ReadKeySwapConfiguration(bool forceReload = false, - CancellationToken cancellationToken = default) - { - Refresh(); - if (!HasMergedInI) - throw new InvalidOperationException("Mod has no merged.ini file."); - - - if (CachedKeySwaps is not null && !forceReload) - return CachedKeySwaps; - - List keySwapLines = new(); - List keySwaps = new(); - var keySwapBlockStarted = false; - var currentLine = -1; - var sectionLine = string.Empty; - await foreach (var line in File.ReadLinesAsync(_modIniPath, cancellationToken)) - { - currentLine++; - if (line.Trim().StartsWith(";") || string.IsNullOrWhiteSpace(line)) - continue; - - if (IniConfigHelpers.IsSection(line) && keySwapBlockStarted || - keySwapBlockStarted && keySwapLines.Count > 9) - { - keySwapBlockStarted = false; - - var keySwap = IniConfigHelpers.ParseKeySwap(keySwapLines, sectionLine); - if (keySwap is not null) - keySwaps.Add(keySwap); - keySwapLines.Clear(); - - if (IniConfigHelpers.IsSection(line)) - { - sectionLine = line; - keySwapBlockStarted = true; - } - else - { - sectionLine = string.Empty; - } - - - continue; - } - - if (IniConfigHelpers.IsSection(line)) - { - keySwapBlockStarted = true; - sectionLine = line; - continue; - } - - if (keySwapLines.Count > 10 && !IniConfigHelpers.IsSection(line) && - keySwapBlockStarted) - { - keySwapBlockStarted = false; - sectionLine = string.Empty; - keySwapLines.Clear(); - continue; - } - - if (keySwapBlockStarted) - keySwapLines.Add(line); - } - - if (keySwaps.Count == 0) - return new List().AsReadOnly(); - - if (_keySwaps is null) - _keySwaps = new List(); - else - _keySwaps.Clear(); - - - _keySwaps.AddRange(keySwaps); - return _keySwaps.AsReadOnly(); - } - - // I wonder how long this abomination will stay in the codebase :) - // This is getting worse and worse - public async Task SaveKeySwapConfiguration(ICollection updatedKeySwaps, - CancellationToken cancellationToken = default) - { - Refresh(); - if (!HasMergedInI) - throw new InvalidOperationException("Mod has no merged.ini file."); - - if (updatedKeySwaps.Count == 0) - throw new ArgumentException("No key swaps to save.", nameof(updatedKeySwaps)); - - if (updatedKeySwaps.Count != _keySwaps?.Count) - throw new ArgumentException("Key swap count mismatch.", nameof(updatedKeySwaps)); - - - var fileLines = new List(); - - await using var fileStream = new FileStream(_modIniPath, FileMode.Open, FileAccess.Read, FileShare.None); - using (var reader = new StreamReader(fileStream)) - { - while (await reader.ReadLineAsync(cancellationToken) is { } line) - fileLines.Add(line); - } - - var sectionStartIndexes = new List(); - for (var i = 0; i < fileLines.Count; i++) - { - var currentLine = fileLines[i]; - if (updatedKeySwaps.Any(keySwap => IniConfigHelpers.IsSection(currentLine, keySwap.SectionKey))) - sectionStartIndexes.Add(i); - } - - if (sectionStartIndexes.Count != updatedKeySwaps.Count) - throw new InvalidOperationException("Key swap count mismatch."); - - if (sectionStartIndexes.Count == 0) - throw new InvalidOperationException("No key swaps found."); - - // Line numbers where the key swap sections starts - // We loop from the beginning to the end so we can remove or add lines without messing up the indexes - for (var i = sectionStartIndexes.Count - 1; i >= 0; i--) - { - var keySwap = updatedKeySwaps.ElementAt(i); - var sectionStartIndex = sectionStartIndexes[i] + 1; - - var newForwardKeyWrittenIndex = -1; - var newBackwardKeyWrittenIndex = -1; - - var oldForwardKeyIndex = -1; - var oldBackwardKeyIndex = -1; - - // When iterating a section go downwards instead of upwards - // 8 as the limit is just an arbitrary number so it doesn't loop forever - for (var lineIndex = sectionStartIndex; lineIndex < sectionStartIndex + 8; lineIndex++) - { - var line = fileLines[lineIndex]; - - if (newForwardKeyWrittenIndex == -1 && IniConfigHelpers.IsIniKey(line, SkinModKeySwap.ForwardIniKey)) - { - var value = IniConfigHelpers.FormatIniKey(SkinModKeySwap.ForwardIniKey, keySwap.ForwardHotkey); - if (value is null) - continue; - fileLines[lineIndex] = value; - - // If forwardkey is defined also set the backward key - if (keySwap.BackwardHotkey is null) continue; - - var backwardValue = - IniConfigHelpers.FormatIniKey(SkinModKeySwap.BackwardIniKey, keySwap.BackwardHotkey); - if (backwardValue is null) - continue; - newBackwardKeyWrittenIndex = lineIndex + 1; - fileLines.Insert(newBackwardKeyWrittenIndex, backwardValue); - } - - // Remove old forward key - else if (newForwardKeyWrittenIndex != -1 && newForwardKeyWrittenIndex != lineIndex && - IniConfigHelpers.IsIniKey(line, SkinModKeySwap.ForwardIniKey)) - { - oldForwardKeyIndex = lineIndex; - } - - else if (newBackwardKeyWrittenIndex == -1 && - IniConfigHelpers.IsIniKey(line, SkinModKeySwap.BackwardIniKey)) - { - var value = IniConfigHelpers.FormatIniKey(SkinModKeySwap.BackwardIniKey, keySwap.BackwardHotkey); - if (value is null) - continue; - fileLines[lineIndex] = value; - - // If backwardkey is defined also set the forward key - if (keySwap.ForwardHotkey is null) continue; - var forwardValue = - IniConfigHelpers.FormatIniKey(SkinModKeySwap.ForwardIniKey, keySwap.ForwardHotkey); - if (forwardValue is null) - continue; - newForwardKeyWrittenIndex = lineIndex + 1; - fileLines.Insert(newForwardKeyWrittenIndex, forwardValue); - } - - // Remove old backward key - else if (newBackwardKeyWrittenIndex != -1 && newBackwardKeyWrittenIndex != lineIndex && - IniConfigHelpers.IsIniKey(line, SkinModKeySwap.BackwardIniKey)) - { - oldBackwardKeyIndex = lineIndex; - } - - else if (IniConfigHelpers.IsSection(line)) - { - break; - } - } - - if (newBackwardKeyWrittenIndex != -1 && newForwardKeyWrittenIndex != -1) - throw new InvalidOperationException("Key bind writing error"); - - if (oldBackwardKeyIndex != -1 && oldForwardKeyIndex != -1) - throw new InvalidOperationException("key bind writing error"); - - if (oldBackwardKeyIndex != -1) - fileLines.RemoveAt(oldBackwardKeyIndex); - else if (oldForwardKeyIndex != -1) - fileLines.RemoveAt(oldForwardKeyIndex); - } - - cancellationToken.ThrowIfCancellationRequested(); - await using var writeStream = new FileStream(_modIniPath, FileMode.Truncate, FileAccess.Write, FileShare.None); - - await using (var writer = new StreamWriter(writeStream)) - { - foreach (var line in fileLines) - await writer.WriteLineAsync(line); - } - - await ReadKeySwapConfiguration(true, CancellationToken.None); - } - - - public async Task ReadSkinModSettings(bool forceReload = false, - CancellationToken cancellationToken = default) - { - Refresh(); - - if (CachedSkinModSettings is not null && !forceReload) - return CachedSkinModSettings; - - - if (!File.Exists(_configFilePath)) - return new SkinModSettings(); - - var fileContents = await File.ReadAllTextAsync(_configFilePath, cancellationToken); - var options = new JsonSerializerOptions - { - ReadCommentHandling = JsonCommentHandling.Skip, - AllowTrailingCommas = true - }; - var skinModSettings = - JsonSerializer.Deserialize(fileContents, options) ?? new SkinModSettings(); - - if (!SetAbsoluteImagePath(skinModSettings)) - skinModSettings.ImagePath = string.Empty; - - CachedSkinModSettings = skinModSettings; - SetCustomName(skinModSettings.CustomName ?? Name); - - return skinModSettings; - } - - - private Task CopyAndSetModImage(SkinModSettings skinModSettings) - { - var uri = Uri.TryCreate(skinModSettings.ImagePath, UriKind.Absolute, out var result) && - result.Scheme == Uri.UriSchemeFile - ? result - : throw new ArgumentException("Invalid image path.", nameof(skinModSettings.ImagePath)); - - if (!File.Exists(uri.LocalPath)) - throw new FileNotFoundException("Image file not found.", uri.LocalPath); - - // Delete old image - if (!string.IsNullOrWhiteSpace(CachedSkinModSettings?.ImagePath)) - { - if (Uri.TryCreate(CachedSkinModSettings.ImagePath, UriKind.Absolute, out var imagePath)) - { - var oldImagePath = imagePath.LocalPath; - if (File.Exists(oldImagePath)) - File.Delete(oldImagePath); - } - else - { - var oldImagePath = Path.Combine(FullPath, CachedSkinModSettings.ImagePath); - if (File.Exists(oldImagePath)) - File.Delete(oldImagePath); - } - } - - - var newImageFileName = ImageName + Path.GetExtension(uri.LocalPath); - var newImagePath = Path.Combine(FullPath, newImageFileName); - - File.Copy(uri.LocalPath, newImagePath, true); - skinModSettings.ImagePath = newImageFileName; - return Task.CompletedTask; - } - - - private string UriPathToModRelativePath(string? uriPath) - { - if (string.IsNullOrWhiteSpace(uriPath)) - return string.Empty; - - if (Uri.IsWellFormedUriString(uriPath, UriKind.Absolute)) - { - var filename = Path.GetFileName(uriPath); - return string.IsNullOrWhiteSpace(filename) ? string.Empty : filename; - } - - var absPath = Path.GetFileName(uriPath); - - var file = Path.GetFileName(absPath); - return string.IsNullOrWhiteSpace(file) ? string.Empty : file; - } - - public async Task SaveSkinModSettings(SkinModSettings skinModSettings, - CancellationToken cancellationToken = default) - { - Refresh(); - - if (CachedSkinModSettings is not null && skinModSettings.Equals(CachedSkinModSettings)) - return; - - if (!string.IsNullOrWhiteSpace(skinModSettings.ImagePath) && - CachedSkinModSettings?.ImagePath != skinModSettings.ImagePath - || - (!string.IsNullOrWhiteSpace(skinModSettings.ImagePath) && - Uri.IsWellFormedUriString(skinModSettings.ImagePath, UriKind.Absolute)) && - CachedSkinModSettings?.ImagePath != skinModSettings.ImagePath) - - await CopyAndSetModImage(skinModSettings); - - skinModSettings.ImagePath = UriPathToModRelativePath(skinModSettings.ImagePath); - - var options = new JsonSerializerOptions - { - ReadCommentHandling = JsonCommentHandling.Skip, - AllowTrailingCommas = true, - WriteIndented = true - }; - - var json = JsonSerializer.Serialize(skinModSettings, options); - await File.WriteAllTextAsync(_configFilePath, json, cancellationToken); - - await ReadSkinModSettings(true, CancellationToken.None).ConfigureAwait(false); - } - - /// - /// This checks that the image path is a valid absolute path or a valid relative path to the mod folder. Also updates the image path if it's relative. - /// - private bool SetAbsoluteImagePath(SkinModSettings skinModSettings) - { - if (!string.IsNullOrWhiteSpace(skinModSettings.ImagePath)) - { - if (File.Exists(skinModSettings.ImagePath)) // Is Absolute path - { - skinModSettings.ImagePath = skinModSettings.ImagePath; - return true; - } - else // Is Relative to mod folder - { - var imagePath = Path.Combine(FullPath, skinModSettings.ImagePath); - if (File.Exists(imagePath)) - { - skinModSettings.ImagePath = new Uri(imagePath).ToString(); - return true; - } - } - } - - // No image path set or image path not found - return false; - } - - public bool Equals(ISkinMod? x, ISkinMod? y) - { - if (ReferenceEquals(x, y)) return true; - if (ReferenceEquals(x, null)) return false; - if (ReferenceEquals(y, null)) return false; - return string.Equals(x.FullPath, y.FullPath, StringComparison.CurrentCultureIgnoreCase); - } - - public int GetHashCode(ISkinMod obj) - { - return StringComparer.CurrentCultureIgnoreCase.GetHashCode(obj.FullPath); - } -} - -public class SkinModSettings // "internal set" messes with the json serializer - : IEquatable -{ - public bool Equals(SkinModSettings? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return CustomName == other.CustomName && Author == other.Author && Version == other.Version && - ModUrl == other.ModUrl && Path.GetFileName(ImagePath) == Path.GetFileName(other.ImagePath) && - CharacterSkinOverride == other.CharacterSkinOverride; - } - - public override bool Equals(object? obj) - { - return ReferenceEquals(this, obj) || obj is SkinModSettings other && Equals(other); - } - - public override int GetHashCode() - { - return HashCode.Combine(CustomName, Author, Version, ModUrl, ImagePath); - } - - public SkinModSettings DeepClone() - { - return new () - { - CustomName = CustomName, - Author = Author, - Version = Version, - ModUrl = ModUrl, - ImagePath = ImagePath, - CharacterSkinOverride = CharacterSkinOverride - }; - } - - public string? CustomName { get; set; } - public string? Author { get; set; } - public string? Version { get; set; } - public string? ModUrl { get; set; } - public string? ImagePath { get; set; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? CharacterSkinOverride { get; set; } -} - -// There needs to be a better way to do this -public class SkinModKeySwap : IEquatable -{ - public Dictionary IniKeyValues { get; } = new(); - - public const string KeySwapIniSection = "KeySwap"; - public string SectionKey { get; set; } = KeySwapIniSection; - - public const string ForwardIniKey = "key"; - - public string? ForwardHotkey - { - get => IniKeyValues.TryGetValue(ForwardIniKey, out var value) ? value : null; - set => IniKeyValues[ForwardIniKey] = value ?? string.Empty; - } - - public const string BackwardIniKey = "back"; - - public string? BackwardHotkey - { - get => IniKeyValues.TryGetValue(BackwardIniKey, out var value) ? value : null; - set => IniKeyValues[BackwardIniKey] = value ?? string.Empty; - } - - public const string TypeIniKey = "type"; - - public string? Type - { - get => IniKeyValues.TryGetValue(TypeIniKey, out var value) ? value : null; - set => IniKeyValues[TypeIniKey] = value ?? string.Empty; - } - - public const string SwapVarIniKey = "$swapvar"; - public string[]? SwapVar { get; set; } - - public bool AnyValues() - { - return ForwardHotkey is not null || BackwardHotkey is not null; - } - - public bool Equals(SkinModKeySwap? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return ForwardHotkey == other.ForwardHotkey && - BackwardHotkey == other.BackwardHotkey && Type == other.Type && Equals(SwapVar, other.SwapVar); - } - - public override bool Equals(object? obj) - { - return ReferenceEquals(this, obj) || obj is SkinModKeySwap other && Equals(other); - } - - public override int GetHashCode() - { - return HashCode.Combine(ForwardHotkey, BackwardHotkey, Type, SwapVar); - } - - public override string ToString() - { - var sb = new StringBuilder(); - sb.Append("Section: "); - sb.Append(SectionKey + " | "); - foreach (var iniKeyValue in IniKeyValues) sb.Append($"{iniKeyValue.Key}: {iniKeyValue.Value} | "); - - return sb.ToString(); - } -} \ No newline at end of file diff --git a/src/GIMI-ModManager.Core/GIMI-ModManager.Core.csproj b/src/GIMI-ModManager.Core/GIMI-ModManager.Core.csproj index 011488dd..2e50563c 100644 --- a/src/GIMI-ModManager.Core/GIMI-ModManager.Core.csproj +++ b/src/GIMI-ModManager.Core/GIMI-ModManager.Core.csproj @@ -18,6 +18,7 @@ + diff --git a/src/GIMI-ModManager.Core/Helpers/DuplicateModAffixHelper.cs b/src/GIMI-ModManager.Core/Helpers/DuplicateModAffixHelper.cs index 1fae391b..7f1ca63a 100644 --- a/src/GIMI-ModManager.Core/Helpers/DuplicateModAffixHelper.cs +++ b/src/GIMI-ModManager.Core/Helpers/DuplicateModAffixHelper.cs @@ -7,7 +7,11 @@ public static partial class DuplicateModAffixHelper [GeneratedRegex(@"__\d+$", RegexOptions.IgnoreCase)] private static partial Regex DuplicateModAffix(); - + /// + /// Tries to append a number to the end of the string, if it already has a number, it increments it. + /// + /// Some string with or without a number at the end + /// New string with a number or incremented number public static string AppendNumberAffix(string name) { ArgumentNullException.ThrowIfNull(name); diff --git a/src/GIMI-ModManager.Core/Helpers/Extensions.cs b/src/GIMI-ModManager.Core/Helpers/Extensions.cs new file mode 100644 index 00000000..84743794 --- /dev/null +++ b/src/GIMI-ModManager.Core/Helpers/Extensions.cs @@ -0,0 +1,25 @@ +namespace GIMI_ModManager.Core.Helpers; + +public static class Extensions +{ + /// + /// Compares two absolute paths, ignoring case and trailing directory separators. + /// + /// + /// + /// + public static bool AbsPathCompare(this string absPath, string absOtherPath) + { + return Path.GetFullPath(absPath) + .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + .Equals( + Path.GetFullPath(absOtherPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), + StringComparison.CurrentCultureIgnoreCase); + } + + public static void ForEach(this IEnumerable enumerable, Action action) + { + foreach (var item in enumerable) + action(item); + } +} \ No newline at end of file diff --git a/src/GIMI-ModManager.Core/Helpers/IniConfigHelpers.cs b/src/GIMI-ModManager.Core/Helpers/IniConfigHelpers.cs index 2243def5..1e191d0e 100644 --- a/src/GIMI-ModManager.Core/Helpers/IniConfigHelpers.cs +++ b/src/GIMI-ModManager.Core/Helpers/IniConfigHelpers.cs @@ -1,29 +1,29 @@ -using GIMI_ModManager.Core.Entities; +using GIMI_ModManager.Core.Entities.Mods.FileModels; namespace GIMI_ModManager.Core.Helpers; // This class just holds code that i don't know where to put yet. public static class IniConfigHelpers { - public static SkinModKeySwap? ParseKeySwap(ICollection fileLines, string sectionLine) + public static IniKeySwapSection? ParseKeySwap(ICollection fileLines, string sectionLine) { - var skinModKeySwap = new SkinModKeySwap + var skinModKeySwap = new IniKeySwapSection { SectionKey = sectionLine.Trim() }; foreach (var line in fileLines) { - if (IsIniKey(line, SkinModKeySwap.ForwardIniKey)) + if (IsIniKey(line, IniKeySwapSection.ForwardIniKey)) skinModKeySwap.ForwardHotkey = GetIniValue(line); - else if (IsIniKey(line, SkinModKeySwap.BackwardIniKey)) + else if (IsIniKey(line, IniKeySwapSection.BackwardIniKey)) skinModKeySwap.BackwardHotkey = GetIniValue(line); - else if (IsIniKey(line, SkinModKeySwap.TypeIniKey)) + else if (IsIniKey(line, IniKeySwapSection.TypeIniKey)) skinModKeySwap.Type = GetIniValue(line); - else if (IsIniKey(line, SkinModKeySwap.SwapVarIniKey)) + else if (IsIniKey(line, IniKeySwapSection.SwapVarIniKey)) skinModKeySwap.SwapVar = GetIniValue(line)?.Split(','); else if (IsSection(line)) diff --git a/src/GIMI-ModManager.Core/Helpers/ModFolderHelpers.cs b/src/GIMI-ModManager.Core/Helpers/ModFolderHelpers.cs index 92e1b77b..dee65d9f 100644 --- a/src/GIMI-ModManager.Core/Helpers/ModFolderHelpers.cs +++ b/src/GIMI-ModManager.Core/Helpers/ModFolderHelpers.cs @@ -39,6 +39,13 @@ public static string GetFolderNameWithDisabledPrefix(string folderName) return DISABLED_PREFIX + folderName; } + /// + /// Compares two folder names, ignoring case and the disabled prefix. + /// + /// + /// + /// Specify if folderNames are absolute paths + /// public static bool FolderNameEquals(string folderName1, string folderName2, bool absolutePaths = false) { if (absolutePaths) @@ -50,4 +57,19 @@ public static bool FolderNameEquals(string folderName1, string folderName2, bool return GetFolderNameWithoutDisabledPrefix(folderName1).Equals(GetFolderNameWithoutDisabledPrefix(folderName2), StringComparison.CurrentCultureIgnoreCase); } + + /// + /// Checks if the folder name has the disabled prefix. + /// + /// + /// Specify if folderName is absolute path or just the folder name + /// + public static bool FolderHasDisabledPrefix(string folderName, bool absolutePath = false) + { + if (absolutePath) + folderName = new DirectoryInfo(folderName).Name; + + return folderName.StartsWith(DISABLED_PREFIX, StringComparison.CurrentCultureIgnoreCase) || + folderName.StartsWith(ALT_DISABLED_PREFIX, StringComparison.CurrentCultureIgnoreCase); + } } \ No newline at end of file diff --git a/src/GIMI-ModManager.Core/Services/GenshinService.cs b/src/GIMI-ModManager.Core/Services/GenshinService.cs index eca1f561..fd5afba8 100644 --- a/src/GIMI-ModManager.Core/Services/GenshinService.cs +++ b/src/GIMI-ModManager.Core/Services/GenshinService.cs @@ -111,7 +111,7 @@ public static Dictionary SearchCharacters(string searchQu IEnumerable characters, int minScore = 100) { var searchResult = new Dictionary(); - searchQuery = searchQuery.ToLower(); + searchQuery = searchQuery.ToLower().Trim(); foreach (var character in characters) { @@ -128,6 +128,9 @@ public static Dictionary SearchCharacters(string searchQu var bestKeyMatch = character.Keys.Max(key => Fuzz.Ratio(key, searchQuery)); result += bestKeyMatch; + if (character.Keys.Any(key => key.Equals(searchQuery, StringComparison.CurrentCultureIgnoreCase))) + result += 100; + var splitNames = loweredDisplayName.Split(); var sameStartChars = 0; diff --git a/src/GIMI-ModManager.Core/Services/ModCrawlerService.cs b/src/GIMI-ModManager.Core/Services/ModCrawlerService.cs index 15ba7f98..2cd4b8e5 100644 --- a/src/GIMI-ModManager.Core/Services/ModCrawlerService.cs +++ b/src/GIMI-ModManager.Core/Services/ModCrawlerService.cs @@ -28,7 +28,7 @@ public IEnumerable GetSubSkinsRecursive(string absPath) var subSkin = subSkins.FirstOrDefault(skin => IsOfSkinType(file, skin)); if (subSkin is null) continue; - _logger.Debug("Detected subSkin {subSkin} for folder {folder}", subSkin.Name, folder.FullName); + _logger.Verbose("Detected subSkin {subSkin} for folder {folder}", subSkin.Name, folder.FullName); yield return subSkin; } @@ -49,7 +49,7 @@ public IEnumerable GetSubSkinsRecursive(string absPath) var subSkin = subSkins.FirstOrDefault(skin => IsOfSkinType(file, skin)); if (subSkin is null) continue; - _logger.Debug("Detected subSkin {subSkin} for folder {folder}", subSkin.Name, folder.FullName); + _logger.Verbose("Detected subSkin {subSkin} for folder {folder}", subSkin.Name, folder.FullName); return subSkin; } diff --git a/src/GIMI-ModManager.Core/Services/SkinManagerService.cs b/src/GIMI-ModManager.Core/Services/SkinManagerService.cs index 9e4d39c2..9080a51a 100644 --- a/src/GIMI-ModManager.Core/Services/SkinManagerService.cs +++ b/src/GIMI-ModManager.Core/Services/SkinManagerService.cs @@ -2,7 +2,10 @@ using GIMI_ModManager.Core.Contracts.Services; using GIMI_ModManager.Core.Entities; using GIMI_ModManager.Core.Entities.Genshin; +using GIMI_ModManager.Core.Entities.Mods.SkinMod; using GIMI_ModManager.Core.Helpers; +using OneOf; +using OneOf.Types; using Serilog; using static GIMI_ModManager.Core.Contracts.Services.RefreshResult; @@ -12,6 +15,7 @@ public sealed class SkinManagerService : ISkinManagerService { private readonly IGenshinService _genshinService; private readonly ILogger _logger; + private readonly ModCrawlerService _modCrawlerService; private DirectoryInfo _unloadedModsFolder = null!; private DirectoryInfo _activeModsFolder = null!; @@ -21,9 +25,10 @@ public sealed class SkinManagerService : ISkinManagerService private readonly List _characterModLists = new(); public IReadOnlyCollection CharacterModLists => _characterModLists.AsReadOnly(); - public SkinManagerService(IGenshinService genshinService, ILogger logger) + public SkinManagerService(IGenshinService genshinService, ILogger logger, ModCrawlerService modCrawlerService) { _genshinService = genshinService; + _modCrawlerService = modCrawlerService; _logger = logger.ForContext(); } @@ -32,7 +37,7 @@ public SkinManagerService(IGenshinService genshinService, ILogger logger) public bool UnloadingModsEnabled { get; private set; } - public Task ScanForModsAsync() + public async Task ScanForModsAsync() { _activeModsFolder.Refresh(); @@ -51,12 +56,17 @@ public Task ScanForModsAsync() foreach (var modFolder in characterModFolder.EnumerateDirectories()) { - var mod = new SkinMod(modFolder, modFolder.Name); - characterModList.TrackMod(mod); + try + { + var mod = await SkinMod.CreateModAsync(modFolder.FullName); + characterModList.TrackMod(mod); + } + catch (Exception e) + { + _logger.Error(e, "Failed to initialize mod '{ModFolder}'", modFolder.FullName); + } } } - - return Task.CompletedTask; } public async Task RefreshModsAsync(GenshinCharacter? refreshForCharacter = null) @@ -64,6 +74,7 @@ public async Task RefreshModsAsync(GenshinCharacter? refreshForCh var modsUntracked = new List(); var newModsFound = new List(); var duplicateModsFound = new List(); + var errors = new List(); foreach (var characterModList in _characterModLists) { @@ -80,7 +91,7 @@ public async Task RefreshModsAsync(GenshinCharacter? refreshForCh foreach (var x in characterModList.Mods) { - if (x.Mod.FullPath.Equals(modDirectory.FullName, StringComparison.CurrentCultureIgnoreCase) + if (x.Mod.FullPath.AbsPathCompare(modDirectory.FullName) && Directory.Exists(Path.Combine(characterModList.AbsModsFolderPath, ModFolderHelpers.GetFolderNameWithDisabledPrefix(modDirectory.Name))) @@ -106,7 +117,7 @@ public async Task RefreshModsAsync(GenshinCharacter? refreshForCh break; } - if (x.Mod.FullPath == modDirectory.FullName) + if (x.Mod.FullPath.AbsPathCompare(modDirectory.FullName)) { mod = x; mod.Mod.ClearCache(); @@ -115,7 +126,7 @@ public async Task RefreshModsAsync(GenshinCharacter? refreshForCh } var disabledName = ModFolderHelpers.GetFolderNameWithDisabledPrefix(modDirectory.Name); - if (x.Mod.FullPath == Path.Combine(characterModList.AbsModsFolderPath, disabledName)) + if (x.Mod.FullPath.AbsPathCompare(Path.Combine(characterModList.AbsModsFolderPath, disabledName))) { mod = x; mod.Mod.ClearCache(); @@ -126,11 +137,26 @@ public async Task RefreshModsAsync(GenshinCharacter? refreshForCh if (mod is not null) continue; - var newMod = new SkinMod(modDirectory, modDirectory.Name); - characterModList.TrackMod(newMod); - newModsFound.Add(newMod); - _logger.Debug("Found new mod '{ModName}' in '{CharacterFolder}'", newMod.Name, - characterModList.Character.DisplayName); + try + { + var newMod = await SkinMod.CreateModAsync(modDirectory.FullName); + + if (GetModById(newMod.Id) is not null) + { + newMod = await SkinMod.CreateModAsync(modDirectory.FullName, true); + } + + characterModList.TrackMod(newMod); + newModsFound.Add(newMod); + _logger.Debug("Found new mod '{ModName}' in '{CharacterFolder}'", newMod.Name, + characterModList.Character.DisplayName); + } + catch (Exception e) + { + _logger.Error(e, "Failed to create mod from folder '{ModFolder}'", modDirectory.FullName); + errors.Add( + $"Failed to track new mod folder: '{modDirectory.FullName}' | For character {characterModList.Character.DisplayName}"); + } } orphanedMods.ForEach(x => @@ -142,11 +168,12 @@ public async Task RefreshModsAsync(GenshinCharacter? refreshForCh }); } - return new RefreshResult(modsUntracked, newModsFound, duplicateModsFound); + return new RefreshResult(modsUntracked, newModsFound, duplicateModsFound, errors: errors); } - public async Task TransferMods(ICharacterModList source, ICharacterModList destination, + public async Task[]>> TransferMods(ICharacterModList source, + ICharacterModList destination, IEnumerable modsEntryIds) { var mods = source.Mods.Where(x => modsEntryIds.Contains(x.Id)).Select(x => x.Mod).ToList(); @@ -168,21 +195,37 @@ public async Task TransferMods(ICharacterModList source, ICharacterModList desti using var sourceDisabled = source.DisableWatcher(); using var destinationDisabled = destination.DisableWatcher(); + var errors = new List>(); foreach (var mod in mods) { source.UnTrackMod(mod); mod.MoveTo(destination.AbsModsFolderPath); destination.TrackMod(mod); - // Remove overrides, i.e. skinOverride - var skinModSettings = (await mod.ReadSkinModSettings()).DeepClone(); - skinModSettings.CharacterSkinOverride = null; - await mod.SaveSkinModSettings(skinModSettings); + try + { + var skinSettings = await mod.Settings.ReadSettingsAsync(); + skinSettings.CharacterSkinOverride = null; + await mod.Settings.SaveSettingsAsync(skinSettings); + } + catch (Exception e) + { + _logger.Error(e, "Failed to clear skin override for mod '{ModName}'", mod.Name); + errors.Add(new Error( + $"Failed to clear skin override for mod '{mod.Name}'. Reason: {e.Message}")); + } } + + return errors.Any() ? errors.ToArray() : new Success(); } public event EventHandler? ModExportProgress; + public ISkinMod? GetModById(Guid id) + { + return _characterModLists.SelectMany(x => x.Mods).FirstOrDefault(x => x.Id == id)?.Mod; + } + public void ExportMods(ICollection characterModLists, string exportPath, bool removeLocalJasmSettings = true, bool zip = true, bool keepCharacterFolderStructure = false, SetModStatus setModStatus = SetModStatus.KeepCurrent) @@ -433,7 +476,8 @@ private void InitializeFolderStructure() } } - public int ReorganizeMods(GenshinCharacter? characterFolderToReorganize = null) + public async Task ReorganizeModsAsync(GenshinCharacter? characterFolderToReorganize = null, + bool disableMods = false) { if (_activeModsFolder is null) throw new InvalidOperationException("ModManagerService is not initialized"); @@ -465,8 +509,12 @@ public int ReorganizeMods(GenshinCharacter? characterFolderToReorganize = null) if (character is not null) continue; - // Is a mod folder, determine which character it belongs to - var closestMatchCharacter = _genshinService.GetCharacter(folder.Name); + + var closestMatchCharacter = + _modCrawlerService.GetFirstSubSkinRecursive(folder.FullName)?.Character as GenshinCharacter ?? + _genshinService.GetCharacter(folder.Name); + + switch (closestMatchCharacter) { case null when characterFolderToReorganize is null: @@ -481,10 +529,40 @@ public int ReorganizeMods(GenshinCharacter? characterFolderToReorganize = null) var modList = GetCharacterModList(closestMatchCharacter); - var mod = new SkinMod(folder, folder.Name); + ISkinMod? mod = null; + try + { + mod = await SkinMod.CreateModAsync(folder); + } + catch (Exception e) + { + _logger.Error(e, "Failed to initialize mod folder '{ModFolder}'", folder.FullName); + continue; + } + try { using var disableWatcher = modList.DisableWatcher(); + + var renameAttempts = 0; + while (modList.FolderAlreadyExists(mod.Name)) + { + var oldName = mod.Name; + mod.Rename(DuplicateModAffixHelper.AppendNumberAffix(mod.Name)); + _logger.Information( + "Mod '{ModName}' already exists in '{CharacterFolder}', renaming to {NewModName}", + oldName, closestMatchCharacter.DisplayName, mod.Name); + renameAttempts++; + if (renameAttempts <= 10) continue; + _logger.Error( + "Failed to rename mod '{ModName}' to '{NewModName}' after 10 attempts, skipping mod", + mod.Name, mod.Name); + break; + } + + if (renameAttempts > 10) continue; + + mod.MoveTo(GetCharacterModList(closestMatchCharacter).AbsModsFolderPath); _logger.Information("Moved mod '{ModName}' to '{CharacterFolder}' mod folder", mod.Name, closestMatchCharacter.DisplayName); @@ -498,6 +576,17 @@ public int ReorganizeMods(GenshinCharacter? characterFolderToReorganize = null) } modList.TrackMod(mod); + if (disableMods && modList.IsModEnabled(mod)) + { + try + { + modList.DisableMod(mod.Id); + } + catch (Exception e) + { + _logger.Error(e, "Failed to disable mod '{ModName}'", mod.Name); + } + } } return movedMods; diff --git a/src/GIMI-ModManager.WinUI/Activation/FirstTimeStartupActivationHandler.cs b/src/GIMI-ModManager.WinUI/Activation/FirstTimeStartupActivationHandler.cs index d5e8ac35..9327faf5 100644 --- a/src/GIMI-ModManager.WinUI/Activation/FirstTimeStartupActivationHandler.cs +++ b/src/GIMI-ModManager.WinUI/Activation/FirstTimeStartupActivationHandler.cs @@ -1,6 +1,6 @@ using GIMI_ModManager.Core.Contracts.Services; using GIMI_ModManager.WinUI.Contracts.Services; -using GIMI_ModManager.WinUI.Models; +using GIMI_ModManager.WinUI.Models.Options; using GIMI_ModManager.WinUI.ViewModels; using Microsoft.UI.Xaml; @@ -31,8 +31,7 @@ protected override bool CanHandleInternal(LaunchActivatedEventArgs args) .Run(async () => await _localSettingsService.ReadSettingAsync(ModManagerOptions.Section)) .GetAwaiter().GetResult(); - return !string.IsNullOrEmpty(options?.GimiRootFolderPath) && - !string.IsNullOrEmpty(options?.ModsFolderPath); + return Directory.Exists(options?.ModsFolderPath) && Directory.Exists(options?.GimiRootFolderPath); } protected override async Task HandleInternalAsync(LaunchActivatedEventArgs args) @@ -40,7 +39,8 @@ protected override async Task HandleInternalAsync(LaunchActivatedEventArgs args) var modManagerOptions = await _localSettingsService.ReadSettingAsync(ModManagerOptions.Section); - await _skinManagerService.Initialize(modManagerOptions!.ModsFolderPath!, null, modManagerOptions.GimiRootFolderPath); + await _skinManagerService.Initialize(modManagerOptions!.ModsFolderPath!, null, + modManagerOptions.GimiRootFolderPath); _navigationService.NavigateTo(typeof(CharactersViewModel).FullName!, args.Arguments, true); } } \ No newline at end of file diff --git a/src/GIMI-ModManager.WinUI/App.xaml.cs b/src/GIMI-ModManager.WinUI/App.xaml.cs index a81e5a21..c5239725 100644 --- a/src/GIMI-ModManager.WinUI/App.xaml.cs +++ b/src/GIMI-ModManager.WinUI/App.xaml.cs @@ -5,14 +5,18 @@ using GIMI_ModManager.Core.Services; using GIMI_ModManager.WinUI.Activation; using GIMI_ModManager.WinUI.Contracts.Services; -using GIMI_ModManager.WinUI.Models; +using GIMI_ModManager.WinUI.Models.Options; using GIMI_ModManager.WinUI.Services; +using GIMI_ModManager.WinUI.Services.AppManagment; +using GIMI_ModManager.WinUI.Services.AppManagment.Updating; +using GIMI_ModManager.WinUI.Services.ModHandling; using GIMI_ModManager.WinUI.Services.Notifications; using GIMI_ModManager.WinUI.ViewModels; using GIMI_ModManager.WinUI.Views; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; using Serilog; using Serilog.Templates; using WinUI3Localizer; @@ -47,6 +51,7 @@ public static T GetService() public static UIElement? AppTitlebar { get; set; } public static bool OverrideShutdown { get; set; } + public static bool UnhandledExceptionHandled { get; set; } public App() { @@ -97,6 +102,8 @@ public App() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); // Views and ViewModels services.AddTransient(); @@ -126,6 +133,21 @@ private async void App_UnhandledException(object sender, Microsoft.UI.Xaml.Unhan { Log.Fatal(e.Exception, "Unhandled exception"); await Log.CloseAndFlushAsync(); + + if (UnhandledExceptionHandled) + return; + // show error dialog + var window = new ErrorWindow(e.Exception) + { + IsAlwaysOnTop = true, + Title = "JASM - Unhandled Exception", + SystemBackdrop = new MicaBackdrop() + }; + window.Activate(); + window.CenterOnScreen(); + MainWindow.Hide(); + e.Handled = true; + UnhandledExceptionHandled = true; } diff --git a/src/GIMI-ModManager.WinUI/Assets/characters.json b/src/GIMI-ModManager.WinUI/Assets/characters.json index cf72d3cf..e7aab645 100644 --- a/src/GIMI-ModManager.WinUI/Assets/characters.json +++ b/src/GIMI-ModManager.WinUI/Assets/characters.json @@ -1500,7 +1500,8 @@ "Id": 53, "DisplayName": "Tartaglia", "Keys": [ - "tartaglia" + "tartaglia", + "childe" ], "ReleaseDate": "2020-11-11T00:00:00", "ImageUri": "Character_Tartaglia_Thumb.png", diff --git a/src/GIMI-ModManager.WinUI/GIMI-ModManager.WinUI.csproj b/src/GIMI-ModManager.WinUI/GIMI-ModManager.WinUI.csproj index 78b0ca4b..c9cd6e96 100644 --- a/src/GIMI-ModManager.WinUI/GIMI-ModManager.WinUI.csproj +++ b/src/GIMI-ModManager.WinUI/GIMI-ModManager.WinUI.csproj @@ -25,12 +25,16 @@ + + + + @@ -160,9 +164,11 @@ + + @@ -507,6 +513,12 @@ PreserveNewest + + MSBuild:Compile + + + MSBuild:Compile + MSBuild:Compile @@ -661,4 +673,8 @@ + + + + diff --git a/src/GIMI-ModManager.WinUI/Helpers/Constants.cs b/src/GIMI-ModManager.WinUI/Helpers/Constants.cs new file mode 100644 index 00000000..48585939 --- /dev/null +++ b/src/GIMI-ModManager.WinUI/Helpers/Constants.cs @@ -0,0 +1,7 @@ +namespace GIMI_ModManager.WinUI.Helpers; + +public static class Constants +{ + public static Uri JASM_GITHUB { get; } = new("https://github.com/Jorixon/JASM"); + public static Uri JASM_GAMEBANANA { get; } = new("https://gamebanana.com/tools/14574"); +} \ No newline at end of file diff --git a/src/GIMI-ModManager.WinUI/Helpers/AttentionTypeToSymbol.cs b/src/GIMI-ModManager.WinUI/Helpers/Xaml/AttentionTypeToSymbol.cs similarity index 88% rename from src/GIMI-ModManager.WinUI/Helpers/AttentionTypeToSymbol.cs rename to src/GIMI-ModManager.WinUI/Helpers/Xaml/AttentionTypeToSymbol.cs index c918cdc9..75e48ca8 100644 --- a/src/GIMI-ModManager.WinUI/Helpers/AttentionTypeToSymbol.cs +++ b/src/GIMI-ModManager.WinUI/Helpers/Xaml/AttentionTypeToSymbol.cs @@ -1,7 +1,7 @@ -using GIMI_ModManager.WinUI.Models.Options; +using GIMI_ModManager.WinUI.Services.Notifications; using Microsoft.UI.Xaml.Data; -namespace GIMI_ModManager.WinUI.Helpers; +namespace GIMI_ModManager.WinUI.Helpers.Xaml; public class AttentionTypeToSymbolConverter : IValueConverter { @@ -14,14 +14,14 @@ public object Convert(object? value, Type targetType, object parameter, string l if (value is not null && value.GetType().IsEnum) { - return GetSymbol((AttentionType) value); + return GetSymbol((AttentionType)value); } if (parameter is string enumString) { if (Enum.TryParse(typeof(AttentionType), enumString, out var result)) { - return GetSymbol((AttentionType) result); + return GetSymbol((AttentionType)result); } } diff --git a/src/GIMI-ModManager.WinUI/Helpers/BoolInverterConverter.cs b/src/GIMI-ModManager.WinUI/Helpers/Xaml/BoolInverterConverter.cs similarity index 84% rename from src/GIMI-ModManager.WinUI/Helpers/BoolInverterConverter.cs rename to src/GIMI-ModManager.WinUI/Helpers/Xaml/BoolInverterConverter.cs index 8b763d2f..20347c39 100644 --- a/src/GIMI-ModManager.WinUI/Helpers/BoolInverterConverter.cs +++ b/src/GIMI-ModManager.WinUI/Helpers/Xaml/BoolInverterConverter.cs @@ -1,7 +1,6 @@ -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Data; -namespace GIMI_ModManager.WinUI.Helpers; +namespace GIMI_ModManager.WinUI.Helpers.Xaml; public class BoolInverterConverter : IValueConverter { diff --git a/src/GIMI-ModManager.WinUI/Helpers/BoolToVisibleConverter.cs b/src/GIMI-ModManager.WinUI/Helpers/Xaml/BoolToVisibleConverter.cs similarity index 93% rename from src/GIMI-ModManager.WinUI/Helpers/BoolToVisibleConverter.cs rename to src/GIMI-ModManager.WinUI/Helpers/Xaml/BoolToVisibleConverter.cs index ca88490a..b985ddd9 100644 --- a/src/GIMI-ModManager.WinUI/Helpers/BoolToVisibleConverter.cs +++ b/src/GIMI-ModManager.WinUI/Helpers/Xaml/BoolToVisibleConverter.cs @@ -1,7 +1,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Data; -namespace GIMI_ModManager.WinUI.Helpers; +namespace GIMI_ModManager.WinUI.Helpers.Xaml; internal class BoolToVisibleConverter : IValueConverter { diff --git a/src/GIMI-ModManager.WinUI/Helpers/EnumToBooleanConverter.cs b/src/GIMI-ModManager.WinUI/Helpers/Xaml/EnumToBooleanConverter.cs similarity index 95% rename from src/GIMI-ModManager.WinUI/Helpers/EnumToBooleanConverter.cs rename to src/GIMI-ModManager.WinUI/Helpers/Xaml/EnumToBooleanConverter.cs index e76e7996..a80a8886 100644 --- a/src/GIMI-ModManager.WinUI/Helpers/EnumToBooleanConverter.cs +++ b/src/GIMI-ModManager.WinUI/Helpers/Xaml/EnumToBooleanConverter.cs @@ -1,7 +1,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Data; -namespace GIMI_ModManager.WinUI.Helpers; +namespace GIMI_ModManager.WinUI.Helpers.Xaml; public class EnumToBooleanConverter : IValueConverter { @@ -35,4 +35,4 @@ public object ConvertBack(object value, Type targetType, object parameter, strin throw new ArgumentException("ExceptionEnumToBooleanConverterParameterMustBeAnEnumName"); } -} +} \ No newline at end of file diff --git a/src/GIMI-ModManager.WinUI/Helpers/StringToUri.cs b/src/GIMI-ModManager.WinUI/Helpers/Xaml/StringToUri.cs similarity index 92% rename from src/GIMI-ModManager.WinUI/Helpers/StringToUri.cs rename to src/GIMI-ModManager.WinUI/Helpers/Xaml/StringToUri.cs index 852bcc08..655f1862 100644 --- a/src/GIMI-ModManager.WinUI/Helpers/StringToUri.cs +++ b/src/GIMI-ModManager.WinUI/Helpers/Xaml/StringToUri.cs @@ -1,6 +1,6 @@ using Microsoft.UI.Xaml.Data; -namespace GIMI_ModManager.WinUI.Helpers; +namespace GIMI_ModManager.WinUI.Helpers.Xaml; public class StringToUri : IValueConverter { diff --git a/src/GIMI-ModManager.WinUI/Helpers/ValueToBoolConverter.cs b/src/GIMI-ModManager.WinUI/Helpers/Xaml/ValueToBoolConverter.cs similarity index 88% rename from src/GIMI-ModManager.WinUI/Helpers/ValueToBoolConverter.cs rename to src/GIMI-ModManager.WinUI/Helpers/Xaml/ValueToBoolConverter.cs index debe00bf..98be461f 100644 --- a/src/GIMI-ModManager.WinUI/Helpers/ValueToBoolConverter.cs +++ b/src/GIMI-ModManager.WinUI/Helpers/Xaml/ValueToBoolConverter.cs @@ -1,6 +1,6 @@ using Microsoft.UI.Xaml.Data; -namespace GIMI_ModManager.WinUI.Helpers; +namespace GIMI_ModManager.WinUI.Helpers.Xaml; public class ValueToBoolConverter : IValueConverter { diff --git a/src/GIMI-ModManager.WinUI/Models/CharacterGridItemModel.cs b/src/GIMI-ModManager.WinUI/Models/CharacterGridItemModel.cs index c55e6d45..c2e3c134 100644 --- a/src/GIMI-ModManager.WinUI/Models/CharacterGridItemModel.cs +++ b/src/GIMI-ModManager.WinUI/Models/CharacterGridItemModel.cs @@ -1,6 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using GIMI_ModManager.Core.Entities.Genshin; -using GIMI_ModManager.WinUI.Models.Options; +using GIMI_ModManager.WinUI.Services.Notifications; namespace GIMI_ModManager.WinUI.Models; diff --git a/src/GIMI-ModManager.WinUI/Models/NewModModel.cs b/src/GIMI-ModManager.WinUI/Models/ModModel.cs similarity index 64% rename from src/GIMI-ModManager.WinUI/Models/NewModModel.cs rename to src/GIMI-ModManager.WinUI/Models/ModModel.cs index 19ccc403..b5b7d559 100644 --- a/src/GIMI-ModManager.WinUI/Models/NewModModel.cs +++ b/src/GIMI-ModManager.WinUI/Models/ModModel.cs @@ -1,14 +1,17 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using GIMI_ModManager.Core.Contracts.Entities; using GIMI_ModManager.Core.Entities; using GIMI_ModManager.Core.Entities.Genshin; -using GIMI_ModManager.WinUI.Models.Options; +using GIMI_ModManager.Core.Entities.Mods.Contract; +using GIMI_ModManager.Core.Helpers; using GIMI_ModManager.WinUI.Services; +using GIMI_ModManager.WinUI.Services.Notifications; namespace GIMI_ModManager.WinUI.Models; -public partial class NewModModel : ObservableObject, IEquatable +public partial class ModModel : ObservableObject, IEquatable { public Guid Id { get; private init; } public GenshinCharacter Character { get; private init; } @@ -32,83 +35,71 @@ public partial class NewModModel : ObservableObject, IEquatable public ObservableCollection SkinModKeySwaps { get; set; } = new(); - private Action? _toggleMod = null; + private Action? _toggleMod = null; /// /// Needed for certain ui components. /// - public NewModModel() + public ModModel() { Id = Guid.Empty; Character = null!; } - public static NewModModel FromMod(CharacterSkinEntry modEntry) + public static ModModel FromMod(CharacterSkinEntry modEntry) { - var name = modEntry.Mod.Name; + return FromMod(modEntry.Mod, modEntry.ModList.Character, modEntry.IsEnabled); + } + + public static ModModel FromMod(ISkinMod skinMod, GenshinCharacter character, bool isEnabled) + { + var name = skinMod.Name; if (!string.IsNullOrWhiteSpace(name)) - name = modEntry.Mod.Name.Replace( - name.StartsWith(CharacterModList.DISABLED_PREFIX) ? "DISABLED_" : "DISABLED", ""); + name = ModFolderHelpers.GetFolderNameWithoutDisabledPrefix(name); - var modModel = new NewModModel + var modModel = new ModModel { - Id = modEntry.Id, - Character = modEntry.ModList.Character, - Name = string.IsNullOrWhiteSpace(name) ? modEntry.Mod.CustomName : name, - FolderName = modEntry.Mod.Name, - IsEnabled = modEntry.IsEnabled + Id = skinMod.Id, + Character = character, + Name = name, + FolderName = skinMod.Name, + IsEnabled = isEnabled }; - if (modEntry.Mod.CachedSkinModSettings is { } settings) + if (skinMod.Settings.GetSettings().TryPickT0(out var settings, out _)) modModel.WithModSettings(settings); - - if (modEntry.Mod.CachedKeySwaps is { } keySwaps) + if (skinMod.KeySwaps is not null && skinMod.KeySwaps.GetKeySwaps().TryPickT0(out var keySwaps, out _)) modModel.SetKeySwaps(keySwaps); + return modModel; } - public NewModModel WithModSettings(SkinModSettings settings) + public ModModel WithModSettings(ModSettings settings) { if (!string.IsNullOrWhiteSpace(settings.CustomName)) Name = settings.CustomName; - ModUrl = settings.ModUrl ?? string.Empty; + ModUrl = settings.ModUrl?.ToString() ?? string.Empty; ModVersion = settings.Version ?? string.Empty; - ImagePath = string.IsNullOrWhiteSpace(settings.ImagePath) - ? PlaceholderImagePath - : new Uri(settings.ImagePath, UriKind.Absolute); + ImagePath = settings.ImagePath ?? PlaceholderImagePath; Author = settings.Author ?? string.Empty; CharacterSkinOverride = settings.CharacterSkinOverride ?? string.Empty; return this; } - public SkinModSettings ToModSettings() - { - return new SkinModSettings - { - CustomName = Name, - Author = Author, - Version = ModVersion, - ModUrl = ModUrl, - ImagePath = ImagePath.Equals(PlaceholderImagePath) ? string.Empty : ImagePath.ToString(), - CharacterSkinOverride = string.IsNullOrWhiteSpace(CharacterSkinOverride) - ? null - : CharacterSkinOverride - }; - } - public void SetKeySwaps(IEnumerable keySwaps) + public void SetKeySwaps(IEnumerable keySwaps) { SkinModKeySwaps.Clear(); foreach (var keySwapModel in keySwaps) SkinModKeySwaps.Add(SkinModKeySwapModel.FromKeySwapSettings(keySwapModel)); } - public NewModModel WithToggleModDelegate(Action toggleMod) + public ModModel WithToggleModDelegate(Action toggleMod) { _toggleMod = toggleMod; return this; @@ -116,7 +107,7 @@ public NewModModel WithToggleModDelegate(Action toggleMod) public override string ToString() { - return "NewModModel: " + Name + " (" + Id + ")"; + return "ModModel: " + Name + " (" + Id + ")"; } @@ -131,16 +122,16 @@ private Task ToggleModAsync() } catch (Exception e) { - App.GetService().ShowNotification($"An error occured " + + App.GetService().ShowNotification($"An error occurred " + (IsEnabled ? "disabling" : "enabling") + $" the mod: {Name}", - e.ToString(), TimeSpan.MaxValue); + e.ToString(), null); } return Task.CompletedTask; } - public bool Equals(NewModModel? other) + public bool Equals(ModModel? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; @@ -152,7 +143,7 @@ public override bool Equals(object? obj) if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != GetType()) return false; - return Equals((NewModModel)obj); + return Equals((ModModel)obj); } public override int GetHashCode() @@ -161,7 +152,7 @@ public override int GetHashCode() } - public bool SettingsEquals(NewModModel? other) + public bool SettingsEquals(ModModel? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; diff --git a/src/GIMI-ModManager.WinUI/Models/LocalSettingsOptions.cs b/src/GIMI-ModManager.WinUI/Models/Options/LocalSettingsOptions.cs similarity index 73% rename from src/GIMI-ModManager.WinUI/Models/LocalSettingsOptions.cs rename to src/GIMI-ModManager.WinUI/Models/Options/LocalSettingsOptions.cs index 1a7459f6..248edd44 100644 --- a/src/GIMI-ModManager.WinUI/Models/LocalSettingsOptions.cs +++ b/src/GIMI-ModManager.WinUI/Models/Options/LocalSettingsOptions.cs @@ -1,4 +1,4 @@ -namespace GIMI_ModManager.WinUI.Models; +namespace GIMI_ModManager.WinUI.Models.Options; public class LocalSettingsOptions { diff --git a/src/GIMI-ModManager.WinUI/Models/Options/ModAttentionSettings.cs b/src/GIMI-ModManager.WinUI/Models/Options/ModAttentionSettings.cs deleted file mode 100644 index 534754fb..00000000 --- a/src/GIMI-ModManager.WinUI/Models/Options/ModAttentionSettings.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Newtonsoft.Json; - -namespace GIMI_ModManager.WinUI.Models.Options; - -public class ModAttentionSettings -{ - [JsonIgnore] public const string Key = "ModAttentionSettings"; - - public Dictionary ModNotifications { get; set; } = new (); -} - -public sealed class ModNotification -{ - public Guid Id { get; init; } = Guid.NewGuid(); - public int CharacterId { get; init; } - public string ModCustomName { get; init; } = string.Empty; - public string ModFolderName { get; init; } = string.Empty; - public bool ShowOnOverview { get; init; } - public AttentionType AttentionType { get; init; } - public string Message { get; init; } = string.Empty; -} - -public enum AttentionType -{ - None, - Added, - Modified, - UpdateAvailable, // Also show in character overview - Error // Also show in character overview -} \ No newline at end of file diff --git a/src/GIMI-ModManager.WinUI/Models/ModManagerOptions.cs b/src/GIMI-ModManager.WinUI/Models/Options/ModManagerOptions.cs similarity index 85% rename from src/GIMI-ModManager.WinUI/Models/ModManagerOptions.cs rename to src/GIMI-ModManager.WinUI/Models/Options/ModManagerOptions.cs index 9d663d3a..270a711c 100644 --- a/src/GIMI-ModManager.WinUI/Models/ModManagerOptions.cs +++ b/src/GIMI-ModManager.WinUI/Models/Options/ModManagerOptions.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace GIMI_ModManager.WinUI.Models; +namespace GIMI_ModManager.WinUI.Models.Options; public class ModManagerOptions { diff --git a/src/GIMI-ModManager.WinUI/Models/Options/AppSettings.cs b/src/GIMI-ModManager.WinUI/Models/Settings/AppSettings.cs similarity index 100% rename from src/GIMI-ModManager.WinUI/Models/Options/AppSettings.cs rename to src/GIMI-ModManager.WinUI/Models/Settings/AppSettings.cs diff --git a/src/GIMI-ModManager.WinUI/Models/Options/CharacterOverviewOptions.cs b/src/GIMI-ModManager.WinUI/Models/Settings/CharacterOverviewSettings.cs similarity index 86% rename from src/GIMI-ModManager.WinUI/Models/Options/CharacterOverviewOptions.cs rename to src/GIMI-ModManager.WinUI/Models/Settings/CharacterOverviewSettings.cs index 9a97866a..1c59bcc7 100644 --- a/src/GIMI-ModManager.WinUI/Models/Options/CharacterOverviewOptions.cs +++ b/src/GIMI-ModManager.WinUI/Models/Settings/CharacterOverviewSettings.cs @@ -1,9 +1,9 @@ using GIMI_ModManager.WinUI.ViewModels; using Newtonsoft.Json; -namespace GIMI_ModManager.WinUI.Models.Options; +namespace GIMI_ModManager.WinUI.Models.Settings; -public class CharacterOverviewOptions +public class CharacterOverviewSettings { [JsonIgnore] public const string Key = "CharacterOverviewOptions"; diff --git a/src/GIMI-ModManager.WinUI/Models/Settings/ModAttentionSettings.cs b/src/GIMI-ModManager.WinUI/Models/Settings/ModAttentionSettings.cs new file mode 100644 index 00000000..eaeb90b8 --- /dev/null +++ b/src/GIMI-ModManager.WinUI/Models/Settings/ModAttentionSettings.cs @@ -0,0 +1,11 @@ +using GIMI_ModManager.WinUI.Services.Notifications; +using Newtonsoft.Json; + +namespace GIMI_ModManager.WinUI.Models.Settings; + +public class ModAttentionSettings +{ + [JsonIgnore] public const string Key = "ModAttentionSettings"; + + public Dictionary ModNotifications { get; set; } = new(); +} \ No newline at end of file diff --git a/src/GIMI-ModManager.WinUI/Models/Options/ScreenSize.cs b/src/GIMI-ModManager.WinUI/Models/Settings/ScreenSizeSettings.cs similarity index 66% rename from src/GIMI-ModManager.WinUI/Models/Options/ScreenSize.cs rename to src/GIMI-ModManager.WinUI/Models/Settings/ScreenSizeSettings.cs index d51dd551..87f1d51e 100644 --- a/src/GIMI-ModManager.WinUI/Models/Options/ScreenSize.cs +++ b/src/GIMI-ModManager.WinUI/Models/Settings/ScreenSizeSettings.cs @@ -1,15 +1,15 @@ using Windows.Foundation; using Newtonsoft.Json; -namespace GIMI_ModManager.WinUI.Models.Options; +namespace GIMI_ModManager.WinUI.Models.Settings; -public class ScreenSize +public class ScreenSizeSettings { - public ScreenSize() + public ScreenSizeSettings() { } - public ScreenSize(double width, double height) + public ScreenSizeSettings(double width, double height) { Width = Convert.ToInt32(width); Height = Convert.ToInt32(height); @@ -22,5 +22,5 @@ public ScreenSize(double width, double height) public bool IsFullScreen { get; set; } [JsonIgnore] public double WidthAsDouble => Convert.ToDouble(Width); [JsonIgnore] public double HeightAsDouble => Convert.ToDouble(Height); - [JsonIgnore] public Size Size => new Size(WidthAsDouble, HeightAsDouble); + [JsonIgnore] public Size Size => new(WidthAsDouble, HeightAsDouble); } \ No newline at end of file diff --git a/src/GIMI-ModManager.WinUI/Models/SkinModKeySwapModel.cs b/src/GIMI-ModManager.WinUI/Models/SkinModKeySwapModel.cs index 07af005c..f249ea30 100644 --- a/src/GIMI-ModManager.WinUI/Models/SkinModKeySwapModel.cs +++ b/src/GIMI-ModManager.WinUI/Models/SkinModKeySwapModel.cs @@ -1,5 +1,5 @@ using CommunityToolkit.Mvvm.ComponentModel; -using GIMI_ModManager.Core.Entities; +using GIMI_ModManager.Core.Entities.Mods.Contract; namespace GIMI_ModManager.WinUI.Models; @@ -11,43 +11,30 @@ public partial class SkinModKeySwapModel : ObservableObject, IEquatable skinSwapSettings.Select(FromKeySwapSettings).ToArray(); - public SkinModKeySwap ToKeySwapSettings() - { - return new() - { - SectionKey = SectionKey, - ForwardHotkey = ForwardHotkey, - BackwardHotkey = BackwardHotkey, - Type = Type, - SwapVar = SwapVar - }; - } public bool Equals(SkinModKeySwapModel? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return Condition == other.Condition && ForwardHotkey == other.ForwardHotkey && - BackwardHotkey == other.BackwardHotkey && Type == other.Type && Equals(SwapVar, other.SwapVar); + BackwardHotkey == other.BackwardHotkey && Type == other.Type; } public override bool Equals(object? obj) @@ -57,6 +44,6 @@ public override bool Equals(object? obj) public override int GetHashCode() { - return HashCode.Combine(Condition, ForwardHotkey, BackwardHotkey, Type, SwapVar); + return HashCode.Combine(Condition, ForwardHotkey, BackwardHotkey, Type); } } \ No newline at end of file diff --git a/src/GIMI-ModManager.WinUI/Models/SkinModSettingsModel.cs b/src/GIMI-ModManager.WinUI/Models/SkinModSettingsModel.cs deleted file mode 100644 index a457dcb8..00000000 --- a/src/GIMI-ModManager.WinUI/Models/SkinModSettingsModel.cs +++ /dev/null @@ -1,47 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; -using GIMI_ModManager.Core.Contracts.Entities; -using GIMI_ModManager.Core.Entities; - -namespace GIMI_ModManager.WinUI.Models; - -public partial class SkinModSettingsModel : ObservableObject -{ - [ObservableProperty] private string? _customName; - - [ObservableProperty] private string? _author; - [ObservableProperty] private string? _version; - [ObservableProperty] private string? _modUrl; - [ObservableProperty] private string? _imageUri = " "; // If this is null or empty the app will crash... - - public static SkinModSettingsModel FromMod(SkinModSettings mod) - { - return new SkinModSettingsModel - { - CustomName = mod.CustomName, - Author = mod.Author, - Version = mod.Version, - ModUrl = mod.ModUrl, - ImageUri = string.IsNullOrEmpty(mod.ImagePath) ? " " : mod.ImagePath, - }; - } - - - protected bool Equals(SkinModSettingsModel other) - { - return CustomName == other.CustomName && Author == other.Author && Version == other.Version && - ModUrl == other.ModUrl && ImageUri == other.ImageUri; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((SkinModSettingsModel)obj); - } - - public override int GetHashCode() - { - return HashCode.Combine(CustomName, Author, Version, ModUrl, ImageUri); - } -} \ No newline at end of file diff --git a/src/GIMI-ModManager.WinUI/Services/ActivationService.cs b/src/GIMI-ModManager.WinUI/Services/ActivationService.cs index 7c98a1cc..e2a1aa4d 100644 --- a/src/GIMI-ModManager.WinUI/Services/ActivationService.cs +++ b/src/GIMI-ModManager.WinUI/Services/ActivationService.cs @@ -6,6 +6,9 @@ using GIMI_ModManager.WinUI.Contracts.Services; using GIMI_ModManager.WinUI.Helpers; using GIMI_ModManager.WinUI.Models.Options; +using GIMI_ModManager.WinUI.Models.Settings; +using GIMI_ModManager.WinUI.Services.AppManagment; +using GIMI_ModManager.WinUI.Services.AppManagment.Updating; using GIMI_ModManager.WinUI.Views; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -76,7 +79,7 @@ public async Task ActivateAsync(object activationArgs) App.MainWindow.Activate(); // Set MainWindow Cleanup on Close. - App.MainWindow.Closed += OnApplicationExit; + App.MainWindow.Closed += OnApplicationExit; // Execute tasks after activation. await StartupAsync(); @@ -128,7 +131,7 @@ private async Task StartupAsync() private async Task SetScreenSize() { - var screenSize = await _localSettingsService.ReadSettingAsync(ScreenSize.Key); + var screenSize = await _localSettingsService.ReadSettingAsync(ScreenSizeSettings.Key); if (screenSize != null) { _logger.Debug($"Window size loaded: {screenSize.Width}x{screenSize.Height}"); @@ -140,9 +143,10 @@ private async Task SetScreenSize() private async Task InitCharacterOverviewSettings() { var characterOverviewSettings = - await _localSettingsService.ReadSettingAsync(CharacterOverviewOptions.Key); + await _localSettingsService.ReadSettingAsync(CharacterOverviewSettings.Key); if (characterOverviewSettings == null) - await _localSettingsService.SaveSettingAsync(CharacterOverviewOptions.Key, new CharacterOverviewOptions()); + await _localSettingsService.SaveSettingAsync(CharacterOverviewSettings.Key, + new CharacterOverviewSettings()); } private Size _previousScreenSize = new(0, 0); @@ -177,7 +181,8 @@ private void ScreenSizeSavingTimer_Tick(object sender, object e) var isFullScreen = false; // TODO: Implement fullscreen _logger.Debug($"Window size saved: {width}x{height}\t\nIsFullscreen: {isFullScreen}"); Task.Run(async () => await App.GetService() - .SaveSettingAsync(ScreenSize.Key, new ScreenSize(width, height) { IsFullScreen = isFullScreen })); + .SaveSettingAsync(ScreenSizeSettings.Key, + new ScreenSizeSettings(width, height) { IsFullScreen = isFullScreen })); _timer?.Stop(); } diff --git a/src/GIMI-ModManager.WinUI/Services/AutoUpdaterService.cs b/src/GIMI-ModManager.WinUI/Services/AppManagment/Updating/AutoUpdaterService.cs similarity index 87% rename from src/GIMI-ModManager.WinUI/Services/AutoUpdaterService.cs rename to src/GIMI-ModManager.WinUI/Services/AppManagment/Updating/AutoUpdaterService.cs index 4cec2384..0ff34962 100644 --- a/src/GIMI-ModManager.WinUI/Services/AutoUpdaterService.cs +++ b/src/GIMI-ModManager.WinUI/Services/AppManagment/Updating/AutoUpdaterService.cs @@ -3,7 +3,7 @@ using Microsoft.UI.Xaml; using Serilog; -namespace GIMI_ModManager.WinUI.Services; +namespace GIMI_ModManager.WinUI.Services.AppManagment.Updating; public class AutoUpdaterService { @@ -87,7 +87,7 @@ public void UpdateAutoUpdater() if (HasStartedSelfUpdateProcess) { _logger.Warning("Self update process already started."); - return new[] { Error.Conflict(description:"Self update process already started.") }; + return new[] { Error.Conflict(description: "Self update process already started.") }; } HasStartedSelfUpdateProcess = true; @@ -109,7 +109,12 @@ public void UpdateAutoUpdater() { _logger.Error("Current auto updater folder does not exist. Could not find the update folder: {Folder}", _currentAutoUpdaterFolder.FullName); - return new[] { Error.NotFound(description: $"Current auto updater folder does not exist. Could not find the update folder: {_currentAutoUpdaterFolder.FullName}") }; + return new[] + { + Error.NotFound( + description: + $"Current auto updater folder does not exist. Could not find the update folder: {_currentAutoUpdaterFolder.FullName}") + }; } if (!ContainsAutoUpdaterExe(_currentAutoUpdaterFolder)) @@ -118,7 +123,12 @@ public void UpdateAutoUpdater() "Current auto updater folder does not contain the auto updater exe. Could not find {Exe} in {Folder}", AutoUpdaterExe, _currentAutoUpdaterFolder.FullName); - return new[] { Error.NotFound(description: $"Current auto updater folder does not contain the auto updater exe. Could not find {AutoUpdaterExe} in {_currentAutoUpdaterFolder.FullName}") }; + return new[] + { + Error.NotFound( + description: + $"Current auto updater folder does not contain the auto updater exe. Could not find {AutoUpdaterExe} in {_currentAutoUpdaterFolder.FullName}") + }; } var isAutoUpdaterRunning = Process.GetProcessesByName(AutoUpdaterExe).Any(); @@ -143,7 +153,7 @@ public void UpdateAutoUpdater() if (process is null || process.HasExited) { _logger.Error("Failed to start Auto Updater."); - return new[] { Error.Unexpected(description:"Failed to start Auto Updater.") }; + return new[] { Error.Unexpected(description: "Failed to start Auto Updater.") }; } } catch (Exception e) diff --git a/src/GIMI-ModManager.WinUI/Services/UpdateChecker.cs b/src/GIMI-ModManager.WinUI/Services/AppManagment/Updating/UpdateChecker.cs similarity index 97% rename from src/GIMI-ModManager.WinUI/Services/UpdateChecker.cs rename to src/GIMI-ModManager.WinUI/Services/AppManagment/Updating/UpdateChecker.cs index fa149ec9..65bade79 100644 --- a/src/GIMI-ModManager.WinUI/Services/UpdateChecker.cs +++ b/src/GIMI-ModManager.WinUI/Services/AppManagment/Updating/UpdateChecker.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; using Serilog; -namespace GIMI_ModManager.WinUI.Services; +namespace GIMI_ModManager.WinUI.Services.AppManagment.Updating; public sealed class UpdateChecker : IDisposable { @@ -143,7 +143,7 @@ private async Task CheckForUpdatesAsync(CancellationToken cancellationToken) var text = await result.Content.ReadAsStringAsync(cancellationToken); var gitHubReleases = - (JsonConvert.DeserializeObject(text)) ?? Array.Empty(); + JsonConvert.DeserializeObject(text) ?? Array.Empty(); var latestReleases = gitHubReleases.Where(r => !r.prerelease); var latestVersion = latestReleases.Select(r => new Version(r.tag_name?.Trim('v') ?? "")).Max(); diff --git a/src/GIMI-ModManager.WinUI/Services/WindowManagerService.cs b/src/GIMI-ModManager.WinUI/Services/AppManagment/WindowManagerService.cs similarity index 94% rename from src/GIMI-ModManager.WinUI/Services/WindowManagerService.cs rename to src/GIMI-ModManager.WinUI/Services/AppManagment/WindowManagerService.cs index 6aa883b0..9a2999eb 100644 --- a/src/GIMI-ModManager.WinUI/Services/WindowManagerService.cs +++ b/src/GIMI-ModManager.WinUI/Services/AppManagment/WindowManagerService.cs @@ -1,11 +1,11 @@ -using GIMI_ModManager.WinUI.Models.Options; +using GIMI_ModManager.WinUI.Models.Settings; using Microsoft.Extensions.Logging; using Microsoft.Graphics.Display; using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -namespace GIMI_ModManager.WinUI.Services; +namespace GIMI_ModManager.WinUI.Services.AppManagment; public class WindowManagerService : IWindowManagerService { @@ -50,7 +50,7 @@ public void ResizeWindow(WindowEx window, int width, int height) windowToResize.Height = height; } - public void ResizeWindow(WindowEx window, ScreenSize newSize) + public void ResizeWindow(WindowEx window, ScreenSizeSettings newSize) { ResizeWindow(window, newSize.Width, newSize.Height); } @@ -134,10 +134,9 @@ public interface IWindowManagerService public WindowEx MainWindow { get; } void ShowWindow(WindowEx window); void ResizeWindow(WindowEx window, int width, int height); - void ResizeWindow(WindowEx window, ScreenSize newSize); + void ResizeWindow(WindowEx window, ScreenSizeSettings newSize); void ResizeWindowPercent(WindowEx window, int widthPercent, int heightPercent); void CloseWindow(WindowEx window); WindowEx CreateWindow(UIElement windowContent, bool activate = true); - Task ShowDialogAsync(ContentDialog dialog, WindowEx? window = null); } \ No newline at end of file diff --git a/src/GIMI-ModManager.WinUI/Services/LocalSettingsService.cs b/src/GIMI-ModManager.WinUI/Services/LocalSettingsService.cs index b9822654..d4e3d24c 100644 --- a/src/GIMI-ModManager.WinUI/Services/LocalSettingsService.cs +++ b/src/GIMI-ModManager.WinUI/Services/LocalSettingsService.cs @@ -3,9 +3,9 @@ using GIMI_ModManager.Core.Helpers; using GIMI_ModManager.WinUI.Contracts.Services; using GIMI_ModManager.WinUI.Helpers; -using GIMI_ModManager.WinUI.Models; using Microsoft.Extensions.Options; using Newtonsoft.Json; +using GIMI_ModManager.WinUI.Models.Options; namespace GIMI_ModManager.WinUI.Services; diff --git a/src/GIMI-ModManager.WinUI/Services/ModDragAndDropService.cs b/src/GIMI-ModManager.WinUI/Services/ModDragAndDropService.cs index 199cdf89..6f763468 100644 --- a/src/GIMI-ModManager.WinUI/Services/ModDragAndDropService.cs +++ b/src/GIMI-ModManager.WinUI/Services/ModDragAndDropService.cs @@ -1,36 +1,40 @@ using Windows.Storage; using GIMI_ModManager.Core.Contracts.Entities; -using GIMI_ModManager.Core.Entities; +using GIMI_ModManager.Core.Contracts.Services; using GIMI_ModManager.Core.Helpers; using GIMI_ModManager.Core.Services; using Serilog; +using static GIMI_ModManager.WinUI.Services.ModDragAndDropService.DragAndDropFinishedArgs; namespace GIMI_ModManager.WinUI.Services; public class ModDragAndDropService { private readonly ILogger _logger; + private readonly ISkinManagerService _skinManagerService; private readonly NotificationManager _notificationManager; public event EventHandler? DragAndDropFinished; - public ModDragAndDropService(ILogger logger, NotificationManager notificationManager) + public ModDragAndDropService(ILogger logger, NotificationManager notificationManager, + ISkinManagerService skinManagerService) { _notificationManager = notificationManager; + _skinManagerService = skinManagerService; _logger = logger.ForContext(); } // Drag and drop directly from 7zip is REALLY STRANGE, I don't know why 7zip 'usually' deletes the files before we can copy them // Sometimes only a few folders are copied, sometimes only a single file is copied, but usually 7zip removes them and the app just crashes // This code is a mess, but it works. - public async Task> AddStorageItemFoldersAsync( + public async Task> AddStorageItemFoldersAsync( ICharacterModList modList, IReadOnlyList? storageItems) { if (storageItems is null || !storageItems.Any()) { _logger.Warning("Drag and drop files called with null/0 storage items."); - return Array.Empty(); + return Array.Empty(); } @@ -39,10 +43,11 @@ public ModDragAndDropService(ILogger logger, NotificationManager notificationMan _notificationManager.ShowNotification( "Drag and drop called with more than one storage item, this is currently not supported", "", TimeSpan.FromSeconds(5)); - return Array.Empty(); + return Array.Empty(); } - var extractResults = new List(); + var extractResults = new List(); + using var disableWatcher = modList.DisableWatcher(); // Warning mess below foreach (var storageItem in storageItems) @@ -76,7 +81,7 @@ public ModDragAndDropService(ILogger logger, NotificationManager notificationMan $"Ignored Folders: {string.Join(" | ", extractResult.IgnoredMods)}", TimeSpan.FromSeconds(7))); - extractResults.Add(new DragAndDropFinishedArgs.ExtractPaths(storageItem.Path, + extractResults.Add(new ExtractPaths(storageItem.Path, extractResult.ExtractedMod.FullPath)); continue; } @@ -133,13 +138,13 @@ public ModDragAndDropService(ILogger logger, NotificationManager notificationMan recursiveCopy.Invoke(sourceFolder, await StorageFolder.GetFolderFromPathAsync(destFolderPath)); } - catch (Exception e) + catch (Exception) { Directory.Delete(destFolderPath); throw; } - extractResults.Add(new DragAndDropFinishedArgs.ExtractPaths(storageItem.Path, destFolderPath)); + extractResults.Add(new ExtractPaths(storageItem.Path, destFolderPath)); } DragAndDropFinished?.Invoke(this, new DragAndDropFinishedArgs(extractResults)); @@ -147,13 +152,21 @@ public ModDragAndDropService(ILogger logger, NotificationManager notificationMan } // ReSharper disable once InconsistentNaming - private static void RecursiveCopy7z(StorageFolder sourceFolder, StorageFolder destinationFolder) + private void RecursiveCopy7z(StorageFolder sourceFolder, StorageFolder destinationFolder) { var tmpFolder = Path.GetTempPath(); var parentDir = new DirectoryInfo(Path.GetDirectoryName(sourceFolder.Path)!); parentDir.MoveTo(Path.Combine(tmpFolder, "JASM_TMP", Guid.NewGuid().ToString("N"))); - var mod = new Mod(parentDir.GetDirectories().First()!); - mod.MoveTo(destinationFolder.Path); + + var modDir = parentDir.EnumerateDirectories().FirstOrDefault(); + + if (modDir is null) + { + throw new DirectoryNotFoundException("No valid mod folder found in archive. Loose files are ignored"); + } + + RecursiveCopy(StorageFolder.GetFolderFromPathAsync(modDir.FullName).GetAwaiter().GetResult(), + destinationFolder); } private void RecursiveCopy(StorageFolder sourceFolder, StorageFolder destinationFolder) diff --git a/src/GIMI-ModManager.WinUI/Services/ModHandling/KeySwapService.cs b/src/GIMI-ModManager.WinUI/Services/ModHandling/KeySwapService.cs new file mode 100644 index 00000000..2174311f --- /dev/null +++ b/src/GIMI-ModManager.WinUI/Services/ModHandling/KeySwapService.cs @@ -0,0 +1,102 @@ +using GIMI_ModManager.Core.Contracts.Services; +using GIMI_ModManager.Core.Entities.Mods.Contract; +using GIMI_ModManager.WinUI.Models; +using OneOf; +using OneOf.Types; +using Serilog; + +namespace GIMI_ModManager.WinUI.Services.ModHandling; + +public class KeySwapService +{ + private readonly ISkinManagerService _skinManagerService; + private readonly ILogger _logger; + private readonly NotificationManager _notificationManager; + + public KeySwapService(ISkinManagerService skinManagerService, ILogger logger, + NotificationManager notificationManager) + { + _skinManagerService = skinManagerService; + _notificationManager = notificationManager; + _logger = logger.ForContext(); + } + + public async Task>> SaveKeySwapsAsync(ModModel modModel) + { + var skinMod = _skinManagerService.GetModById(modModel.Id); + + if (skinMod is null) + return new ModNotFound(modModel.Id); + + + if (skinMod.KeySwaps is null) + return new NotFound(); + + var keySwapSections = new List(modModel.SkinModKeySwaps.Count); + + foreach (var modModelSkinModKeySwap in modModel.SkinModKeySwaps) + { + var variants = int.TryParse(modModelSkinModKeySwap.VariationsCount, out var variantsCount) + ? variantsCount + : -1; + + var keySwapSection = new KeySwapSection() + { + SectionName = modModelSkinModKeySwap.SectionKey, + ForwardKey = modModelSkinModKeySwap.ForwardHotkey, + BackwardKey = modModelSkinModKeySwap.BackwardHotkey, + Variants = variants == -1 ? null : variants, + Type = modModelSkinModKeySwap.Type ?? "Unknown" + }; + + keySwapSections.Add(keySwapSection); + } + + + try + { + await Task.Run(() => skinMod.KeySwaps.SaveKeySwapConfiguration(keySwapSections)); + return new Success(); + } + catch (Exception e) + { + _logger.Error(e, "Failed to save key swap configuration for mod {ModName}", skinMod.Name); + _notificationManager.ShowNotification($"Failed to save key swap configuration for mod {skinMod.Name}", + $"An error occurred when saving. Reason: {e.Message}", null); + return new Error(e); + } + } + + public async Task>> GetKeySwapsAsync(Guid modId) + { + var skinMod = _skinManagerService.GetModById(modId); + + if (skinMod is null) + return new ModNotFound(modId); + + if (skinMod.KeySwaps is null) + { + _logger.Debug("Key swap manager for mod {ModName} is null", skinMod.Name); + return new NotFound(); + } + + + var getResult = skinMod.KeySwaps.GetKeySwaps(); + + if (getResult.IsT0) + return getResult.AsT0; + + try + { + await Task.Run(() => skinMod.KeySwaps.ReadKeySwapConfiguration()); + getResult = skinMod.KeySwaps.GetKeySwaps(); + + return getResult.AsT0; + } + catch (Exception e) + { + _logger.Error(e, "Failed to read key swap configuration for mod {ModName}", skinMod.Name); + return new Error(e); + } + } +} \ No newline at end of file diff --git a/src/GIMI-ModManager.WinUI/Services/ModHandling/ModSettingsService.cs b/src/GIMI-ModManager.WinUI/Services/ModHandling/ModSettingsService.cs new file mode 100644 index 00000000..662ee66f --- /dev/null +++ b/src/GIMI-ModManager.WinUI/Services/ModHandling/ModSettingsService.cs @@ -0,0 +1,155 @@ +using System.Diagnostics; +using GIMI_ModManager.Core.Contracts.Services; +using GIMI_ModManager.Core.Entities.Mods.Contract; +using GIMI_ModManager.Core.Entities.Mods.SkinMod; +using GIMI_ModManager.WinUI.Models; +using OneOf; +using OneOf.Types; +using Serilog; +using Success = ErrorOr.Success; + +namespace GIMI_ModManager.WinUI.Services.ModHandling; + +public class ModSettingsService +{ + private readonly ISkinManagerService _skinManagerService; + private readonly ILogger _logger; + private readonly NotificationManager _notificationManager; + + public ModSettingsService(ISkinManagerService skinManagerService, NotificationManager notificationManager, + ILogger logger) + { + _skinManagerService = skinManagerService; + _notificationManager = notificationManager; + _logger = logger.ForContext(); + } + + + public async Task>> SaveSettingsAsync(ModModel modModel) + { + var mod = _skinManagerService.GetModById(modModel.Id); + + if (mod is null) + return new ModNotFound(modModel.Id); + + + var modUrl = Uri.TryCreate(modModel.ModUrl, UriKind.Absolute, out var uriResult) && + (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps) + ? uriResult + : null; + + var modSettings = new ModSettings( + id: modModel.Id, + customName: EmptyStringToNull(modModel.Name), + author: EmptyStringToNull(modModel.Author), + version: EmptyStringToNull(modModel.ModVersion), + modUrl: modUrl, + imagePath: modModel.ImagePath == ModModel.PlaceholderImagePath ? null : modModel.ImagePath, + characterSkinOverride: EmptyStringToNull(modModel.CharacterSkinOverride) + ); + + + try + { + await Task.Run(() => mod.Settings.SaveSettingsAsync(modSettings)); + return new Success(); + } + catch (Exception e) + { + _logger.Error(e, "Failed to save settings for mod {modName}", mod.Name); + + _notificationManager.ShowNotification($"Failed to save settings for mod {mod.Name}", + $"An Error Occurred. Reason: {e.Message}", + TimeSpan.FromSeconds(5)); + + return new Error(e); + } + } + + public async Task>> SetCharacterSkinOverride(Guid modId, + string skinName) + { + var mod = _skinManagerService.GetModById(modId); + + if (mod is null) + return new ModNotFound(modId); + + var modSettings = await GetSettingsAsync(modId); + + if (modSettings.TryPickT0(out var settings, out var errorResults)) + { + var newSettings = settings.DeepCopyWithProperties(characterSkinOverride: skinName); + try + { + await mod.Settings.SaveSettingsAsync(newSettings); + return new Success(); + } + catch (Exception e) + { + _logger.Error(e, "Failed to save settings for mod {modName}", mod.Name); + + _notificationManager.ShowNotification($"Failed to save settings for mod {mod.Name}", + $"An Error Occurred. Reason: {e.Message}", + TimeSpan.FromSeconds(5)); + + return new Error(e); + } + } + + if (errorResults.IsT0) + return errorResults.AsT0; + + if (errorResults.IsT1) + return errorResults.AsT1; + + return errorResults.AsT2; + } + + public async Task>> GetSettingsAsync(Guid modId, + bool forceReload = false) + { + var mod = _skinManagerService.GetModById(modId); + + if (mod is null) + { + Debugger.Break(); + _logger.Debug("Could not find mod with id {ModId}", modId); + return new ModNotFound(modId); + } + + try + { + return await mod.Settings.ReadSettingsAsync(); + } + catch (ModSettingsNotFoundException e) + { + _logger.Error(e, "Could not find settings file for mod {ModName}", mod.Name); + _notificationManager.ShowNotification($"Could not find settings file for mod {mod.Name}", "", + TimeSpan.FromSeconds(5)); + return new NotFound(); + } + catch (Exception e) + { + _logger.Error(e, "Failed to read settings for mod {modName}", mod.Name); + + _notificationManager.ShowNotification($"Failed to read settings for mod {mod.Name}", + $"An error occurred. Reason: {e.Message}", + TimeSpan.FromSeconds(5)); + + return new Error(e); + } + } + + + private static string? EmptyStringToNull(string? str) => string.IsNullOrWhiteSpace(str) ? null : str; +} + +public readonly struct ModNotFound +{ + public ModNotFound(Guid modId) + { + ModId = modId; + } + + public Guid ModId { get; } +} \ No newline at end of file diff --git a/src/GIMI-ModManager.WinUI/Services/Notifications/AttentionType.cs b/src/GIMI-ModManager.WinUI/Services/Notifications/AttentionType.cs new file mode 100644 index 00000000..f13a86db --- /dev/null +++ b/src/GIMI-ModManager.WinUI/Services/Notifications/AttentionType.cs @@ -0,0 +1,10 @@ +namespace GIMI_ModManager.WinUI.Services.Notifications; + +public enum AttentionType +{ + None, + Added, + Modified, + UpdateAvailable, // Also show in character overview + Error // Also show in character overview +} \ No newline at end of file diff --git a/src/GIMI-ModManager.WinUI/Services/Notifications/ModNotification.cs b/src/GIMI-ModManager.WinUI/Services/Notifications/ModNotification.cs new file mode 100644 index 00000000..74f0916a --- /dev/null +++ b/src/GIMI-ModManager.WinUI/Services/Notifications/ModNotification.cs @@ -0,0 +1,12 @@ +namespace GIMI_ModManager.WinUI.Services.Notifications; + +public sealed class ModNotification +{ + public Guid Id { get; init; } = Guid.NewGuid(); + public int CharacterId { get; init; } + public string ModCustomName { get; init; } = string.Empty; + public string ModFolderName { get; init; } = string.Empty; + public bool ShowOnOverview { get; init; } + public AttentionType AttentionType { get; init; } + public string Message { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/src/GIMI-ModManager.WinUI/Services/Notifications/ModNotificationManager.cs b/src/GIMI-ModManager.WinUI/Services/Notifications/ModNotificationManager.cs index 632f5479..80c56f8c 100644 --- a/src/GIMI-ModManager.WinUI/Services/Notifications/ModNotificationManager.cs +++ b/src/GIMI-ModManager.WinUI/Services/Notifications/ModNotificationManager.cs @@ -1,6 +1,6 @@ using GIMI_ModManager.Core.Entities.Genshin; using GIMI_ModManager.WinUI.Contracts.Services; -using GIMI_ModManager.WinUI.Models.Options; +using GIMI_ModManager.WinUI.Models.Settings; using Serilog; namespace GIMI_ModManager.WinUI.Services.Notifications; @@ -153,8 +153,6 @@ await _localSettingsService.SaveSettingAsync(ModAttentionSettings.Key, modAttent } - - public class ModNotificationEvent : EventArgs { public ModNotification ModNotification { get; } diff --git a/src/GIMI-ModManager.WinUI/Services/NotImplemented.cs b/src/GIMI-ModManager.WinUI/Services/Notifications/NotImplemented.cs similarity index 89% rename from src/GIMI-ModManager.WinUI/Services/NotImplemented.cs rename to src/GIMI-ModManager.WinUI/Services/Notifications/NotImplemented.cs index 22c46ac2..67ccf69a 100644 --- a/src/GIMI-ModManager.WinUI/Services/NotImplemented.cs +++ b/src/GIMI-ModManager.WinUI/Services/Notifications/NotImplemented.cs @@ -1,4 +1,4 @@ -namespace GIMI_ModManager.WinUI.Services; +namespace GIMI_ModManager.WinUI.Services.Notifications; // This is a static class to easily launch a not implemented notification from different places in the app. internal static class NotImplemented diff --git a/src/GIMI-ModManager.WinUI/Services/NotificationManager.cs b/src/GIMI-ModManager.WinUI/Services/Notifications/NotificationManager.cs similarity index 100% rename from src/GIMI-ModManager.WinUI/Services/NotificationManager.cs rename to src/GIMI-ModManager.WinUI/Services/Notifications/NotificationManager.cs diff --git a/src/GIMI-ModManager.WinUI/Services/ProcessManagerService.cs b/src/GIMI-ModManager.WinUI/Services/ProcessManagerService.cs index c0df68a1..231f7256 100644 --- a/src/GIMI-ModManager.WinUI/Services/ProcessManagerService.cs +++ b/src/GIMI-ModManager.WinUI/Services/ProcessManagerService.cs @@ -20,7 +20,6 @@ public abstract partial class BaseProcessManager : ObservableOb private protected string _prcoessPath = null!; private protected string _workingDirectory = string.Empty; - private bool _exitHandlerRegistered; private protected bool _isGenshinClass; public string ProcessName { get; protected set; } = string.Empty; @@ -169,17 +168,6 @@ public void StartProcess() ProcessStatus = ProcessStatus.NotRunning; _logger.Information("{ProcessName} exited with exit code: {ExitCode}", ProcessName, _process.ExitCode); }; - - if (_exitHandlerRegistered) return; - - - //App.MainWindow.Closed += MainWindowExitHandler; - //_exitHandlerRegistered = true; - } - - private void MainWindowExitHandler(object sender, WindowEventArgs args) - { - StopProcess(); } diff --git a/src/GIMI-ModManager.WinUI/ViewModels/CharacterDetailsViewModel.cs b/src/GIMI-ModManager.WinUI/ViewModels/CharacterDetailsViewModel.cs index f38c479c..9d4aa336 100644 --- a/src/GIMI-ModManager.WinUI/ViewModels/CharacterDetailsViewModel.cs +++ b/src/GIMI-ModManager.WinUI/ViewModels/CharacterDetailsViewModel.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; -using System.Text.Json; using Windows.Storage; using Windows.Storage.Pickers; using Windows.System; @@ -10,6 +9,7 @@ using GIMI_ModManager.Core.Contracts.Services; using GIMI_ModManager.Core.Entities; using GIMI_ModManager.Core.Entities.Genshin; +using GIMI_ModManager.Core.Entities.Mods.Contract; using GIMI_ModManager.Core.Helpers; using GIMI_ModManager.Core.Services; using GIMI_ModManager.WinUI.Contracts.Services; @@ -19,6 +19,7 @@ using GIMI_ModManager.WinUI.Models.Options; using GIMI_ModManager.WinUI.Models.ViewModels; using GIMI_ModManager.WinUI.Services; +using GIMI_ModManager.WinUI.Services.ModHandling; using GIMI_ModManager.WinUI.Services.Notifications; using GIMI_ModManager.WinUI.ViewModels.SubVms; using Serilog; @@ -36,6 +37,7 @@ public partial class CharacterDetailsViewModel : ObservableRecipient, INavigatio private readonly ModDragAndDropService _modDragAndDropService; private readonly ModCrawlerService _modCrawlerService; private readonly ModNotificationManager _modNotificationManager; + private readonly ModSettingsService _modSettingsService; private ICharacterModList _modList = null!; public ModListVM ModListVM { get; } = null!; @@ -60,7 +62,7 @@ public CharacterDetailsViewModel(IGenshinService genshinService, ILogger logger, INavigationService navigationService, ISkinManagerService skinManagerService, NotificationManager notificationService, ILocalSettingsService localSettingsService, ModDragAndDropService modDragAndDropService, ModCrawlerService modCrawlerService, - ModNotificationManager modNotificationManager) + ModNotificationManager modNotificationManager, ModSettingsService modSettingsService) { _genshinService = genshinService; _logger = logger.ForContext(); @@ -71,6 +73,7 @@ public CharacterDetailsViewModel(IGenshinService genshinService, ILogger logger, _modDragAndDropService = modDragAndDropService; _modCrawlerService = modCrawlerService; _modNotificationManager = modNotificationManager; + _modSettingsService = modSettingsService; _modDragAndDropService.DragAndDropFinished += async (sender, args) => { @@ -95,7 +98,7 @@ await App.MainWindow.DispatcherQueue.EnqueueAsync( ModListVM = new ModListVM(skinManagerService, modNotificationManager); ModListVM.OnModsSelected += OnModsSelected; - ModPaneVM = new ModPaneVM(skinManagerService, notificationService); + ModPaneVM = new ModPaneVM(); } private async void OnModsSelected(object? sender, ModListVM.ModSelectedEventArgs args) @@ -235,7 +238,7 @@ public async void OnNavigatedTo(object parameter) _notificationService.ShowNotification("Error while loading modes.", $"An error occurred while loading the mods for this character.\n{e.Message}", TimeSpan.FromSeconds(10)); - _navigationService.GoBack(); + ErrorNavigateBack(); } var lastSelectedSkin = SelectableInGameSkins.FirstOrDefault(selectCharacterTemplate => @@ -246,9 +249,9 @@ public async void OnNavigatedTo(object parameter) if (lastSelectedSkin is not null) await SwitchCharacterSkin(lastSelectedSkin); } - // This function is called from the NewModModel _toggleMod delegate. + // This function is called from the ModModel _toggleMod delegate. // This is a hacky way to get the toggle button to work. - private void ToggleMod(NewModModel thisMod) + private void ToggleMod(ModModel thisMod) { var modList = _skinManagerService.GetCharacterModList(thisMod.Character); if (thisMod.IsEnabled) @@ -334,13 +337,13 @@ private async Task RefreshMods() await _refreshMods(); var selectedMods = ModListVM.Mods.Where(mod => selectedModPaths.Any(oldModPath => ModFolderHelpers.FolderNameEquals(mod.FolderName, oldModPath))).ToArray(); - ModListVM.SelectionChanged(selectedMods, new List()); + ModListVM.SelectionChanged(selectedMods, new List()); } private async Task _refreshMods() { var refreshResult = await Task.Run(() => _skinManagerService.RefreshModsAsync(ShownCharacter)); - var modList = new List(); + var modList = new List(); var mods = _genshinService.IsMultiModCharacter(ShownCharacter) || !MultipleInGameSkins ? _modList.Mods @@ -348,7 +351,7 @@ private async Task _refreshMods() foreach (var skinEntry in mods) { - var newModModel = NewModModel.FromMod(skinEntry); + var newModModel = ModModel.FromMod(skinEntry); newModModel.WithToggleModDelegate(ToggleMod); var modSettings = await LoadModSettings(skinEntry); @@ -364,7 +367,7 @@ private async Task _refreshMods() if (inMemoryModNotification != null) newModModel.ModNotifications.Add(inMemoryModNotification); - //newModModel.ModNotifications.Add(new ModNotification() + //modModel.ModNotifications.Add(new ModNotification() //{ // CharacterId = ShownCharacter.Id, // ShowOnOverview = false, @@ -398,15 +401,20 @@ private async Task _refreshMods() } [RelayCommand] - private async Task SwitchCharacterSkin(SelectCharacterTemplate characterTemplate) + private async Task SwitchCharacterSkin(SelectCharacterTemplate? characterTemplate) { - if (characterTemplate?.DisplayName is not null && characterTemplate.DisplayName.Equals( + if (characterTemplate is null) + return; + + if (characterTemplate.DisplayName.Equals( SelectedInGameSkin.DisplayName, StringComparison.CurrentCultureIgnoreCase)) return; var characterSkin = ShownCharacter.InGameSkins.FirstOrDefault(skin => skin.DisplayName.Equals(characterTemplate.DisplayName, StringComparison.CurrentCultureIgnoreCase)); + + if (characterSkin is null) { _logger.Error("Could not find character skin {SkinName} for character {CharacterName}", @@ -417,9 +425,12 @@ private async Task SwitchCharacterSkin(SelectCharacterTemplate characterTemplate SelectedInGameSkin = SkinVM.FromSkin(characterSkin); MoveModsFlyoutVM.SetActiveSkin(SelectedInGameSkin); + + foreach (var selectableInGameSkin in SelectableInGameSkins) selectableInGameSkin.IsSelected = selectableInGameSkin.DisplayName.Equals(characterTemplate.DisplayName, StringComparison.CurrentCultureIgnoreCase); + _lastSelectedSkin[ShownCharacter] = SelectedInGameSkin.DisplayName; await RefreshMods().ConfigureAwait(false); } @@ -453,22 +464,21 @@ private async Task> FilterModsToSkin(IEn return filteredMods; } - private async Task LoadModSettings(CharacterSkinEntry characterSkinEntry) + private async Task LoadModSettings(CharacterSkinEntry characterSkinEntry) { - try - { - var modSettings = await characterSkinEntry.Mod.ReadSkinModSettings(); - return modSettings; - } - catch (JsonException e) - { - _logger.Error(e, "Error while reading mod settings for {ModName}", characterSkinEntry.Mod.Name); - _notificationService.ShowNotification("Error while reading mod settings.", - $"An error occurred while reading the mod settings for {characterSkinEntry.Mod.Name}, See logs for details.\n{e.Message}", - TimeSpan.FromSeconds(10)); - } + var result = await _modSettingsService.GetSettingsAsync(characterSkinEntry.Mod.Id); + + + ModSettings? modSettings = null; + + modSettings = result.Match( + modSettings => modSettings, + notFound => null, + modNotFound => null, + error => null); + - return null; + return modSettings; } [RelayCommand] @@ -528,19 +538,19 @@ await Task.Run(() => await RefreshMods().ConfigureAwait(false); } - public void ChangeModDetails(NewModModel newModModel) + public void ChangeModDetails(ModModel modModel) { - var oldMod = _modList.Mods.FirstOrDefault(mod => mod.Id == newModModel.Id); + var oldMod = _modList.Mods.FirstOrDefault(mod => mod.Id == modModel.Id); if (oldMod == null) { - _logger.Warning("Could not find mod with id {ModId} to change details.", newModModel.Id); + _logger.Warning("Could not find mod with id {ModId} to change details.", modModel.Id); return; } - var oldModModel = NewModModel.FromMod(oldMod); + var oldModModel = ModModel.FromMod(oldMod); - if (oldModModel.Name != newModModel.Name) + if (oldModModel.Name != modModel.Name) NotImplemented.Show("Setting custom mod names are not persisted between sessions", TimeSpan.FromSeconds(10)); } @@ -555,9 +565,9 @@ public async Task ModList_KeyHandler(IEnumerable modEntryId, VirtualKey ke } - private IEnumerable GetNewModModels() + private IEnumerable GetNewModModels() { - return _modList.Mods.Select(mod => NewModModel.FromMod(mod).WithToggleModDelegate(ToggleMod)); + return _modList.Mods.Select(mod => ModModel.FromMod(mod).WithToggleModDelegate(ToggleMod)); } diff --git a/src/GIMI-ModManager.WinUI/ViewModels/CharactersViewModel.cs b/src/GIMI-ModManager.WinUI/ViewModels/CharactersViewModel.cs index b5b03806..a139fe21 100644 --- a/src/GIMI-ModManager.WinUI/ViewModels/CharactersViewModel.cs +++ b/src/GIMI-ModManager.WinUI/ViewModels/CharactersViewModel.cs @@ -4,14 +4,14 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using GIMI_ModManager.Core.Contracts.Services; -using GIMI_ModManager.Core.Entities; using GIMI_ModManager.Core.Entities.Genshin; using GIMI_ModManager.Core.Services; using GIMI_ModManager.WinUI.Contracts.Services; using GIMI_ModManager.WinUI.Contracts.ViewModels; using GIMI_ModManager.WinUI.Models; -using GIMI_ModManager.WinUI.Models.Options; +using GIMI_ModManager.WinUI.Models.Settings; using GIMI_ModManager.WinUI.Services; +using GIMI_ModManager.WinUI.Services.ModHandling; using GIMI_ModManager.WinUI.Services.Notifications; using GIMI_ModManager.WinUI.ViewModels.SubVms; using Serilog; @@ -28,6 +28,7 @@ public partial class CharactersViewModel : ObservableRecipient, INavigationAware private readonly ModDragAndDropService _modDragAndDropService; private readonly ModNotificationManager _modNotificationManager; private readonly ModCrawlerService _modCrawlerService; + private readonly ModSettingsService _modSettingsService; public readonly GenshinProcessManager GenshinProcessManager; @@ -60,12 +61,14 @@ public partial class CharactersViewModel : ObservableRecipient, INavigationAware private CharacterGridItemModel[] _lastCharacters = Array.Empty(); + private bool isNavigating = true; + public CharactersViewModel(IGenshinService genshinService, ILogger logger, INavigationService navigationService, ISkinManagerService skinManagerService, ILocalSettingsService localSettingsService, NotificationManager notificationManager, ElevatorService elevatorService, GenshinProcessManager genshinProcessManager, ThreeDMigtoProcessManager threeDMigtoProcessManager, ModDragAndDropService modDragAndDropService, ModNotificationManager modNotificationManager, - ModCrawlerService modCrawlerService) + ModCrawlerService modCrawlerService, ModSettingsService modSettingsService) { _genshinService = genshinService; _logger = logger.ForContext(); @@ -79,6 +82,7 @@ public CharactersViewModel(IGenshinService genshinService, ILogger logger, INavi _modDragAndDropService = modDragAndDropService; _modNotificationManager = modNotificationManager; _modCrawlerService = modCrawlerService; + _modSettingsService = modSettingsService; ElevatorService.PropertyChanged += (sender, args) => { @@ -151,83 +155,10 @@ public bool SuggestionBox_Chosen(CharacterGridItemModel? character) return true; } - //private void ResetContent() - //{ - // var neitherPinnedNorHiddenCharacters = _characters.Where(x => - // !PinnedCharacters.Contains(x) && !HiddenCharacters.Contains(x)).ToArray(); - - // var listLocationIndex = 0; - // for (var i = 0; i < PinnedCharacters.Count; i++) - // { - // var character = PinnedCharacters.ElementAtOrDefault(i); - - // if (character is null) - // { - // Characters.Add(_characters[i]); - // listLocationIndex = i + 1; - // continue; - // } - - // if (character.Id != _characters[i].Id) - // { - // Characters.Insert(i, _characters[i]); - // } - - // listLocationIndex = i + 1; - // } - - // var nextListLocationIndex = listLocationIndex; - - // for (var i = listLocationIndex; i < neitherPinnedNorHiddenCharacters.Length + listLocationIndex; i++) - // { - // var index = i - listLocationIndex; - // var character = Characters.ElementAtOrDefault(i); - - // if (character is null) - // { - // Characters.Add(_characters[index]); - // nextListLocationIndex = i + 1; - // continue; - // } - - // if (character.Id != _characters[index].Id) - // { - // Characters.Insert(i, _characters[index]); - // } - - // nextListLocationIndex = i + 1; - // } - - // for (var i = nextListLocationIndex; i < HiddenCharacters.Count + nextListLocationIndex; i++) - // { - // var index = i - nextListLocationIndex; - // var character = HiddenCharacters.ElementAtOrDefault(i); - - // if (character is null) - // { - // if (HiddenCharacters.Contains(_characters[index])) - // { - // continue; - // } - - // Characters.Add(_characters[index]); - // continue; - // } - - // if (character.Id != _characters[index].Id) - // { - // if (HiddenCharacters.Contains(_characters[index])) - // { - // continue; - // } - - // Characters.Insert(i, _characters[index]); - // } - // } - //} - private void ResetContent() { + if (isNavigating) return; + var filteredCharacters = FilterCharacters(_backendCharacters); var sortedCharacters = _sortingMethod.Sort(filteredCharacters).ToList(); @@ -406,18 +337,14 @@ public async void OnNavigatedTo(object parameter) var subSkin = _modCrawlerService.GetFirstSubSkinRecursive(characterSkinEntry.Mod.FullPath)?.Name; - var modSettings = new SkinModSettings(); - try - { - modSettings = await characterSkinEntry.Mod.ReadSkinModSettings(); - } - catch (Exception e) - { - _logger.Error(e, "Error reading mod settings for {Mod}", characterSkinEntry.Mod.FullPath); - } + var modSettingsResult = await _modSettingsService.GetSettingsAsync(characterSkinEntry.Id); + + + var mod = ModModel.FromMod(characterSkinEntry); + - var mod = NewModModel.FromMod(characterSkinEntry); - mod.WithModSettings(modSettings); + if (modSettingsResult.IsT0) + mod.WithModSettings(modSettingsResult.AsT0); if (!string.IsNullOrWhiteSpace(mod.CharacterSkinOverride)) subSkin = mod.CharacterSkinOverride; @@ -438,7 +365,7 @@ public async void OnNavigatedTo(object parameter) break; } - if (addWarning) + if (addWarning || subSkinsFound.Count > 1 && modList.Character.InGameSkins.Count == 1) charactersWithMultipleActiveSkins.Add(modList.Character.Id); } @@ -473,7 +400,7 @@ public async void OnNavigatedTo(object parameter) // ShowOnlyModsCharacters var settings = await _localSettingsService - .ReadOrCreateSettingAsync(CharacterOverviewOptions.Key); + .ReadOrCreateSettingAsync(CharacterOverviewSettings.Key); if (settings.ShowOnlyCharactersWithMods) { ShowOnlyCharactersWithMods = true; @@ -487,6 +414,7 @@ await _localSettingsService FindCharacterById(_genshinService.OtherCharacterId), _lastCharacters, SortByDescending); SelectedSortingMethod = settings.SortingMethod; + isNavigating = false; ResetContent(); } @@ -602,15 +530,15 @@ private void HideCharacter(GenshinCharacter character) NotImplemented.Show("Hiding characters is not implemented yet"); } - private async Task ReadCharacterSettings() + private async Task ReadCharacterSettings() { - return await _localSettingsService.ReadSettingAsync(CharacterOverviewOptions.Key) ?? - new CharacterOverviewOptions(); + return await _localSettingsService.ReadSettingAsync(CharacterOverviewSettings.Key) ?? + new CharacterOverviewSettings(); } - private async Task SaveCharacterSettings(CharacterOverviewOptions settings) + private async Task SaveCharacterSettings(CharacterOverviewSettings settings) { - await _localSettingsService.SaveSettingAsync(CharacterOverviewOptions.Key, settings); + await _localSettingsService.SaveSettingAsync(CharacterOverviewSettings.Key, settings); } @@ -767,6 +695,7 @@ public Task ModDroppedOnAutoDetect(IReadOnlyList storageItems) [RelayCommand] private async Task SortBy(IEnumerable methodTypes) { + if (isNavigating) return; var sortingMethodType = methodTypes.First(); _sortingMethod = new SortingMethod(sortingMethodType, FindCharacterById(_genshinService.OtherCharacterId), diff --git a/src/GIMI-ModManager.WinUI/ViewModels/DebugViewModel.cs b/src/GIMI-ModManager.WinUI/ViewModels/DebugViewModel.cs index e1727877..6fa37c17 100644 --- a/src/GIMI-ModManager.WinUI/ViewModels/DebugViewModel.cs +++ b/src/GIMI-ModManager.WinUI/ViewModels/DebugViewModel.cs @@ -35,10 +35,12 @@ public DebugViewModel(ILogger logger, NotificationManager notificationManager, public ObservableCollection InGameSkins = new(); [RelayCommand] - private async Task TestCrawlerAsync() + private Task TestCrawlerAsync() { foreach (var subSkin in _modCrawlerService.GetSubSkinsRecursive(Path)) InGameSkins.Add(SkinVM.FromSkin(subSkin)); + + return Task.CompletedTask; } [RelayCommand] diff --git a/src/GIMI-ModManager.WinUI/ViewModels/SettingsViewModel.cs b/src/GIMI-ModManager.WinUI/ViewModels/SettingsViewModel.cs index fb7b2b67..69956774 100644 --- a/src/GIMI-ModManager.WinUI/ViewModels/SettingsViewModel.cs +++ b/src/GIMI-ModManager.WinUI/ViewModels/SettingsViewModel.cs @@ -14,9 +14,10 @@ using GIMI_ModManager.Core.Services; using GIMI_ModManager.WinUI.Contracts.Services; using GIMI_ModManager.WinUI.Helpers; -using GIMI_ModManager.WinUI.Models; using GIMI_ModManager.WinUI.Models.Options; using GIMI_ModManager.WinUI.Services; +using GIMI_ModManager.WinUI.Services.AppManagment; +using GIMI_ModManager.WinUI.Services.AppManagment.Updating; using GIMI_ModManager.WinUI.Validators.PreConfigured; using GIMI_ModManager.WinUI.ViewModels.SubVms; using Microsoft.UI.Xaml; @@ -291,12 +292,14 @@ private async Task ReorganizeModsAsync() try { var movedModsCount = await Task.Run(() => - _skinManagerService.ReorganizeMods()); // Mods folder + _skinManagerService.ReorganizeModsAsync()); // Mods folder movedModsCount += await Task.Run(() => - _skinManagerService.ReorganizeMods( + _skinManagerService.ReorganizeModsAsync( genshinService.GetCharacter(genshinService.OtherCharacterId))); // Others folder + await _skinManagerService.RefreshModsAsync(); + if (movedModsCount == -1) _notificationManager.ShowNotification("Mods reorganization failed.", "See logs for more details.", TimeSpan.FromSeconds(5)); diff --git a/src/GIMI-ModManager.WinUI/ViewModels/ShellViewModel.cs b/src/GIMI-ModManager.WinUI/ViewModels/ShellViewModel.cs index 1906f5cf..c30fcab9 100644 --- a/src/GIMI-ModManager.WinUI/ViewModels/ShellViewModel.cs +++ b/src/GIMI-ModManager.WinUI/ViewModels/ShellViewModel.cs @@ -1,11 +1,9 @@ -using System.Windows.Input; -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using GIMI_ModManager.WinUI.Contracts.Services; using GIMI_ModManager.WinUI.Services; +using GIMI_ModManager.WinUI.Services.AppManagment.Updating; using GIMI_ModManager.WinUI.Views; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Navigation; namespace GIMI_ModManager.WinUI.ViewModels; diff --git a/src/GIMI-ModManager.WinUI/ViewModels/StartupViewModel.cs b/src/GIMI-ModManager.WinUI/ViewModels/StartupViewModel.cs index 0666c391..ef42f581 100644 --- a/src/GIMI-ModManager.WinUI/ViewModels/StartupViewModel.cs +++ b/src/GIMI-ModManager.WinUI/ViewModels/StartupViewModel.cs @@ -1,17 +1,14 @@ -using System.Text; -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using GIMI_ModManager.WinUI.Contracts.Services; -using Windows.Storage.Pickers; using GIMI_ModManager.Core.Contracts.Services; -using GIMI_ModManager.WinUI.Models; -using GIMI_ModManager.WinUI.Services; -using Serilog; -using FluentValidation; +using GIMI_ModManager.WinUI.Contracts.Services; using GIMI_ModManager.WinUI.Contracts.ViewModels; -using GIMI_ModManager.WinUI.Validators; +using GIMI_ModManager.WinUI.Models.Options; +using GIMI_ModManager.WinUI.Services; +using GIMI_ModManager.WinUI.Services.AppManagment; using GIMI_ModManager.WinUI.Validators.PreConfigured; -using PathPicker = GIMI_ModManager.WinUI.ViewModels.SubVms.PathPicker; +using GIMI_ModManager.WinUI.ViewModels.SubVms; +using Serilog; namespace GIMI_ModManager.WinUI.ViewModels; @@ -25,7 +22,8 @@ public partial class StartupViewModel : ObservableRecipient, INavigationAware public PathPicker PathToGIMIFolderPicker { get; } public PathPicker PathToModsFolderPicker { get; } - [ObservableProperty] private bool _reorganizeModsOnStartup = false; + [ObservableProperty] private bool _reorganizeModsOnStartup; + [ObservableProperty] private bool _disableMods; public StartupViewModel(INavigationService navigationService, ILocalSettingsService localSettingsService, IWindowManagerService windowManagerService, ISkinManagerService skinManagerService) @@ -63,11 +61,12 @@ await _localSettingsService.SaveSettingAsync(ModManagerOptions.Section, modManagerOptions); _logger.Information("Saved startup settings: {@ModManagerOptions}", modManagerOptions); - await _skinManagerService.Initialize(modManagerOptions.ModsFolderPath!, null, modManagerOptions.GimiRootFolderPath); + await _skinManagerService.Initialize(modManagerOptions.ModsFolderPath!, null, + modManagerOptions.GimiRootFolderPath); if (ReorganizeModsOnStartup) { - await Task.Run(() => _skinManagerService.ReorganizeMods()); + await Task.Run(() => _skinManagerService.ReorganizeModsAsync(disableMods: DisableMods)); } @@ -94,10 +93,19 @@ private async Task BrowseGimiModFolderAsync() private async Task BrowseModsFolderAsync() => await PathToModsFolderPicker.BrowseFolderPathAsync(App.MainWindow); - public void OnNavigatedTo(object parameter) + public async void OnNavigatedTo(object parameter) { - _windowManagerService.ResizeWindowPercent(_windowManagerService.MainWindow, 40, 60); + _windowManagerService.ResizeWindowPercent(_windowManagerService.MainWindow, 40, 50); _windowManagerService.MainWindow.CenterOnScreen(); + + var settings = + await _localSettingsService.ReadOrCreateSettingAsync(ModManagerOptions.Section); + + if (!string.IsNullOrWhiteSpace(settings.GimiRootFolderPath)) + PathToGIMIFolderPicker.Path = settings.GimiRootFolderPath; + + if (!string.IsNullOrWhiteSpace(settings.ModsFolderPath)) + PathToModsFolderPicker.Path = settings.ModsFolderPath; } public void OnNavigatedFrom() diff --git a/src/GIMI-ModManager.WinUI/ViewModels/SubVms/ModListVM.cs b/src/GIMI-ModManager.WinUI/ViewModels/SubVms/ModListVM.cs index c1d8fb08..7aeb048a 100644 --- a/src/GIMI-ModManager.WinUI/ViewModels/SubVms/ModListVM.cs +++ b/src/GIMI-ModManager.WinUI/ViewModels/SubVms/ModListVM.cs @@ -12,9 +12,9 @@ public partial class ModListVM : ObservableRecipient { private readonly ISkinManagerService _skinManagerService; private readonly ModNotificationManager _modNotificationManager; - public readonly ObservableCollection BackendMods = new(); + public readonly ObservableCollection BackendMods = new(); - public ObservableCollection SelectedMods { get; } = new(); + public ObservableCollection SelectedMods { get; } = new(); [ObservableProperty] private InfoBarSeverity _severity = InfoBarSeverity.Warning; [ObservableProperty] private bool _isInfoBarOpen; @@ -23,7 +23,7 @@ public partial class ModListVM : ObservableRecipient [ObservableProperty] private int _selectedModsCount; - public ObservableCollection Mods { get; } = new(); + public ObservableCollection Mods { get; } = new(); public bool DisableInfoBar { get; set; } = false; @@ -40,11 +40,11 @@ private void Mods_CollectionChanged(object? sender, { if (e.NewItems is not null) { - foreach (NewModModel item in e.NewItems) + foreach (ModModel item in e.NewItems) { item.PropertyChanged += (o, args) => { - if (args.PropertyName != nameof(NewModModel.IsEnabled)) return; + if (args.PropertyName != nameof(ModModel.IsEnabled)) return; if (Mods.Count(x => x.IsEnabled) > 1) { @@ -59,7 +59,7 @@ private void Mods_CollectionChanged(object? sender, } } - public void SetBackendMods(IEnumerable mods) + public void SetBackendMods(IEnumerable mods) { BackendMods.Clear(); foreach (var mod in mods) @@ -68,7 +68,7 @@ public void SetBackendMods(IEnumerable mods) } } - public void ReplaceMods(IEnumerable mods) + public void ReplaceMods(IEnumerable mods) { Mods.Clear(); foreach (var mod in mods) @@ -99,7 +99,7 @@ public void ResetContent(SortMethod? sortMethod = null) { isEnabledComparer.IsDescending = sortMethod.IsDescending; - void AddMods(IEnumerable mods) + void AddMods(IEnumerable mods) { foreach (var mod in mods) { @@ -109,21 +109,21 @@ void AddMods(IEnumerable mods) switch (sortMethod.PropertyName) { - case nameof(NewModModel.IsEnabled): + case nameof(ModModel.IsEnabled): AddMods(sortMethod.IsDescending ? BackendMods.OrderByDescending(modModel => modModel, isEnabledComparer) : BackendMods.OrderBy(modModel => modModel, isEnabledComparer)); break; - case nameof(NewModModel.Name): + case nameof(ModModel.Name): AddMods(sortMethod.IsDescending ? BackendMods.OrderByDescending(modModel => modModel.Name) : BackendMods.OrderBy(modModel => modModel.Name)); break; - case nameof(NewModModel.FolderName): + case nameof(ModModel.FolderName): AddMods(sortMethod.IsDescending ? BackendMods.OrderByDescending(modModel => modModel.FolderName) : BackendMods.OrderBy(modModel => modModel.FolderName)); @@ -150,7 +150,7 @@ void AddMods(IEnumerable mods) ResetInfoBar(); } - public void SelectionChanged(ICollection selectedMods, ICollection removedMods) + public void SelectionChanged(ICollection selectedMods, ICollection removedMods) { if (selectedMods.Any()) { @@ -178,12 +178,12 @@ public void SelectionChanged(ICollection selectedMods, ICollection< public class ModSelectedEventArgs : EventArgs { - public ModSelectedEventArgs(IEnumerable mods) + public ModSelectedEventArgs(IEnumerable mods) { Mods = mods.ToArray(); } - public ICollection Mods { get; } + public ICollection Mods { get; } } @@ -203,12 +203,12 @@ public void ResetInfoBar() } } -public sealed class ModEnabledComparer : IComparer +public sealed class ModEnabledComparer : IComparer { public bool IsDescending; - public int Compare(NewModModel? x, NewModModel? y) + public int Compare(ModModel? x, ModModel? y) { if (x is null || y is null) return 0; if (x.IsEnabled == y.IsEnabled) diff --git a/src/GIMI-ModManager.WinUI/ViewModels/SubVms/ModPaneVM.cs b/src/GIMI-ModManager.WinUI/ViewModels/SubVms/ModPaneVM.cs index 62566d61..7affec86 100644 --- a/src/GIMI-ModManager.WinUI/ViewModels/SubVms/ModPaneVM.cs +++ b/src/GIMI-ModManager.WinUI/ViewModels/SubVms/ModPaneVM.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Text.Json; using Windows.Storage; using Windows.Storage.Pickers; using Windows.Storage.Streams; @@ -10,66 +9,68 @@ using GIMI_ModManager.Core.Contracts.Services; using GIMI_ModManager.WinUI.Models; using GIMI_ModManager.WinUI.Services; +using GIMI_ModManager.WinUI.Services.ModHandling; using Serilog; namespace GIMI_ModManager.WinUI.ViewModels.SubVms; public partial class ModPaneVM : ObservableRecipient { - private readonly ISkinManagerService _skinManagerService; - private readonly NotificationManager _notificationManager; + private readonly ISkinManagerService _skinManagerService = App.GetService(); + private readonly NotificationManager _notificationManager = App.GetService(); private readonly ILogger _logger = Log.ForContext(); + + private readonly KeySwapService _keySwapService = App.GetService(); + private readonly ModSettingsService _modSettingsService = App.GetService(); + private ISkinMod _selectedSkinMod = null!; - private ICharacterModList _modList = null!; - private NewModModel _backendModModel = null!; + private ModModel _backendModModel = null!; - [ObservableProperty] private NewModModel _selectedModModel = null!; + [ObservableProperty] private ModModel _selectedModModel = null!; [ObservableProperty] private bool _isReadOnlyMode = true; [ObservableProperty] private bool _isEditingModName = false; - public ModPaneVM(ISkinManagerService skinManagerService, NotificationManager notificationManager) - { - _skinManagerService = skinManagerService; - _notificationManager = notificationManager; - } - - public async Task LoadMod(NewModModel modModel, CancellationToken cancellationToken = default) + public async Task LoadMod(ModModel modModel, CancellationToken cancellationToken = default) { if (modModel.Id == SelectedModModel?.Id) return; UnloadMod(); - var skinEntry = _skinManagerService.GetCharacterModList(modModel.Character).Mods - .First(x => x.Id == modModel.Id); - _selectedSkinMod = skinEntry.Mod; - _backendModModel = NewModModel.FromMod(skinEntry); + var mod = _skinManagerService.GetModById(modModel.Id); + if (mod == null) + { + UnloadMod(); + return; + } + + _selectedSkinMod = mod; + + _backendModModel = ModModel.FromMod(mod, modModel.Character, modModel.IsEnabled); SelectedModModel = modModel; SelectedModModel.PropertyChanged += (_, _) => SettingsPropertiesChanged(); - await ReloadModSettings(cancellationToken).ConfigureAwait(false); } private async Task ReloadModSettings(CancellationToken cancellationToken = default) { if (_selectedSkinMod is null || _backendModModel is null || SelectedModModel is null) return; - try - { - var skinModSettings = - await _selectedSkinMod.ReadSkinModSettings(true, cancellationToken: cancellationToken); - _backendModModel.WithModSettings(skinModSettings); - SelectedModModel.WithModSettings(skinModSettings); - } - catch (JsonException e) + + var readSettingsResult = await _modSettingsService.GetSettingsAsync(_backendModModel.Id); + + + if (!readSettingsResult.TryPickT0(out var modSettings, out _)) { - _logger.Error(e, "Error while reading mod settings for {ModName}", _backendModModel.FolderName); - _notificationManager.ShowNotification("Error while reading mod settings.", - $"An error occurred while reading the mod settings for {_backendModModel.FolderName}, See logs for details.\n{e.Message}", - TimeSpan.FromSeconds(10)); + UnloadMod(); + return; } + SelectedModModel.WithModSettings(modSettings); + _backendModModel.WithModSettings(modSettings); + + Debug.Assert(_backendModModel.Equals(SelectedModModel)); @@ -79,9 +80,18 @@ private async Task ReloadModSettings(CancellationToken cancellationToken = defau return; } - var keySwaps = await _selectedSkinMod.ReadKeySwapConfiguration(cancellationToken: cancellationToken); - _backendModModel.SetKeySwaps(keySwaps); + var readKeySwapResult = await _keySwapService.GetKeySwapsAsync(_backendModModel.Id); + + if (!readKeySwapResult.TryPickT0(out var keySwaps, out _)) + { + IsReadOnlyMode = false; + return; + } + SelectedModModel.SetKeySwaps(keySwaps); + _backendModModel.SetKeySwaps(keySwaps); + + foreach (var skinModKeySwapModel in SelectedModModel.SkinModKeySwaps) skinModKeySwapModel.PropertyChanged += (_, _) => SettingsPropertiesChanged(); @@ -100,8 +110,7 @@ public void UnloadMod() _selectedSkinMod = null!; _backendModModel = null!; IsEditingModName = false; - SelectedModModel = new NewModModel(); - _modList = null!; + SelectedModModel = new ModModel(); SettingsPropertiesChanged(); } @@ -221,7 +230,7 @@ public async Task SetImageFromBitmapStreamAsync(RandomAccessStreamReference acce await Task.Run(async () => { - var stream = await accessStreamReference.OpenReadAsync(); + using var stream = await accessStreamReference.OpenReadAsync(); await using var fileStream = File.Create(tmpFile); await stream.AsStreamForRead().CopyToAsync(fileStream); }); @@ -253,46 +262,46 @@ private async Task SaveModSettingsAsync(CancellationToken cancellationToken = de { IsReadOnlyMode = true; var errored = false; - await Task.Run(async () => - { - var skinModSettings = SelectedModModel.ToModSettings(); - try - { - await _selectedSkinMod.SaveSkinModSettings(skinModSettings, cancellationToken); - } - catch (Exception e) - { - errored = true; - App.MainWindow.DispatcherQueue.TryEnqueue(() => - { - _notificationManager.ShowNotification("Error saving mod settings", - "An error occurred while saving the mod settings. Please check the log for more details.", - TimeSpan.FromSeconds(10)); - App.GetService().Error(e, "Error saving mod settings"); - }); - } + var saveResult = await _modSettingsService.SaveSettingsAsync(SelectedModModel); - if (!_selectedSkinMod.HasMergedInI || !SelectedModModel.SkinModKeySwaps.Any()) return; + if (saveResult.TryPickT2(out var error, out var notFoundOrSuccess)) + { + errored = true; + } + else if (notFoundOrSuccess.TryPickT1(out var modNotFound, out _)) + { + _notificationManager.ShowNotification("Error saving mod settings", + $"Could not find mod with id {modNotFound.ModId}", + TimeSpan.FromSeconds(5)); + } - var keySwaps = SelectedModModel.SkinModKeySwaps.Select(x => x.ToKeySwapSettings()).ToList(); - try - { - await _selectedSkinMod.SaveKeySwapConfiguration(keySwaps, cancellationToken); - } - catch (Exception e) - { - errored = true; - App.MainWindow.DispatcherQueue.TryEnqueue(() => + + if (_selectedSkinMod.HasMergedInI || SelectedModModel.SkinModKeySwaps.Any()) + { + var saveKeySwapResult = await _keySwapService.SaveKeySwapsAsync(SelectedModModel); + + saveKeySwapResult.Switch( + success => { }, + missingIni => { - _notificationManager.ShowNotification("Error saving key swap configuration", - "An error occured while saving the key swap configuration. Please check the log for more details.", - TimeSpan.FromSeconds(10)); - App.GetService().Error(e, "Error saving key swap configuration"); - }); - } - }, cancellationToken); + errored = true; + _notificationManager.ShowNotification("Error saving keyswap", + $"Could not find ini file for mod {SelectedModModel.Name}", + TimeSpan.FromSeconds(5)); + }, + modNotFound => + { + errored = true; + _notificationManager.ShowNotification("Error saving mod settings", + $"Could not find mod with id {modNotFound.ModId}", + TimeSpan.FromSeconds(5)); + }, + _ => { } + ); + } + IsReadOnlyMode = false; @@ -312,6 +321,6 @@ private void SettingsPropertiesChanged() [RelayCommand] private void ClearImage() { - SelectedModModel.ImagePath = NewModModel.PlaceholderImagePath; + SelectedModModel.ImagePath = ModModel.PlaceholderImagePath; } } \ No newline at end of file diff --git a/src/GIMI-ModManager.WinUI/ViewModels/SubVms/MoveModsFlyoutVM.cs b/src/GIMI-ModManager.WinUI/ViewModels/SubVms/MoveModsFlyoutVM.cs index f77ae7a0..fb30f7ae 100644 --- a/src/GIMI-ModManager.WinUI/ViewModels/SubVms/MoveModsFlyoutVM.cs +++ b/src/GIMI-ModManager.WinUI/ViewModels/SubVms/MoveModsFlyoutVM.cs @@ -8,6 +8,8 @@ using GIMI_ModManager.WinUI.Models.CustomControlTemplates; using GIMI_ModManager.WinUI.Models.ViewModels; using GIMI_ModManager.WinUI.Services; +using GIMI_ModManager.WinUI.Services.AppManagment; +using GIMI_ModManager.WinUI.Services.ModHandling; using Microsoft.UI.Xaml.Controls; using Serilog; @@ -18,6 +20,7 @@ public partial class MoveModsFlyoutVM : ObservableRecipient private readonly ISkinManagerService _skinManagerService; private readonly IGenshinService _genshinService; private readonly ILogger _logger = App.GetService().ForContext(); + private readonly ModSettingsService _modSettingsService = App.GetService(); private GenshinCharacter _shownCharacter = null!; @@ -45,7 +48,7 @@ [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(DeleteModsCommand), name public event EventHandler? CloseFlyoutEvent; public ObservableCollection SuggestedCharacters { get; init; } = new(); - private List SelectedMods { get; init; } = new(); + private List SelectedMods { get; init; } = new(); public ObservableCollection SelectableCharacterSkins { get; init; } = new(); @@ -84,7 +87,7 @@ public void SetActiveSkin(SkinVM skinVm) [RelayCommand] - private void SetSelectedMods(IEnumerable modModel) + private void SetSelectedMods(IEnumerable modModel) { SelectedMods.Clear(); SelectedMods.AddRange(modModel); @@ -261,25 +264,18 @@ private async Task OverrideModCharacterSkin() foreach (var modModel in SelectedMods) { - var skinMod = _skinManagerService.GetCharacterModList(_shownCharacter).Mods - .First(mod => mod.Id == modModel.Id).Mod; + var result = await _modSettingsService.SetCharacterSkinOverride(modModel.Id, characterSkinToSet.Name); - var skinModSettings = modModel.WithModSettings(await skinMod.ReadSkinModSettings()); + if (result.IsT0) continue; - skinModSettings.CharacterSkinOverride = characterSkinToSet.Name; - try - { - await skinMod.SaveSkinModSettings(skinModSettings.ToModSettings()); - } - catch (Exception e) - { - _logger.Error(e, "Error saving skin mod settings"); - var notificationManager = App.GetService(); - notificationManager.ShowNotification("Error Saving Skin Mod Settings", - $"Error saving skin mod settings for {skinMod.Name}\n{e.Message}", - TimeSpan.FromSeconds(10)); - } + var error = result.IsT1 ? result.AsT1.ToString() : result.AsT2.ToString(); + _logger.Error("Failed to override character skin for mod {modName}", modModel.Name); + App.GetService().ShowNotification( + $"Failed to override character skin for mod {modModel.Name}", + $"An Error Occurred. Reason: {error}", + TimeSpan.FromSeconds(5)); + continue; } ModCharactersSkinOverriden?.Invoke(this, EventArgs.Empty); diff --git a/src/GIMI-ModManager.WinUI/Views/CharacterDetailsPage.xaml b/src/GIMI-ModManager.WinUI/Views/CharacterDetailsPage.xaml index 2a4a47ed..7a7f3c8b 100644 --- a/src/GIMI-ModManager.WinUI/Views/CharacterDetailsPage.xaml +++ b/src/GIMI-ModManager.WinUI/Views/CharacterDetailsPage.xaml @@ -6,21 +6,19 @@ xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls" xmlns:controls1="using:GIMI_ModManager.WinUI.Views.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:entities="using:GIMI_ModManager.Core.Entities" xmlns:genshin="using:GIMI_ModManager.Core.Entities.Genshin" - xmlns:helpers="using:GIMI_ModManager.WinUI.Helpers" - xmlns:local="using:GIMI_ModManager.WinUI.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:models="using:GIMI_ModManager.WinUI.Models" - xmlns:options="using:GIMI_ModManager.WinUI.Models.Options" + xmlns:notifications="using:GIMI_ModManager.WinUI.Services.Notifications" + xmlns:xaml="using:GIMI_ModManager.WinUI.Helpers.Xaml" x:Name="CharacterDetailsRoot" mc:Ignorable="d"> - - - - + + + + - + - + - + - + { - foreach (var newSelectedMods in args?.NewItems?.OfType() ?? new List(0)) + foreach (var newSelectedMods in args?.NewItems?.OfType() ?? new List(0)) { - var equalItemInGrid = ModListGrid.ItemsSource.OfType() + var equalItemInGrid = ModListGrid.ItemsSource.OfType() .FirstOrDefault(x => x.Id == newSelectedMods.Id); - if (!ModListGrid.SelectedItems.OfType().Contains(equalItemInGrid)) + if (!ModListGrid.SelectedItems.OfType().Contains(equalItemInGrid)) ModListGrid.SelectedItems.Add(equalItemInGrid); } - foreach (var removedSelectedMods in args?.OldItems?.OfType() ?? new List(0)) + foreach (var removedSelectedMods in args?.OldItems?.OfType() ?? new List(0)) { - var equalItemInGrid = ModListGrid.ItemsSource.OfType() + var equalItemInGrid = ModListGrid.ItemsSource.OfType() .FirstOrDefault(x => x.Id == removedSelectedMods.Id); if (ModListGrid.SelectedItems.Contains(equalItemInGrid)) @@ -59,7 +59,7 @@ public CharacterDetailsPage() ModListGrid.Loaded += (sender, args) => { - var modEntry = ModListGrid.ItemsSource.OfType()?.FirstOrDefault(mod => mod.IsEnabled); + var modEntry = ModListGrid.ItemsSource.OfType()?.FirstOrDefault(mod => mod.IsEnabled); ModListGrid.SelectedItem = modEntry; // set focus to the first item ModListGrid.Focus(FocusState.Programmatic); @@ -277,7 +277,7 @@ private async void ModListArea_OnDrop(object sender, DragEventArgs e) private void ModListGrid_OnCellEditEnded(object? sender, DataGridCellEditEndedEventArgs e) { - var modModel = (NewModModel)e.Row.DataContext; + var modModel = (ModModel)e.Row.DataContext; ViewModel.ChangeModDetails(modModel); } @@ -285,7 +285,7 @@ private async void ModListGrid_OnKeyDown(object sender, KeyRoutedEventArgs e) { if (e.Key == VirtualKey.Space) { - await ViewModel.ModList_KeyHandler(ModListGrid.SelectedItems.OfType().Select(mod => mod.Id), + await ViewModel.ModList_KeyHandler(ModListGrid.SelectedItems.OfType().Select(mod => mod.Id), e.Key); e.Handled = true; } @@ -293,7 +293,7 @@ await ViewModel.ModList_KeyHandler(ModListGrid.SelectedItems.OfType if (e.Key == VirtualKey.Delete) { e.Handled = true; - ViewModel.MoveModsFlyoutVM.SetSelectedModsCommand.Execute(ModListGrid.SelectedItems.OfType() + ViewModel.MoveModsFlyoutVM.SetSelectedModsCommand.Execute(ModListGrid.SelectedItems.OfType() .ToArray()); await ViewModel.MoveModsFlyoutVM.DeleteModsCommand.ExecuteAsync(null); ViewModel.MoveModsFlyoutVM.ResetStateCommand.Execute(null); @@ -302,8 +302,8 @@ await ViewModel.ModList_KeyHandler(ModListGrid.SelectedItems.OfType private void ModListGrid_OnSelectionChanged(object sender, SelectionChangedEventArgs e) { - ViewModel.ModListVM.SelectionChanged(e.AddedItems.OfType().ToArray(), - e.RemovedItems.OfType().ToArray()); + ViewModel.ModListVM.SelectionChanged(e.AddedItems.OfType().ToArray(), + e.RemovedItems.OfType().ToArray()); } @@ -399,7 +399,7 @@ private void Image_OnImageFailed(object sender, ExceptionRoutedEventArgs e) private void ModNameCell_OnDoubleTapped(object sender, DoubleTappedRoutedEventArgs e) { if (ViewModel.ModPaneVM.IsReadOnlyMode || - ViewModel.ModPaneVM.SelectedModModel.SettingsEquals(new NewModModel())) + ViewModel.ModPaneVM.SelectedModModel.SettingsEquals(new ModModel())) return; ViewModel.ModPaneVM.IsEditingModName = true; diff --git a/src/GIMI-ModManager.WinUI/Views/CharactersPage.xaml b/src/GIMI-ModManager.WinUI/Views/CharactersPage.xaml index 3adf3895..a6c5b5b8 100644 --- a/src/GIMI-ModManager.WinUI/Views/CharactersPage.xaml +++ b/src/GIMI-ModManager.WinUI/Views/CharactersPage.xaml @@ -6,18 +6,16 @@ xmlns:animations="using:CommunityToolkit.WinUI.UI.Animations" xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:entities="using:GIMI_ModManager.Core.Entities" - xmlns:helpers="using:GIMI_ModManager.WinUI.Helpers" - xmlns:local="using:GIMI_ModManager.WinUI.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:models="using:GIMI_ModManager.WinUI.Models" xmlns:subVms="using:GIMI_ModManager.WinUI.ViewModels.SubVms" + xmlns:xaml="using:GIMI_ModManager.WinUI.Helpers.Xaml" mc:Ignorable="d"> - - - + + +