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">
-
-
-
+
+
+