CVR Manager 1.0.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace CVRManager.ViewModels;
|
||||
|
||||
public abstract class ObservableObject : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected bool Set<T>(ref T field, T value, [CallerMemberName] string? name = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
|
||||
field = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void Raise([CallerMemberName] string? name = null) =>
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
|
||||
public class RelayCommand : ICommand
|
||||
{
|
||||
readonly Func<Task> run;
|
||||
|
||||
public RelayCommand(Func<Task> run) => this.run = run;
|
||||
public RelayCommand(Action run) => this.run = () => { run(); return Task.CompletedTask; };
|
||||
|
||||
public event EventHandler? CanExecuteChanged { add { } remove { } }
|
||||
public bool CanExecute(object? parameter) => true;
|
||||
public async void Execute(object? parameter) => await run();
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Input;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using Microsoft.VisualBasic.FileIO;
|
||||
|
||||
namespace CVRManager.ViewModels;
|
||||
|
||||
public class GalleryItemVM : ObservableObject
|
||||
{
|
||||
Bitmap? thumb;
|
||||
|
||||
public GalleryItemVM(GalleryViewModel owner, string path)
|
||||
{
|
||||
Path = path;
|
||||
Name = System.IO.Path.GetFileName(path);
|
||||
DateText = File.GetLastWriteTime(path).ToString("yyyy-MM-dd HH:mm");
|
||||
CopyCommand = new RelayCommand(() => owner.CopyAsync(this));
|
||||
DeleteCommand = new RelayCommand(() => owner.DeleteAsync(this));
|
||||
OpenCommand = new RelayCommand(() => owner.Open(this));
|
||||
ShowCommand = new RelayCommand(() => owner.Show(this));
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
public string Name { get; }
|
||||
public string DateText { get; }
|
||||
public Bitmap? Thumb { get => thumb; set => Set(ref thumb, value); }
|
||||
public ICommand CopyCommand { get; }
|
||||
public ICommand DeleteCommand { get; }
|
||||
public ICommand OpenCommand { get; }
|
||||
public ICommand ShowCommand { get; }
|
||||
}
|
||||
|
||||
public class GalleryViewModel : ObservableObject
|
||||
{
|
||||
const int PageSize = 48;
|
||||
readonly MainViewModel main;
|
||||
List<string> all = new();
|
||||
int shown;
|
||||
string status = "";
|
||||
bool loading;
|
||||
|
||||
public GalleryViewModel(MainViewModel main)
|
||||
{
|
||||
this.main = main;
|
||||
RefreshCommand = new RelayCommand(RefreshAsync);
|
||||
MoreCommand = new RelayCommand(LoadMore);
|
||||
OpenFolderCommand = new RelayCommand(() =>
|
||||
{
|
||||
Directory.CreateDirectory(Root);
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("explorer.exe", $"\"{Root}\"") { UseShellExecute = true });
|
||||
});
|
||||
}
|
||||
|
||||
public static string Root => System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "ChilloutVR");
|
||||
|
||||
public ObservableCollection<GalleryItemVM> Items { get; } = new();
|
||||
public ICommand RefreshCommand { get; }
|
||||
public ICommand MoreCommand { get; }
|
||||
public ICommand OpenFolderCommand { get; }
|
||||
public string Status { get => status; private set => Set(ref status, value); }
|
||||
public bool HasMore => shown < all.Count;
|
||||
public bool IsEmpty => !loading && all.Count == 0;
|
||||
public string FolderText => Root;
|
||||
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
loading = true;
|
||||
Status = "Looking for screenshots...";
|
||||
var found = await Task.Run(() =>
|
||||
{
|
||||
if (!Directory.Exists(Root)) return new List<string>();
|
||||
return Directory.EnumerateFiles(Root, "*.png", System.IO.SearchOption.AllDirectories)
|
||||
.Select(f => (f, t: File.GetLastWriteTimeUtc(f)))
|
||||
.OrderByDescending(x => x.t)
|
||||
.Select(x => x.f)
|
||||
.ToList();
|
||||
});
|
||||
loading = false;
|
||||
all = found;
|
||||
shown = 0;
|
||||
Items.Clear();
|
||||
LoadMore();
|
||||
Raise(nameof(IsEmpty));
|
||||
}
|
||||
|
||||
void LoadMore()
|
||||
{
|
||||
var batch = all.Skip(shown).Take(PageSize).ToList();
|
||||
shown += batch.Count;
|
||||
foreach (var path in batch)
|
||||
{
|
||||
var item = new GalleryItemVM(this, path);
|
||||
Items.Add(item);
|
||||
_ = LoadThumbAsync(item);
|
||||
}
|
||||
Status = all.Count == 0 ? "No screenshots yet." : $"Showing {shown} of {all.Count} screenshots";
|
||||
Raise(nameof(HasMore));
|
||||
}
|
||||
|
||||
static readonly SemaphoreSlim DecodeGate = new(3);
|
||||
|
||||
static async Task LoadThumbAsync(GalleryItemVM item)
|
||||
{
|
||||
await DecodeGate.WaitAsync();
|
||||
try
|
||||
{
|
||||
var bmp = await Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var fs = File.OpenRead(item.Path);
|
||||
return Bitmap.DecodeToWidth(fs, 360);
|
||||
}
|
||||
catch { return null; }
|
||||
});
|
||||
if (bmp != null) Dispatcher.UIThread.Post(() => item.Thumb = bmp);
|
||||
}
|
||||
finally { DecodeGate.Release(); }
|
||||
}
|
||||
|
||||
public async Task CopyAsync(GalleryItemVM item)
|
||||
{
|
||||
if (main.CopyImage == null) return;
|
||||
try
|
||||
{
|
||||
await main.CopyImage(item.Path);
|
||||
main.ReportStatus($"Copied {item.Name} to the clipboard.");
|
||||
}
|
||||
catch (Exception e) { main.ReportStatus($"Could not copy the image: {e.Message}"); }
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(GalleryItemVM item)
|
||||
{
|
||||
if (main.Confirm != null && !await main.Confirm($"Delete {item.Name}?\nIt is moved to the Recycle Bin.")) return;
|
||||
try
|
||||
{
|
||||
await Task.Run(() => FileSystem.DeleteFile(item.Path, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin));
|
||||
Items.Remove(item);
|
||||
all.Remove(item.Path);
|
||||
shown = Math.Max(0, shown - 1);
|
||||
Status = $"Showing {shown} of {all.Count} screenshots";
|
||||
Raise(nameof(HasMore));
|
||||
Raise(nameof(IsEmpty));
|
||||
}
|
||||
catch (Exception e) { main.ReportStatus($"Could not delete the image: {e.Message}"); }
|
||||
}
|
||||
|
||||
public void Open(GalleryItemVM item) =>
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(item.Path) { UseShellExecute = true });
|
||||
|
||||
public void Show(GalleryItemVM item) =>
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("explorer.exe", $"/select,\"{item.Path}\"") { UseShellExecute = true });
|
||||
}
|
||||
@@ -0,0 +1,934 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Input;
|
||||
using CVRManager.Models;
|
||||
using CVRManager.Services;
|
||||
|
||||
namespace CVRManager.ViewModels;
|
||||
|
||||
public class AssignmentVM : ObservableObject
|
||||
{
|
||||
readonly InstallVM owner;
|
||||
public ModAssignment Info { get; }
|
||||
public ModDef Mod { get; }
|
||||
|
||||
public AssignmentVM(InstallVM owner, ModAssignment info, ModDef mod)
|
||||
{
|
||||
this.owner = owner;
|
||||
Info = info;
|
||||
Mod = mod;
|
||||
RemoveCommand = new RelayCommand(() => owner.RemoveAssignment(this));
|
||||
}
|
||||
|
||||
public string Name => Mod.Name;
|
||||
public List<string> Versions => new[] { "latest" }.Concat(Mod.Releases.Select(r => r.Tag)).Concat(Mod.Local ? new[] { "local" } : Array.Empty<string>()).Distinct().ToList();
|
||||
|
||||
public string Version
|
||||
{
|
||||
get => Info.Version;
|
||||
set
|
||||
{
|
||||
if (value == null || Info.Version == value) return;
|
||||
Info.Version = value;
|
||||
Raise();
|
||||
Raise(nameof(Resolved));
|
||||
owner.MarkPending();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Enabled
|
||||
{
|
||||
get => Info.Enabled;
|
||||
set
|
||||
{
|
||||
if (Info.Enabled == value) return;
|
||||
Info.Enabled = value;
|
||||
Raise();
|
||||
owner.MarkPending();
|
||||
}
|
||||
}
|
||||
|
||||
public string Resolved
|
||||
{
|
||||
get
|
||||
{
|
||||
var tag = ModStore.ResolveTag(Mod, Info.Version);
|
||||
if (tag.Length == 0) return "no versions";
|
||||
var deployed = ModStore.IsDownloaded(Mod, tag) ? "downloaded" : "not downloaded";
|
||||
return Info.Version == "latest" ? $"= {tag} ({deployed})" : deployed;
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand RemoveCommand { get; }
|
||||
public void RefreshVersions() { Raise(nameof(Versions)); Raise(nameof(Resolved)); }
|
||||
}
|
||||
|
||||
public class InstallVM : ObservableObject
|
||||
{
|
||||
public MainViewModel Main { get; }
|
||||
public InstallInfo Info { get; }
|
||||
public ObservableCollection<AssignmentVM> Mods { get; } = new();
|
||||
public ObservableCollection<InstallHookVM> Hooks { get; } = new();
|
||||
|
||||
string installedBuild = "";
|
||||
string cacheText = "";
|
||||
bool pending;
|
||||
string? mlVersionChoice;
|
||||
ModDef? addChoice;
|
||||
|
||||
public InstallVM(MainViewModel main, InstallInfo info)
|
||||
{
|
||||
Main = main;
|
||||
Info = info;
|
||||
ImportModsCommand = new RelayCommand(() => Main.ImportInstallModsAsync(this));
|
||||
LaunchCommand = new RelayCommand(() => Main.LaunchAsync(this));
|
||||
StopCommand = new RelayCommand(() => Main.StopAsync(this));
|
||||
LaunchDesktopCommand = new RelayCommand(() => Main.LaunchAsync(this, "desktop"));
|
||||
LaunchVrCommand = new RelayCommand(() => Main.LaunchAsync(this, "vr"));
|
||||
UpdateCommand = new RelayCommand(() => Main.UpdateInstallAsync(this, false));
|
||||
VerifyCommand = new RelayCommand(() => Main.UpdateInstallAsync(this, true));
|
||||
OpenFolderCommand = new RelayCommand(() => Main.OpenFolder(this));
|
||||
ApplyCommand = new RelayCommand(() => Main.ApplyModsAsync(this));
|
||||
DeleteCommand = new RelayCommand(() => Main.DeleteInstallAsync(this));
|
||||
DuplicateCommand = new RelayCommand(() => Main.DuplicateInstallAsync(this));
|
||||
LinkCacheCommand = new RelayCommand(() => Main.LinkCacheAsync(this));
|
||||
AddModCommand = new RelayCommand(AddMod);
|
||||
EnterCodeCommand = new RelayCommand(() => Main.BeginCodeEntry(this));
|
||||
ToggleMlCommand = new RelayCommand(() => Main.ToggleMelonLoaderAsync(this));
|
||||
SetMlVersionCommand = new RelayCommand(() => Main.SetMelonLoaderVersionAsync(this));
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
public ICommand ImportModsCommand { get; }
|
||||
public ICommand LaunchCommand { get; }
|
||||
public ICommand StopCommand { get; }
|
||||
public ICommand LaunchDesktopCommand { get; }
|
||||
public ICommand LaunchVrCommand { get; }
|
||||
public ICommand UpdateCommand { get; }
|
||||
public ICommand VerifyCommand { get; }
|
||||
public ICommand OpenFolderCommand { get; }
|
||||
public ICommand ApplyCommand { get; }
|
||||
public ICommand DeleteCommand { get; }
|
||||
public ICommand DuplicateCommand { get; }
|
||||
public ICommand LinkCacheCommand { get; }
|
||||
public ICommand AddModCommand { get; }
|
||||
public ICommand EnterCodeCommand { get; }
|
||||
public ICommand ToggleMlCommand { get; }
|
||||
public ICommand SetMlVersionCommand { get; }
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => Info.Name;
|
||||
set { if (Info.Name != value) { Info.Name = value; Raise(); Raise(nameof(Title)); Main.Save(); Main.RefreshModUsage(); Main.ScheduleFolderRename(this); } }
|
||||
}
|
||||
|
||||
public string Branch => Info.Branch;
|
||||
|
||||
public IReadOnlyList<LaunchModeOption> LaunchModes => LaunchModeOption.All;
|
||||
|
||||
public LaunchModeOption LaunchModeChoice
|
||||
{
|
||||
get => LaunchModeOption.Find(Info.LaunchMode);
|
||||
set
|
||||
{
|
||||
if (value == null || Info.LaunchMode == value.Key) return;
|
||||
Info.LaunchMode = value.Key;
|
||||
Raise();
|
||||
Raise(nameof(LaunchTip));
|
||||
Main.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public string LaunchTip => $"Launch ({LaunchModeChoice.Label})";
|
||||
|
||||
public AccountChoice? AccountChoice
|
||||
{
|
||||
get => Main.AccountChoices.FirstOrDefault(c => c.Id == Info.AccountId) ?? Main.AccountChoices.FirstOrDefault();
|
||||
set
|
||||
{
|
||||
if (value == null || value.Id == Info.AccountId) return;
|
||||
Info.AccountId = value.Id;
|
||||
Raise();
|
||||
Main.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void RaiseAccount() => Raise(nameof(AccountChoice));
|
||||
|
||||
public string LaunchArgs
|
||||
{
|
||||
get => Info.LaunchArgs;
|
||||
set { if (Info.LaunchArgs != value) { Info.LaunchArgs = value; Raise(); Main.Save(); } }
|
||||
}
|
||||
|
||||
LaunchArgOption? argChoice;
|
||||
public LaunchArgOption? ArgChoice
|
||||
{
|
||||
get => argChoice;
|
||||
set
|
||||
{
|
||||
if (value == null) return;
|
||||
var text = value.Insert;
|
||||
var current = Info.LaunchArgs.Trim();
|
||||
if (!(" " + current + " ").Contains(" " + text.TrimEnd() + " ", StringComparison.Ordinal))
|
||||
LaunchArgs = current.Length == 0 ? text : current + " " + text;
|
||||
argChoice = value;
|
||||
Raise();
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() => { argChoice = null; Raise(); });
|
||||
}
|
||||
}
|
||||
|
||||
public BranchChoice? BranchHint
|
||||
{
|
||||
get => Main.BranchChoices.FirstOrDefault(c => c.Name == Info.Branch);
|
||||
set
|
||||
{
|
||||
if (value == null || value.Name == Info.Branch) return;
|
||||
Info.Branch = value.Name;
|
||||
Info.BranchPassword = value.Code;
|
||||
Raise();
|
||||
Raise(nameof(Branch));
|
||||
Raise(nameof(Title));
|
||||
RaiseStatus();
|
||||
Main.Save();
|
||||
Main.RefreshModUsage();
|
||||
}
|
||||
}
|
||||
|
||||
string subTab = "overview";
|
||||
public bool ShowOverview { get => subTab == "overview"; set { if (value) SetSub("overview"); } }
|
||||
public bool ShowModsTab { get => subTab == "mods"; set { if (value) SetSub("mods"); } }
|
||||
public bool ShowLogs { get => subTab == "logs"; set { if (value) SetSub("logs"); } }
|
||||
|
||||
void SetSub(string t)
|
||||
{
|
||||
if (subTab == t) return;
|
||||
subTab = t;
|
||||
Raise(nameof(ShowOverview));
|
||||
Raise(nameof(ShowModsTab));
|
||||
Raise(nameof(ShowLogs));
|
||||
mlStamp = unityStamp = default;
|
||||
RefreshLogs();
|
||||
}
|
||||
|
||||
string logTab = "ml";
|
||||
public bool ShowMlLog { get => logTab == "ml"; set { if (value) { logTab = "ml"; Raise(); Raise(nameof(ShowUnityLog)); } } }
|
||||
public bool ShowUnityLog { get => logTab == "unity"; set { if (value) { logTab = "unity"; Raise(); Raise(nameof(ShowMlLog)); } } }
|
||||
|
||||
string mlLog = "";
|
||||
string unityLog = "";
|
||||
string mlRaw = "";
|
||||
long mlColourVersion = -1;
|
||||
(long, long) mlStamp, unityStamp;
|
||||
public string MlLogText { get => mlLog; private set => Set(ref mlLog, value); }
|
||||
public string UnityLogText { get => unityLog; private set => Set(ref unityLog, value); }
|
||||
|
||||
bool logReading;
|
||||
|
||||
public void RefreshLogs()
|
||||
{
|
||||
if (!ShowLogs || logReading) return;
|
||||
logReading = true;
|
||||
var gameDir = Paths.GameDir(Info);
|
||||
var ml = Path.Combine(gameDir, "MelonLoader", "Latest.log");
|
||||
var ms = mlStamp;
|
||||
var us = unityStamp;
|
||||
var raw = mlRaw;
|
||||
var seen = mlColourVersion;
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
string? a = null, b = null;
|
||||
var version = seen;
|
||||
try
|
||||
{
|
||||
var dataDir = Paths.GameDataDir(gameDir);
|
||||
var unity = Path.Combine(dataDir, "Player.log");
|
||||
var prev = Path.Combine(dataDir, "Player-prev.log");
|
||||
b = ReadTail(unity, ref us, "No Unity log yet.");
|
||||
var fresh = ReadTail(ml, ref ms, "");
|
||||
version = MlLogColours.Refresh(prev, unity);
|
||||
if (fresh != null) raw = fresh;
|
||||
if (fresh != null || version != seen)
|
||||
a = raw.Length == 0 ? "No MelonLoader log yet." : MlLogColours.Colour(raw, prev, unity);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
logReading = false;
|
||||
mlStamp = ms;
|
||||
unityStamp = us;
|
||||
mlRaw = raw;
|
||||
mlColourVersion = version;
|
||||
if (a != null) MlLogText = a;
|
||||
if (b != null) UnityLogText = b;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static string? ReadTail(string path, ref (long, long) stamp, string empty)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
if (stamp == (-1, 0)) return null;
|
||||
stamp = (-1, 0);
|
||||
return empty;
|
||||
}
|
||||
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
||||
var s = (fs.Length, 0L);
|
||||
if (s == stamp) return null;
|
||||
stamp = s;
|
||||
const int max = 262144;
|
||||
if (fs.Length > max) fs.Seek(-max, SeekOrigin.End);
|
||||
using var r = new StreamReader(fs);
|
||||
return r.ReadToEnd();
|
||||
}
|
||||
catch (Exception e) when (e is IOException or UnauthorizedAccessException) { return null; }
|
||||
}
|
||||
|
||||
public string Title => $"{Info.Name} · {Info.Branch}";
|
||||
public string InstalledBuild { get => installedBuild; private set { if (Set(ref installedBuild, value)) { Raise(nameof(UpdateText)); Raise(nameof(HasGame)); } } }
|
||||
public bool HasGame => File.Exists(Paths.GameExe(Info));
|
||||
|
||||
public string UpdateText
|
||||
{
|
||||
get
|
||||
{
|
||||
var remote = Main.Branches.FirstOrDefault(b => b.Name == Info.Branch)?.BuildId ?? "";
|
||||
var local = string.IsNullOrEmpty(InstalledBuild) ? "not installed" : $"build {InstalledBuild}";
|
||||
if (remote.Length == 0) return $"{local} · remote build unknown";
|
||||
if (InstalledBuild.Length == 0) return $"{local} · remote build {remote}";
|
||||
return InstalledBuild == remote ? $"{local} · up to date" : $"{local} · update available (build {remote})";
|
||||
}
|
||||
}
|
||||
|
||||
string installedBranch = "";
|
||||
public string InstalledBranch { get => installedBranch; private set => Set(ref installedBranch, value); }
|
||||
|
||||
public string RemoteBuild => Main.Branches.FirstOrDefault(b => b.Name == Info.Branch)?.BuildId ?? "";
|
||||
public bool NotInstalled => InstalledBuild.Length == 0;
|
||||
public bool BranchMismatch => InstalledBuild.Length > 0 && InstalledBranch.Length > 0 && !InstalledBranch.Equals(Info.Branch, StringComparison.OrdinalIgnoreCase);
|
||||
public bool UpdateAvailable => BranchMismatch || (InstalledBuild.Length > 0 && RemoteBuild.Length > 0 && InstalledBuild != RemoteBuild);
|
||||
public bool UpdateUnknown => InstalledBuild.Length > 0 && !BranchMismatch && RemoteBuild.Length == 0;
|
||||
public bool UpToDate => InstalledBuild.Length > 0 && RemoteBuild.Length > 0 && !UpdateAvailable;
|
||||
public string UpdateChipText => BranchMismatch ? $"Files are from {InstalledBranch}: update to switch to {Info.Branch}" : $"Update available: build {RemoteBuild}";
|
||||
public string BuildText => InstalledBuild.Length > 0 ? $"Build {InstalledBuild}" : "Not installed";
|
||||
public string CardStatus => NotInstalled ? "Not installed"
|
||||
: BranchMismatch ? $"Out of date: {InstalledBranch} files"
|
||||
: UpdateAvailable ? $"Out of date: build {InstalledBuild}"
|
||||
: UpdateUnknown ? $"Build {InstalledBuild}: latest unknown"
|
||||
: $"Build {InstalledBuild}";
|
||||
|
||||
void RaiseStatus()
|
||||
{
|
||||
Raise(nameof(UpdateText));
|
||||
Raise(nameof(RemoteBuild));
|
||||
Raise(nameof(NotInstalled));
|
||||
Raise(nameof(BranchMismatch));
|
||||
Raise(nameof(UpdateAvailable));
|
||||
Raise(nameof(UpdateUnknown));
|
||||
Raise(nameof(UpToDate));
|
||||
Raise(nameof(UpdateChipText));
|
||||
Raise(nameof(BuildText));
|
||||
Raise(nameof(CardStatus));
|
||||
}
|
||||
|
||||
int instances;
|
||||
public bool IsRunning => instances > 0;
|
||||
public int InstanceCount => instances;
|
||||
public string RunningText => instances > 1 ? $"Running ×{instances}" : instances == 1 ? "Running" : "";
|
||||
public string StopText => instances > 1 ? $"■ Stop all ({instances})" : "■ Stop";
|
||||
public string StopShortText => instances > 1 ? $"■ ×{instances}" : "■";
|
||||
public string StopTip => instances > 1 ? "Stop all running instances of this install" : "Stop the game";
|
||||
|
||||
public void InstanceStarted() { instances++; NotifyInstances(); }
|
||||
public void InstanceEnded() { if (instances > 0) instances--; NotifyInstances(); }
|
||||
public void ResetInstances() { instances = 0; NotifyInstances(); }
|
||||
|
||||
void NotifyInstances()
|
||||
{
|
||||
Raise(nameof(IsRunning));
|
||||
Raise(nameof(InstanceCount));
|
||||
Raise(nameof(RunningText));
|
||||
Raise(nameof(StopText));
|
||||
Raise(nameof(StopShortText));
|
||||
Raise(nameof(StopTip));
|
||||
}
|
||||
public string CacheText { get => cacheText; private set => Set(ref cacheText, value); }
|
||||
public bool Pending { get => pending; private set { if (Set(ref pending, value)) Raise(nameof(PendingText)); } }
|
||||
public string PendingText => pending ? "Unapplied mod changes" : "";
|
||||
|
||||
public bool MlPresent => File.Exists(Path.Combine(Paths.GameDir(Info), "version.dll"));
|
||||
public bool MlOffPresent => File.Exists(Path.Combine(Paths.GameDir(Info), "version.dll.off"));
|
||||
public string? MlInstalledVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var sub in new[] { "net35", "net6" })
|
||||
{
|
||||
var dll = Path.Combine(Paths.GameDir(Info), "MelonLoader", sub, "MelonLoader.dll");
|
||||
if (!File.Exists(dll)) continue;
|
||||
var v = System.Diagnostics.FileVersionInfo.GetVersionInfo(dll).ProductVersion?.Split('+', '-')[0];
|
||||
if (!string.IsNullOrEmpty(v)) return v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ModAssignment? MlAssignment => Info.Mods.FirstOrDefault(m => m.ModId == ModDef.MelonLoaderId);
|
||||
|
||||
public string MlText
|
||||
{
|
||||
get
|
||||
{
|
||||
var a = MlAssignment;
|
||||
var installed = MlInstalledVersion is { } iv ? $" · files are v{iv}" : "";
|
||||
if (MlPresent) return (a is { Enabled: true } ? $"MelonLoader enabled · managed {a.Version}" : "MelonLoader enabled · not managed by this app") + installed;
|
||||
if (MlOffPresent) return "MelonLoader disabled · version.dll renamed to version.dll.off";
|
||||
return a != null ? "MelonLoader disabled" : "No MelonLoader";
|
||||
}
|
||||
}
|
||||
|
||||
public string MlToggleText => MlPresent ? "Disable MelonLoader" : "Enable MelonLoader";
|
||||
public IEnumerable<string> MlVersions => Main.MelonLoader.Releases.Select(r => r.Tag);
|
||||
|
||||
public string? MlVersionChoice
|
||||
{
|
||||
get => mlVersionChoice
|
||||
?? (MlAssignment is { } a && a.Version != "latest" ? a.Version : null)
|
||||
?? (MlInstalledVersion is { } v ? Main.MelonLoader.Releases.Select(r => r.Tag).FirstOrDefault(t => t.TrimStart('v') == v) : null);
|
||||
set { if (Set(ref mlVersionChoice, value)) Raise(); }
|
||||
}
|
||||
|
||||
public IEnumerable<ModDef> AddableMods => Main.ModCatalog
|
||||
.Where(m => m.Id != ModDef.MelonLoaderId && Info.Mods.All(a => a.ModId != m.Id));
|
||||
|
||||
public ModDef? AddChoice { get => addChoice; set => Set(ref addChoice, value); }
|
||||
|
||||
void AddMod()
|
||||
{
|
||||
if (AddChoice == null) return;
|
||||
var a = new ModAssignment { ModId = AddChoice.Id };
|
||||
Info.Mods.Add(a);
|
||||
Mods.Add(new AssignmentVM(this, a, AddChoice));
|
||||
AddChoice = null;
|
||||
Raise(nameof(AddableMods));
|
||||
MarkPending();
|
||||
}
|
||||
|
||||
public void RemoveAssignment(AssignmentVM vm)
|
||||
{
|
||||
Info.Mods.Remove(vm.Info);
|
||||
Mods.Remove(vm);
|
||||
Raise(nameof(AddableMods));
|
||||
MarkPending(true);
|
||||
}
|
||||
|
||||
public void MarkPending(bool force = false)
|
||||
{
|
||||
Pending = true;
|
||||
Main.Save();
|
||||
Main.NotifyAssignments();
|
||||
}
|
||||
|
||||
public void ClearPending() => Pending = false;
|
||||
|
||||
public void RebuildHooks()
|
||||
{
|
||||
Hooks.Clear();
|
||||
foreach (var h in Main.HookCatalog) Hooks.Add(new InstallHookVM(this, h));
|
||||
}
|
||||
|
||||
public void Rebuild()
|
||||
{
|
||||
RebuildHooks();
|
||||
Mods.Clear();
|
||||
foreach (var a in Info.Mods.Where(a => a.ModId != ModDef.MelonLoaderId).ToList())
|
||||
{
|
||||
var mod = Main.ModCatalog.FirstOrDefault(m => m.Id == a.ModId);
|
||||
if (mod != null) Mods.Add(new AssignmentVM(this, a, mod));
|
||||
}
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
InstalledBuild = AppManifest.LocalBuildId(Info);
|
||||
InstalledBranch = AppManifest.LocalBranch(Info);
|
||||
CacheText = CacheService.GetState(Info) switch
|
||||
{
|
||||
CacheState.Shared => "Cache: shared (junction to the shared cache)",
|
||||
CacheState.Local => "Cache: stored inside this install, not shared",
|
||||
CacheState.LinkedElsewhere => "Cache: linked to a folder other than the shared cache",
|
||||
_ => "Cache: not created yet",
|
||||
};
|
||||
Raise(nameof(HasGame));
|
||||
RaiseStatus();
|
||||
Raise(nameof(MlText));
|
||||
Raise(nameof(MlToggleText));
|
||||
Raise(nameof(MlVersions));
|
||||
Raise(nameof(MlVersionChoice));
|
||||
Raise(nameof(AddableMods));
|
||||
Raise(nameof(BranchHint));
|
||||
foreach (var m in Mods) m.RefreshVersions();
|
||||
}
|
||||
}
|
||||
|
||||
public class ReleaseRow
|
||||
{
|
||||
public ReleaseRow(ModDef mod, ReleaseInfo release)
|
||||
{
|
||||
Tag = release.Tag;
|
||||
var parts = new List<string> { release.Tag + (release.Prerelease ? " (pre-release)" : "") };
|
||||
if (release.Published != default) parts.Add(release.Published.ToString("yyyy-MM-dd"));
|
||||
if (release.GameVersion.Length > 0) parts.Add("game " + release.GameVersion);
|
||||
if (release.LoaderVersion.Length > 0) parts.Add("MelonLoader " + release.LoaderVersion);
|
||||
if (release.Snapshot) parts.Add("the file from your install");
|
||||
parts.Add(ModStore.IsDownloaded(mod, release.Tag) ? "downloaded" : "not downloaded");
|
||||
Text = string.Join(" · ", parts);
|
||||
}
|
||||
|
||||
public string Tag { get; }
|
||||
public string Text { get; }
|
||||
}
|
||||
|
||||
public class SourceVM : ObservableObject
|
||||
{
|
||||
readonly MainViewModel main;
|
||||
public MainViewModel Main => main;
|
||||
public SourceDef Info { get; }
|
||||
|
||||
public SourceVM(MainViewModel main, SourceDef info)
|
||||
{
|
||||
this.main = main;
|
||||
Info = info;
|
||||
RemoveCommand = new RelayCommand(() => main.RemoveSource(this));
|
||||
}
|
||||
|
||||
public ObservableCollection<ModVM> Mods { get; } = new();
|
||||
string filter = "";
|
||||
public string Filter { get => filter; set { if (Set(ref filter, value)) Raise(nameof(VisibleMods)); } }
|
||||
public IEnumerable<ModVM> VisibleMods => Mods.Where(m => filter.Trim().Length == 0
|
||||
|| m.Name.Contains(filter.Trim(), StringComparison.OrdinalIgnoreCase)
|
||||
|| m.Info.Author.Contains(filter.Trim(), StringComparison.OrdinalIgnoreCase)
|
||||
|| m.Info.Description.Contains(filter.Trim(), StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
public void SetMods(IEnumerable<ModVM> mods)
|
||||
{
|
||||
Mods.Clear();
|
||||
foreach (var m in mods) Mods.Add(m);
|
||||
Raise(nameof(VisibleMods));
|
||||
}
|
||||
|
||||
public string Name => Info.Name;
|
||||
public string Detail => Info.Kind == "cvrmg" ? "cvrmg.com" : $"github.com/{Info.Repo}";
|
||||
public bool CanRemove => !Info.Builtin || Info.Kind != "cvrmg";
|
||||
public ICommand RemoveCommand { get; }
|
||||
int count = -1;
|
||||
public int Count { get => count; set { if (Set(ref count, value)) Raise(nameof(CountText)); } }
|
||||
public string CountText => count < 0 ? "loading..." : $"{count} mods";
|
||||
public string Kind => Info.Kind == "cvrmg" ? "CVRMG" : "GitHub releases";
|
||||
public string Link => Info.Kind == "cvrmg" ? "https://cvrmg.com" : $"https://github.com/{Info.Repo}";
|
||||
}
|
||||
|
||||
public class GroupHeader : ObservableObject
|
||||
{
|
||||
readonly Action<GroupHeader> toggle;
|
||||
bool expanded = true;
|
||||
|
||||
public GroupHeader(string title, int count, bool expanded, Action<GroupHeader> toggle)
|
||||
{
|
||||
Title = title;
|
||||
Count = count;
|
||||
this.expanded = expanded;
|
||||
this.toggle = toggle;
|
||||
ToggleCommand = new RelayCommand(() => this.toggle(this));
|
||||
}
|
||||
|
||||
public string Title { get; }
|
||||
public int Count { get; }
|
||||
public bool Expanded => expanded;
|
||||
public string Text => $"{(expanded ? "▾" : "▸")} {Title} ({Count})";
|
||||
public ICommand ToggleCommand { get; }
|
||||
}
|
||||
|
||||
public class ModVM : ObservableObject
|
||||
{
|
||||
public MainViewModel Main { get; }
|
||||
public ModDef Info { get; }
|
||||
public ObservableCollection<ReleaseRow> Releases { get; } = new();
|
||||
ReleaseRow? selected;
|
||||
|
||||
public static string[] Types { get; } = { "Mods", "Plugins", "UserLibs", "Root" };
|
||||
|
||||
public ModVM(MainViewModel main, ModDef info)
|
||||
{
|
||||
Main = main;
|
||||
Info = info;
|
||||
RefreshCommand = new RelayCommand(() => Main.RefreshModAsync(this));
|
||||
DownloadCommand = new RelayCommand(() => Main.DownloadModAsync(this));
|
||||
DeleteDownloadCommand = new RelayCommand(() => Main.DeleteModDownloadAsync(this));
|
||||
RemoveCommand = new RelayCommand(() => Main.RemoveModAsync(this));
|
||||
AddLocalCommand = new RelayCommand(() => Main.AddLocalFilesAsync(this));
|
||||
ManageCommand = new RelayCommand(() => Main.ManagedMod = this);
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
static readonly Dictionary<string, string> FileVersions = new();
|
||||
|
||||
string? FileVersionOf(string tag)
|
||||
{
|
||||
var dir = Paths.ModFilesDir(Info, tag);
|
||||
if (!ModStore.IsDownloaded(Info, tag) || !Directory.Exists(dir)) return null;
|
||||
if (FileVersions.TryGetValue(dir, out var cached)) return cached;
|
||||
var dll = Directory.EnumerateFiles(dir, "*.dll", SearchOption.AllDirectories)
|
||||
.OrderBy(f => ModImportService.Norm(Path.GetFileName(f)) == ModImportService.Norm(Info.FileName) ? 0 : 1).FirstOrDefault();
|
||||
return FileVersions[dir] = dll == null ? "" : AssemblyReader.Read(dll).Version;
|
||||
}
|
||||
|
||||
string InstalledVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Assignment is not { Enabled: true } a) return "";
|
||||
var tag = ModStore.ResolveTag(Info, a.Version);
|
||||
var rel = Info.Releases.FirstOrDefault(r => r.Tag == tag);
|
||||
if (rel?.Snapshot == true)
|
||||
{
|
||||
var i = tag.IndexOf(" (", StringComparison.Ordinal);
|
||||
return tag.StartsWith("installed ", StringComparison.Ordinal) ? tag[10..(i > 10 ? i : tag.Length)] : tag;
|
||||
}
|
||||
var fromFile = FileVersionOf(tag);
|
||||
return string.IsNullOrEmpty(fromFile) ? "" : fromFile;
|
||||
}
|
||||
}
|
||||
|
||||
public string InstalledText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Assignment is not { Enabled: true } a) return "—";
|
||||
var v = InstalledVersion;
|
||||
return v.Length > 0 ? v : ModStore.ResolveTag(Info, a.Version) is { Length: > 0 } t ? t : "—";
|
||||
}
|
||||
}
|
||||
|
||||
ReleaseInfo? Newest => Info.Releases.FirstOrDefault(r => !r.Snapshot && !r.Prerelease) ?? Info.Releases.FirstOrDefault(r => !r.Snapshot);
|
||||
public string LatestText => Info.LatestVersion.Length > 0 ? Info.LatestVersion : "—";
|
||||
|
||||
static int CompareVersions(string a, string b)
|
||||
{
|
||||
a = a.Trim().TrimStart('v', 'V');
|
||||
b = b.Trim().TrimStart('v', 'V');
|
||||
if (Version.TryParse(a, out var x) && Version.TryParse(b, out var y))
|
||||
return (x.Major, x.Minor, Math.Max(x.Build, 0), Math.Max(x.Revision, 0)).CompareTo((y.Major, y.Minor, Math.Max(y.Build, 0), Math.Max(y.Revision, 0)));
|
||||
|
||||
var ca = System.Text.RegularExpressions.Regex.Matches(a, @"\d+|\D+");
|
||||
var cb = System.Text.RegularExpressions.Regex.Matches(b, @"\d+|\D+");
|
||||
for (var i = 0; i < Math.Min(ca.Count, cb.Count); i++)
|
||||
{
|
||||
var pa = ca[i].Value;
|
||||
var pb = cb[i].Value;
|
||||
var c = char.IsDigit(pa[0]) && char.IsDigit(pb[0])
|
||||
? System.Numerics.BigInteger.Parse(pa).CompareTo(System.Numerics.BigInteger.Parse(pb))
|
||||
: string.Compare(pa, pb, StringComparison.OrdinalIgnoreCase);
|
||||
if (c != 0) return c;
|
||||
}
|
||||
return ca.Count.CompareTo(cb.Count);
|
||||
}
|
||||
|
||||
public bool UpdateAvailable
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Assignment is not { Enabled: true } a) return false;
|
||||
var installed = InstalledVersion;
|
||||
if (installed.Length > 0 && Info.LatestVersion.Length > 0) return CompareVersions(Info.LatestVersion, installed) > 0;
|
||||
if (Newest == null) return false;
|
||||
var tag = ModStore.ResolveTag(Info, a.Version);
|
||||
var rel = Info.Releases.FirstOrDefault(r => r.Tag == tag);
|
||||
return rel is { Snapshot: false } && tag != Newest.Tag;
|
||||
}
|
||||
}
|
||||
|
||||
public string StatusText => Assignment is not { Enabled: true } ? "" : UpdateAvailable ? "Update available" : "Installed";
|
||||
public string ShortDescription => Info.Description.Split('\n')[0].Trim();
|
||||
public string AuthorLine => string.Join(" · ", new[] { Info.Author, Info.Source == "cvrmg" ? "CVRMG" : Info.Source == "github" ? Info.Repo : "local" }.Where(x => x.Length > 0));
|
||||
public string CategoryText => Info.Category.Length > 0 ? Info.Category : Info.Source == "local" ? "Local files" : "Uncategorized";
|
||||
|
||||
InstallVM? Current => Main.SelectedInstall;
|
||||
ModAssignment? Assignment => Current?.Info.Mods.FirstOrDefault(a => a.ModId == Info.Id);
|
||||
public bool HasInstall => Current != null;
|
||||
public bool InstallAssigned => Assignment != null;
|
||||
|
||||
public bool InCatalog => Main.IsInCatalog(Info);
|
||||
|
||||
public bool InstallUsed
|
||||
{
|
||||
get => Assignment is { Enabled: true };
|
||||
set
|
||||
{
|
||||
var cur = Current;
|
||||
if (cur == null) return;
|
||||
Main.EnsureInCatalog(this);
|
||||
var a = Assignment;
|
||||
if (value)
|
||||
{
|
||||
if (a == null) cur.Info.Mods.Add(a = new ModAssignment { ModId = Info.Id });
|
||||
a.Enabled = true;
|
||||
}
|
||||
else if (a != null) a.Enabled = false;
|
||||
cur.Rebuild();
|
||||
cur.MarkPending();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> VersionChoices => new[] { "latest" }.Concat(Info.Releases.Select(r => r.Tag)).Distinct().ToList();
|
||||
|
||||
public string InstallVersion
|
||||
{
|
||||
get => Assignment?.Version ?? "latest";
|
||||
set
|
||||
{
|
||||
if (value == null || Current == null) return;
|
||||
Main.EnsureInCatalog(this);
|
||||
var a = Assignment;
|
||||
if (a == null) Current.Info.Mods.Add(a = new ModAssignment { ModId = Info.Id });
|
||||
if (a.Version == value && a.Enabled) return;
|
||||
a.Version = value;
|
||||
a.Enabled = true;
|
||||
Current!.Rebuild();
|
||||
Current.MarkPending();
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshUsedBy() => Raise(nameof(UsedBy));
|
||||
|
||||
public void NotifyInstall()
|
||||
{
|
||||
Raise(nameof(HasInstall));
|
||||
Raise(nameof(InstallAssigned));
|
||||
Raise(nameof(InstallUsed));
|
||||
Raise(nameof(VersionChoices));
|
||||
Raise(nameof(InstallVersion));
|
||||
Raise(nameof(ToggleInstallText));
|
||||
Raise(nameof(InstalledText));
|
||||
Raise(nameof(LatestText));
|
||||
Raise(nameof(UpdateAvailable));
|
||||
Raise(nameof(StatusText));
|
||||
Raise(nameof(ShortDescription));
|
||||
}
|
||||
|
||||
public ICommand ToggleInstallCommand => new RelayCommand(() => InstallUsed = !InstallUsed);
|
||||
public string ToggleInstallText => InstallUsed ? "Remove from install" : "Add to install";
|
||||
|
||||
public ICommand RefreshCommand { get; }
|
||||
public ICommand DownloadCommand { get; }
|
||||
public ICommand DeleteDownloadCommand { get; }
|
||||
public ICommand RemoveCommand { get; }
|
||||
public ICommand AddLocalCommand { get; }
|
||||
public ICommand ManageCommand { get; }
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => Info.Name;
|
||||
set { if (Info.Name != value) { Info.Name = value; Raise(); Main.Save(); } }
|
||||
}
|
||||
|
||||
public string Type
|
||||
{
|
||||
get => Info.Type;
|
||||
set { if (value != null && Info.Type != value) { Info.Type = value; Raise(); Main.Save(); } }
|
||||
}
|
||||
|
||||
public string AssetFilter
|
||||
{
|
||||
get => Info.AssetFilter;
|
||||
set { if (Info.AssetFilter != value) { Info.AssetFilter = value; Raise(); Main.Save(); } }
|
||||
}
|
||||
|
||||
public bool IsLocal => Info.Source == "local";
|
||||
public bool IsBuiltin => Info.Id == ModDef.MelonLoaderId;
|
||||
public bool CanRemove => !IsBuiltin;
|
||||
public string Source => Info.Source switch
|
||||
{
|
||||
"cvrmg" => $"CVRMG #{Info.CvrmgId}",
|
||||
"local" => "Local files",
|
||||
_ => $"github.com/{Info.Repo}",
|
||||
};
|
||||
|
||||
public IEnumerable<string> SourceOptions => new[] { Info.Repo.Length > 0 && !Info.Local ? "GitHub" : "", Info.CvrmgId > 0 ? "CVRMG" : "" }.Where(x => x.Length > 0);
|
||||
public bool HasSourceChoice => SourceOptions.Count() > 1;
|
||||
|
||||
public string? SelectedSource
|
||||
{
|
||||
get => Info.Source switch { "cvrmg" => "CVRMG", "github" => "GitHub", _ => null };
|
||||
set
|
||||
{
|
||||
if (value == null) return;
|
||||
var s = value == "CVRMG" ? "cvrmg" : "github";
|
||||
if (s != Info.Source) _ = Main.SwitchSourceAsync(this, s);
|
||||
}
|
||||
}
|
||||
|
||||
public string Details
|
||||
{
|
||||
get
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (Info.Source == "cvrmg" || Info.CvrmgId > 0) parts.Add("on CVRMG");
|
||||
if (Info.Author.Length > 0) parts.Add("by " + Info.Author);
|
||||
if (Info.Category.Length > 0) parts.Add(Info.Category);
|
||||
if (Info.Status.Length > 0 && Info.Status != "approved") parts.Add($"CVRMG status: {Info.Status}");
|
||||
if (Info.SourceLink.Length > 0) parts.Add(Info.SourceLink);
|
||||
return string.Join(" · ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
public string Description => Info.Description.Length > 600 ? Info.Description[..600] + "..." : Info.Description;
|
||||
public bool HasDescription => Info.Description.Length > 0;
|
||||
public ReleaseRow? SelectedRelease { get => selected; set => Set(ref selected, value); }
|
||||
|
||||
public string UsedBy
|
||||
{
|
||||
get
|
||||
{
|
||||
var lines = Main.Installs
|
||||
.Select(i => (i, a: i.Info.Mods.FirstOrDefault(m => m.ModId == Info.Id)))
|
||||
.Where(x => x.a != null)
|
||||
.Select(x => $"{x.i.Info.Name} ({x.i.Info.Branch}): {(x.a!.Enabled ? "" : "disabled, ")}{x.a.Version}")
|
||||
.ToList();
|
||||
return lines.Count == 0 ? "Not used by any install" : string.Join("\n", lines);
|
||||
}
|
||||
}
|
||||
|
||||
public void Rebuild()
|
||||
{
|
||||
var keep = SelectedRelease?.Tag;
|
||||
Releases.Clear();
|
||||
foreach (var r in Info.Releases) Releases.Add(new ReleaseRow(Info, r));
|
||||
if (Info.Local && Info.Releases.Count == 0) Releases.Add(new ReleaseRow(Info, new ReleaseInfo { Tag = "local" }));
|
||||
SelectedRelease = Releases.FirstOrDefault(r => r.Tag == keep) ?? Releases.FirstOrDefault();
|
||||
Raise(nameof(UsedBy));
|
||||
Raise(nameof(Name));
|
||||
Raise(nameof(Source));
|
||||
Raise(nameof(SelectedSource));
|
||||
Raise(nameof(Details));
|
||||
Raise(nameof(Description));
|
||||
NotifyInstall();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class InstallHookVM : ObservableObject
|
||||
{
|
||||
readonly InstallVM owner;
|
||||
public HookDef Hook { get; }
|
||||
|
||||
public InstallHookVM(InstallVM owner, HookDef hook)
|
||||
{
|
||||
this.owner = owner;
|
||||
Hook = hook;
|
||||
RunCommand = new RelayCommand(() => owner.Main.RunHookAsync(owner, hook));
|
||||
}
|
||||
|
||||
public string Name => Hook.Name;
|
||||
|
||||
public string When
|
||||
{
|
||||
get
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (Hook.BeforeUpdate) parts.Add("before update");
|
||||
if (Hook.AfterUpdate) parts.Add("after install / update");
|
||||
if (Hook.BeforeLaunch) parts.Add("before launch");
|
||||
return parts.Count == 0 ? "manual only" : string.Join(", ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Enabled
|
||||
{
|
||||
get => owner.Info.HookIds.Contains(Hook.Id);
|
||||
set
|
||||
{
|
||||
if (Enabled == value) return;
|
||||
if (value) owner.Info.HookIds.Add(Hook.Id);
|
||||
else owner.Info.HookIds.Remove(Hook.Id);
|
||||
Raise();
|
||||
owner.Main.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand RunCommand { get; }
|
||||
}
|
||||
|
||||
public class AccountVM : ObservableObject
|
||||
{
|
||||
public MainViewModel Main { get; }
|
||||
public GameAccount Info { get; }
|
||||
|
||||
public AccountVM(MainViewModel main, GameAccount info)
|
||||
{
|
||||
Main = main;
|
||||
Info = info;
|
||||
RemoveCommand = new RelayCommand(() => Main.RemoveAccountAsync(this));
|
||||
}
|
||||
|
||||
public ICommand RemoveCommand { get; }
|
||||
public string Username => Info.Username;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => Info.Name;
|
||||
set
|
||||
{
|
||||
var name = value.Trim();
|
||||
if (name.Length == 0 || Info.Name == name) return;
|
||||
Info.Name = name;
|
||||
Raise();
|
||||
Main.AccountsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class HookVM : ObservableObject
|
||||
{
|
||||
public MainViewModel Main { get; }
|
||||
public HookDef Info { get; }
|
||||
|
||||
public HookVM(MainViewModel main, HookDef info)
|
||||
{
|
||||
Main = main;
|
||||
Info = info;
|
||||
RemoveCommand = new RelayCommand(() => Main.RemoveHookAsync(this));
|
||||
}
|
||||
|
||||
public ICommand RemoveCommand { get; }
|
||||
|
||||
public bool CanRemove => true;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => Info.Name;
|
||||
set { if (Info.Name != value) { Info.Name = value; Raise(); Main.HooksChanged(); } }
|
||||
}
|
||||
|
||||
public string Command
|
||||
{
|
||||
get => Info.Command;
|
||||
set { if (Info.Command != value) { Info.Command = value; Raise(); Main.Save(); } }
|
||||
}
|
||||
|
||||
public string Arguments
|
||||
{
|
||||
get => Info.Arguments;
|
||||
set { if (Info.Arguments != value) { Info.Arguments = value; Raise(); Main.Save(); } }
|
||||
}
|
||||
|
||||
public bool BeforeUpdate { get => Info.BeforeUpdate; set { if (Info.BeforeUpdate != value) { Info.BeforeUpdate = value; Raise(); Main.HooksChanged(); } } }
|
||||
public bool AfterUpdate { get => Info.AfterUpdate; set { if (Info.AfterUpdate != value) { Info.AfterUpdate = value; Raise(); Main.HooksChanged(); } } }
|
||||
public bool BeforeLaunch { get => Info.BeforeLaunch; set { if (Info.BeforeLaunch != value) { Info.BeforeLaunch = value; Raise(); Main.HooksChanged(); } } }
|
||||
public bool DefaultOn { get => Info.DefaultOn; set { if (Info.DefaultOn != value) { Info.DefaultOn = value; Raise(); Main.Save(); } } }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user