30 Commits

Author SHA1 Message Date
f089629c74 something 2025-12-19 15:19:09 +01:00
836282c6ac update nuget packages & fixes 2025-12-19 14:54:28 +01:00
ee6b8d443d update update-url 2025-12-19 14:09:01 +01:00
5947f81307 new extras system 2025-12-19 14:07:59 +01:00
32c4065940 Merge branch 'feat/mvvm-for-client' into 'master'
client(ui): migrate to mvvm

See merge request litw-refined/minecraft-modpack-updater!3
2025-12-12 19:32:05 +00:00
8c29cf9e8a client(ui): migrate to mvvm 2025-12-12 19:32:05 +00:00
16b1a655aa ui(manager): use Menus with HeaderMenuItems instead of ImageDropDownButtons 2025-12-02 15:54:45 +01:00
12b5a63003 update nuget packages 2025-12-02 15:54:03 +01:00
035d2eeb9b remove getter for LastMinecraftProfilePath config option 2025-11-28 09:00:33 +01:00
eaaca4ddb8 fix duplicated recent files 2025-11-26 16:30:19 +01:00
c1960abe3a update to .net 10 2025-11-23 11:41:34 +01:00
cbd4b1c346 cli: info 2025-11-23 11:39:56 +01:00
db108fe36e manager: scroll into view 2025-11-23 10:11:37 +01:00
bd8a08f03c check null and whitespace for github release name 2025-11-22 14:42:12 +01:00
7e2d7f56df fix update 2025-11-22 14:17:21 +01:00
c7b2fff7e7 fix GitHub tag detection 2025-11-22 14:05:27 +01:00
a6a3d9d5a9 add missing source tag 2025-11-22 13:59:43 +01:00
5a90cc7e82 increase timeout of curseforge requsts 2025-11-22 13:44:14 +01:00
1e3d2701fc use dedicated publish script 2025-11-22 09:30:08 +01:00
f5e84b6da7 fix delete Artifacts directory 2025-11-21 19:57:52 +01:00
c864d9125a merge update urls and include app short name in distro 2025-11-21 19:52:58 +01:00
Pascal
e7aa79db64 ui(client): recent profile selector 2025-11-20 06:49:21 +01:00
Pascal
c0f013a6c5 code clenaup 2025-11-19 07:56:55 +01:00
Pascal
f21e31ebcc ui(manager): use popup for app update message 2025-11-19 07:55:46 +01:00
Pascal
0ae2a780b0 ui(manager): arrange DataGrid columns width & order 2025-11-19 07:47:25 +01:00
Pascal
474f76df4a disable update on debug builds 2025-11-19 07:40:05 +01:00
Pascal
6454d97173 ui(manager): add name column 2025-11-19 07:30:56 +01:00
Pascal
e57d2316de ui(manager): search for DataGrid 2025-11-19 07:21:19 +01:00
2cbe25e0f8 ui(manager): improve search bindings 2025-11-18 16:21:34 +01:00
86f93cf3d7 publish: remove artifacts 2025-11-18 07:30:05 +01:00
46 changed files with 913 additions and 762 deletions

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "publish-scripts"]
path = publish-scripts
url = https://git.pilzinsel64.de/Pilz.NET/publish-scripts.git

View File

@@ -16,6 +16,10 @@
<NoWarn>1591</NoWarn> <NoWarn>1591</NoWarn>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>$(DefineConstants);DISABLE_UPDATE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'"> <PropertyGroup Condition="'$(Configuration)'=='Release'">
<DebugSymbols>False</DebugSymbols> <DebugSymbols>False</DebugSymbols>
<DebugType>None</DebugType> <DebugType>None</DebugType>

View File

@@ -1,5 +1,6 @@
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
namespace ModpackUpdater.Apps.Client.Gui; namespace ModpackUpdater.Apps.Client.Gui;
@@ -15,7 +16,26 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
desktop.MainWindow = new MainForm(); {
DisableAvaloniaDataAnnotationValidation();
desktop.MainWindow = new MainView
{
DataContext = new MainViewModel(),
};
}
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
} }
private void DisableAvaloniaDataAnnotationValidation()
{
// Get an array of plugins to remove
var dataValidationPluginsToRemove =
BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
// remove each entry found
foreach (var plugin in dataValidationPluginsToRemove)
{
BindingPlugins.DataValidators.Remove(plugin);
}
}
} }

View File

@@ -7,19 +7,21 @@ public class AppConfig : ISettingsNode, ISettingsIdentifier
{ {
public static string Identifier => "pilz.appconfig"; public static string Identifier => "pilz.appconfig";
public string? LastMinecraftProfilePath { get; set; } public List<string> RecentMinecraftProfilePaths { get; } = [];
[JsonIgnore, Obsolete] [JsonProperty, Obsolete]
public string? ConfigFilePath { get; private set; } private string? LastMinecraftProfilePath
[JsonProperty("ConfigFilePath"), Obsolete]
private string ConfigFilePathLegacy
{ {
set => ConfigFilePath = value; set
{
if (!string.IsNullOrWhiteSpace(value))
RecentMinecraftProfilePaths.Insert(0, value);
}
} }
public void Reset() public void Reset()
{ {
LastMinecraftProfilePath = null; RecentMinecraftProfilePaths.Clear();
} }
public static AppConfig Instance => Program.Settings.Get<AppConfig>(); public static AppConfig Instance => Program.Settings.Get<AppConfig>();

View File

@@ -11,32 +11,46 @@ namespace ModpackUpdater.Apps.Client.Gui.LangRes {
using System; using System;
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] /// <summary>
[System.Diagnostics.DebuggerNonUserCodeAttribute()] /// A strongly-typed resource class, for looking up localized strings, etc.
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()] /// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class GeneralLangRes { public class GeneralLangRes {
private static System.Resources.ResourceManager resourceMan; private static global::System.Resources.ResourceManager resourceMan;
private static System.Globalization.CultureInfo resourceCulture; private static global::System.Globalization.CultureInfo resourceCulture;
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal GeneralLangRes() { internal GeneralLangRes() {
} }
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] /// <summary>
public static System.Resources.ResourceManager ResourceManager { /// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get { get {
if (object.Equals(null, resourceMan)) { if (object.ReferenceEquals(resourceMan, null)) {
System.Resources.ResourceManager temp = new System.Resources.ResourceManager("ModpackUpdater.Apps.Client.Gui.LangRes.GeneralLangRes", typeof(GeneralLangRes).Assembly); global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ModpackUpdater.Apps.Client.Gui.LangRes.GeneralLangRes", typeof(GeneralLangRes).Assembly);
resourceMan = temp; resourceMan = temp;
} }
return resourceMan; return resourceMan;
} }
} }
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] /// <summary>
public static System.Globalization.CultureInfo Culture { /// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get { get {
return resourceCulture; return resourceCulture;
} }
@@ -45,118 +59,184 @@ namespace ModpackUpdater.Apps.Client.Gui.LangRes {
} }
} }
/// <summary>
/// Looks up a localized string similar to An update is available!.
/// </summary>
public static string AnUpdateIsAvailable { public static string AnUpdateIsAvailable {
get { get {
return ResourceManager.GetString("AnUpdateIsAvailable", resourceCulture); return ResourceManager.GetString("AnUpdateIsAvailable", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Check for updates.
/// </summary>
public static string CheckForUpdates { public static string CheckForUpdates {
get { get {
return ResourceManager.GetString("CheckForUpdates", resourceCulture); return ResourceManager.GetString("CheckForUpdates", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Checking for Updates....
/// </summary>
public static string CheckingForUpdates { public static string CheckingForUpdates {
get { get {
return ResourceManager.GetString("CheckingForUpdates", resourceCulture); return ResourceManager.GetString("CheckingForUpdates", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Config incomplete or not loaded!.
/// </summary>
public static string ConfigIncompleteOrNotLoaded { public static string ConfigIncompleteOrNotLoaded {
get { get {
return ResourceManager.GetString("ConfigIncompleteOrNotLoaded", resourceCulture); return ResourceManager.GetString("ConfigIncompleteOrNotLoaded", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Downloading program update....
/// </summary>
public static string DownloadProgramUpdate { public static string DownloadProgramUpdate {
get { get {
return ResourceManager.GetString("DownloadProgramUpdate", resourceCulture); return ResourceManager.GetString("DownloadProgramUpdate", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Error on update check or while updating!.
/// </summary>
public static string ErrorOnUpdateCheckOrUpdating { public static string ErrorOnUpdateCheckOrUpdating {
get { get {
return ResourceManager.GetString("ErrorOnUpdateCheckOrUpdating", resourceCulture); return ResourceManager.GetString("ErrorOnUpdateCheckOrUpdating", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Everything is right and up-to-date..
/// </summary>
public static string EverythingIsRightAndUpToDate { public static string EverythingIsRightAndUpToDate {
get { get {
return ResourceManager.GetString("EverythingIsRightAndUpToDate", resourceCulture); return ResourceManager.GetString("EverythingIsRightAndUpToDate", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Install.
/// </summary>
public static string Install { public static string Install {
get { get {
return ResourceManager.GetString("Install", resourceCulture); return ResourceManager.GetString("Install", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Installation key.
/// </summary>
public static string InstallationKey { public static string InstallationKey {
get { get {
return ResourceManager.GetString("InstallationKey", resourceCulture); return ResourceManager.GetString("InstallationKey", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Installation key seems to be invalid.
/// </summary>
public static string InstallationKeyNotValid {
get {
return ResourceManager.GetString("InstallationKeyNotValid", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Installing....
/// </summary>
public static string Installing { public static string Installing {
get { get {
return ResourceManager.GetString("Installing", resourceCulture); return ResourceManager.GetString("Installing", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Minecraft profile.
/// </summary>
public static string MinecraftProfile { public static string MinecraftProfile {
get { get {
return ResourceManager.GetString("MinecraftProfile", resourceCulture); return ResourceManager.GetString("MinecraftProfile", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Minecraft profile folder seems to be not valid..
/// </summary>
public static string MinecraftProfileFolderSeemsInvalid { public static string MinecraftProfileFolderSeemsInvalid {
get { get {
return ResourceManager.GetString("MinecraftProfileFolderSeemsInvalid", resourceCulture); return ResourceManager.GetString("MinecraftProfileFolderSeemsInvalid", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Modpack config url.
/// </summary>
public static string ModpackConfigUrl { public static string ModpackConfigUrl {
get { get {
return ResourceManager.GetString("ModpackConfigUrl", resourceCulture); return ResourceManager.GetString("ModpackConfigUrl", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to No recent profiles available..
/// </summary>
public static string NoRecentProfilesAvailable {
get {
return ResourceManager.GetString("NoRecentProfilesAvailable", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Repair.
/// </summary>
public static string Repair { public static string Repair {
get { get {
return ResourceManager.GetString("Repair", resourceCulture); return ResourceManager.GetString("Repair", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Select.
/// </summary>
public static string Select { public static string Select {
get { get {
return ResourceManager.GetString("Select", resourceCulture); return ResourceManager.GetString("Select", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Select the minecraft profile folder (usually named .minecraft).
/// </summary>
public static string SelectMinecraftProfileFolder { public static string SelectMinecraftProfileFolder {
get { get {
return ResourceManager.GetString("SelectMinecraftProfileFolder", resourceCulture); return ResourceManager.GetString("SelectMinecraftProfileFolder", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Status.
/// </summary>
public static string Status { public static string Status {
get { get {
return ResourceManager.GetString("Status", resourceCulture); return ResourceManager.GetString("Status", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to The update servers are in maintenance..
/// </summary>
public static string UpdateServerInMaintenance { public static string UpdateServerInMaintenance {
get { get {
return ResourceManager.GetString("UpdateServerInMaintenance", resourceCulture); return ResourceManager.GetString("UpdateServerInMaintenance", resourceCulture);
} }
} }
public static string InstallationKeyNotValid {
get {
return ResourceManager.GetString("InstallationKeyNotValid", resourceCulture);
}
}
} }
} }

View File

@@ -174,4 +174,7 @@
<data name="InstallationKeyNotValid" xml:space="preserve"> <data name="InstallationKeyNotValid" xml:space="preserve">
<value>Installation key seems to be invalid</value> <value>Installation key seems to be invalid</value>
</data> </data>
<data name="NoRecentProfilesAvailable" xml:space="preserve">
<value>No recent profiles available.</value>
</data>
</root> </root>

View File

@@ -1,341 +0,0 @@
using System.Diagnostics;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using ModpackUpdater.Apps.Client.Gui.LangRes;
using ModpackUpdater.Manager;
using MsBox.Avalonia;
using MsBox.Avalonia.Enums;
using Pilz;
using Pilz.Extensions;
using Pilz.Runtime;
using Pilz.UI.Symbols;
using Pilz.Updating.Client;
namespace ModpackUpdater.Apps.Client.Gui;
public partial class MainForm : Window
{
private readonly UpdateCheckOptions updateOptions = new();
private ModpackInfo modpackInfo = new();
private ModpackConfig updateConfig = new();
private ModpackFeatures? features;
private UpdateCheckResult? lastUpdateCheckResult;
private bool currentUpdating;
private bool loadingData;
private int curOptionsRow = 3;
public MainForm()
{
InitializeComponent();
Title = $"{Title} (v{Assembly.GetExecutingAssembly().GetAppVersion().ToShortString()})";
Closing += MainForm_Closing;
Loaded += MainForm_Loaded;
ButtonSearchProfileFolder.ImageSource = AppGlobals.Symbols.GetImageSource(AppSymbols.opened_folder);
ButtonCheckForUpdates.ImageSource = AppGlobals.Symbols.GetImageSource(AppSymbols.update_done);
ButtonInstall.ImageSource = AppGlobals.Symbols.GetImageSource(AppSymbols.software_installer);
MenuItemRepair.Icon = AppGlobals.Symbols.GetImage(AppSymbols.wrench, SymbolSize.Small);
ClearStatus();
LoadProfileToUi();
}
#region Features
private void SetStatus(string statusText, IImage? image)
{
TextStatus.Text = statusText;
ImageStatus.Source = image;
}
private void ClearStatus()
{
TextStatus.Text = "-";
ImageStatus.Source = null;
}
private void LoadProfileToUi()
{
loadingData = true;
TextBoxMinecraftProfileFolder.Text = modpackInfo.LocalPath ?? AppConfig.Instance.LastMinecraftProfilePath ?? TextBoxMinecraftProfileFolder.Text;
TextBoxModpackConfig.Text = modpackInfo.ConfigUrl ?? TextBoxModpackConfig.Text;
TextBoxInstallKey.Text = modpackInfo.ExtrasKey ?? TextBoxInstallKey.Text;
Dispatcher.UIThread.Post(() => loadingData = false, DispatcherPriority.Background);
}
private void LoadOptionsToUi()
{
//foreach (var set in )
//{
// // ...
//}
}
private async void CheckStatusAndUpdate(bool loadProfileToUi)
{
if (CheckStatus(loadProfileToUi))
await ExecuteUpdate(false, false);
}
private bool CheckStatus(bool loadProfileToUi)
{
try
{
modpackInfo = ModpackInfo.TryLoad(TextBoxMinecraftProfileFolder.Text?.Trim());
}
catch
{
// Ignore
}
if (loadProfileToUi)
LoadProfileToUi();
try
{
updateConfig = ModpackConfig.LoadFromUrl(TextBoxModpackConfig.Text);
}
catch
{
// Ignore
}
features = new(updateConfig);
modpackInfo.ExtrasKey = TextBoxInstallKey.Text?.Trim();
if (!features.IsInvalid() && !string.IsNullOrWhiteSpace(TextBoxInstallKey.Text) && !AllowExtras())
{
SetStatus(GeneralLangRes.InstallationKeyNotValid, AppGlobals.Symbols.GetImageSource(AppSymbols.general_warning_sign));
return false;
}
LabelInstallKey.IsVisible = TextBoxInstallKey.IsVisible = !string.IsNullOrWhiteSpace(updateConfig.UnleashApiUrl);
if (string.IsNullOrWhiteSpace(TextBoxMinecraftProfileFolder.Text) /*|| modpackInfo.Valid*/)
{
SetStatus(GeneralLangRes.MinecraftProfileFolderSeemsInvalid, AppGlobals.Symbols.GetImageSource(AppSymbols.general_warning_sign));
ButtonCheckForUpdates.IsEnabled = false;
ButtonInstall.IsEnabled = false;
return false;
}
else if (string.IsNullOrWhiteSpace(TextBoxModpackConfig.Text))
{
SetStatus(GeneralLangRes.ConfigIncompleteOrNotLoaded, AppGlobals.Symbols.GetImageSource(AppSymbols.general_warning_sign));
ButtonCheckForUpdates.IsEnabled = false;
ButtonInstall.IsEnabled = false;
return false;
}
else if (updateConfig.Maintenance && !updateOptions.IgnoreMaintenance)
{
SetStatus(GeneralLangRes.UpdateServerInMaintenance, AppGlobals.Symbols.GetImageSource(AppSymbols.services));
ButtonCheckForUpdates.IsEnabled = false;
ButtonInstall.IsEnabled = false;
return false;
}
LoadOptionsToUi();
ButtonCheckForUpdates.IsEnabled = true;
ButtonInstall.IsEnabled = true;
return true;
}
private async Task ExecuteUpdate(bool doInstall, bool repair)
{
// Ensure set extras key
modpackInfo.ExtrasKey = TextBoxInstallKey.Text?.Trim();
var updater = new ModpackInstaller(updateConfig, modpackInfo);
updater.InstallProgessUpdated += Update_InstallProgessUpdated;
updater.CheckingProgressUpdated += Updated_CheckingProgresssUpdated;
void error()
{
SetStatus(GeneralLangRes.ErrorOnUpdateCheckOrUpdating, AppGlobals.Symbols.GetImageSource(AppSymbols.close));
currentUpdating = false;
}
void installing()
{
SetStatus(GeneralLangRes.Installing, AppGlobals.Symbols.GetImageSource(AppSymbols.software_installer));
currentUpdating = true;
}
void updatesAvailable()
{
SetStatus(GeneralLangRes.AnUpdateIsAvailable, AppGlobals.Symbols.GetImageSource(AppSymbols.software_installer));
}
void everythingOk()
{
SetStatus(GeneralLangRes.EverythingIsRightAndUpToDate, AppGlobals.Symbols.GetImageSource(AppSymbols.done));
currentUpdating = false;
}
// Check only if not pressed "install", not really needed otherwise.
if (lastUpdateCheckResult is null || !doInstall || repair)
{
SetStatus(GeneralLangRes.CheckingForUpdates, AppGlobals.Symbols.GetImageSource(AppSymbols.update_done));
// Check for extras once again
updateOptions.IncludeExtras = AllowExtras();
// Force re-install on repair
updateOptions.IgnoreInstalledVersion = repair;
try
{
lastUpdateCheckResult = await updater.Check(updateOptions);
}
catch
{
error();
if (Debugger.IsAttached)
throw;
}
}
// Error while update check
if (lastUpdateCheckResult is null || lastUpdateCheckResult.HasError)
{
error();
return;
}
// Load options
// lastUpdateCheckResult.OptionsAvailable...
// lastUpdateCheckResult.OptionsEnabled...
// No updates available
if (!lastUpdateCheckResult.HasUpdates)
{
everythingOk();
return;
}
// Updates available (but don't install)
if (!doInstall)
{
updatesAvailable();
return;
}
// Install updates
installing();
currentUpdating = true;
try
{
// Install
if (await updater.Install(lastUpdateCheckResult) == false)
{
error();
return;
}
// Success
lastUpdateCheckResult = null; // Reset last update check, a new one would be needed now.
everythingOk();
}
catch (Exception)
{
// Error
error();
if (Debugger.IsAttached)
throw;
}
}
private bool AllowExtras()
{
return features != null && features.IsEnabled(ModpackFeatures.FeatureAllowExtas, new AllowExtrasFeatureContext(modpackInfo));
}
private void Updated_CheckingProgresssUpdated(int toCheck, int processed)
{
SetStatus(Math.Round(processed / (double)toCheck * 100d, 1) + "%", AppGlobals.Symbols.GetImageSource(AppSymbols.update_done));
}
private void Update_InstallProgessUpdated(UpdateCheckResult result, int processedSyncs)
{
var actionCount = result.Actions.Count;
SetStatus(Math.Round(processedSyncs / (double)actionCount * 100d, 1) + "%", AppGlobals.Symbols.GetImageSource(AppSymbols.software_installer));
}
#endregion
#region Gui
private void MainForm_Closing(object? sender, WindowClosingEventArgs e)
{
AppConfig.Instance.LastMinecraftProfilePath = TextBoxMinecraftProfileFolder.Text?.Trim();
}
private async void MainForm_Loaded(object? sender, RoutedEventArgs e)
{
var updates = new AppUpdates(Program.UpdateUrl, this);
updates.OnDownloadProgramUpdate += (o, _) => SetStatus(GeneralLangRes.DownloadProgramUpdate, AppGlobals.Symbols.GetImageSource(AppSymbols.software_installer));
await updates.UpdateApp();
ClearStatus();
CheckStatusAndUpdate(true);
}
private void TextBoxMinecraftProfileFolder_TextChanged(object? o, TextChangedEventArgs args)
{
if (!loadingData)
CheckStatusAndUpdate(true);
}
private void TextBoxModpackConfig_TextChanged(object? o, RoutedEventArgs args)
{
if (!loadingData)
CheckStatusAndUpdate(false);
}
private void TextBoxInstallKey_TextChanged(object? o, RoutedEventArgs args)
{
if (!loadingData)
CheckStatusAndUpdate(false);
}
private async void ButtonSearchProfileFolder_Click(object? sender, RoutedEventArgs e)
{
var filePaths = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = GeneralLangRes.SelectMinecraftProfileFolder,
SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)),
AllowMultiple = false,
});
if (filePaths.Count >= 1)
TextBoxMinecraftProfileFolder.Text = filePaths[0].Path.AbsolutePath;
}
private async void ButtonCheckForUpdates_Click(object? sender, RoutedEventArgs e)
{
ClearStatus();
await ExecuteUpdate(false, false);
}
private async void ButtonInstall_Click(object? sender, RoutedEventArgs e)
{
if (currentUpdating)
return;
ClearStatus();
await ExecuteUpdate(true, false);
}
private async void MenuItemRepair_Click(object? sender, RoutedEventArgs e)
{
if (currentUpdating)
return;
ClearStatus();
await ExecuteUpdate(true, true);
}
#endregion
}

View File

@@ -4,15 +4,23 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:lang="clr-namespace:ModpackUpdater.Apps.Client.Gui.LangRes" xmlns:lang="clr-namespace:ModpackUpdater.Apps.Client.Gui.LangRes"
xmlns:pilz="https://git.pilzinsel64.de/pilz-framework/pilz" xmlns:pilz="https://git.pilzinsel64.de/pilz-framework/pilz"
xmlns:gui="clr-namespace:ModpackUpdater.Apps.Client.Gui"
xmlns:symbols="clr-namespace:Pilz.UI.Symbols;assembly=Pilz.UI" xmlns:symbols="clr-namespace:Pilz.UI.Symbols;assembly=Pilz.UI"
mc:Ignorable="d" mc:Ignorable="d"
x:Class="ModpackUpdater.Apps.Client.Gui.MainForm" x:Class="ModpackUpdater.Apps.Client.Gui.MainView"
x:DataType="gui:MainViewModel"
x:Name="window"
Width="520" Width="520"
SizeToContent="Height" SizeToContent="Height"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
CanMaximize="false" CanMaximize="false"
Title="Minecraft Modpack Updater" Title="Minecraft Modpack Updater"
Icon="/Assets/app.ico"> Icon="/Assets/app.ico"
Loaded="Control_OnLoaded">
<Design.DataContext>
<gui:MainViewModel/>
</Design.DataContext>
<Grid <Grid
RowDefinitions="Auto,Auto,Auto,Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto"
@@ -21,6 +29,13 @@
Margin="3" Margin="3"
x:Name="MainGrid"> x:Name="MainGrid">
<Grid.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="HasInitialized"/>
<Binding Path="!IsUpdating"/>
</MultiBinding>
</Grid.IsEnabled>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" MinWidth="250"/> <ColumnDefinition Width="*" MinWidth="250"/>
@@ -30,11 +45,11 @@
<!-- Labels --> <!-- Labels -->
<Label Grid.Row="0" Grid.Column="0" Margin="3" Content="{x:Static lang:GeneralLangRes.MinecraftProfile}" Target="TextBoxMinecraftProfileFolder"/> <Label Grid.Row="0" Grid.Column="0" Margin="3" Content="{x:Static lang:GeneralLangRes.MinecraftProfile}" Target="TextBoxMinecraftProfileFolder"/>
<Label Grid.Row="1" Grid.Column="0" Margin="3" Content="{x:Static lang:GeneralLangRes.ModpackConfigUrl}" Target="TextBoxModpackConfig"/> <Label Grid.Row="1" Grid.Column="0" Margin="3" Content="{x:Static lang:GeneralLangRes.ModpackConfigUrl}" Target="TextBoxModpackConfig"/>
<Label Grid.Row="2" Grid.Column="0" Margin="3" IsVisible="false" x:Name="LabelInstallKey" Content="{x:Static lang:GeneralLangRes.InstallationKey}" Target="TextBoxInstallKey"/> <Label Grid.Row="2" Grid.Column="0" Margin="3" IsVisible="{Binding CanUseExtrasKey}" Content="{x:Static lang:GeneralLangRes.InstallationKey}" Target="TextBoxInstallKey"/>
<TextBlock Grid.Row="3" Grid.Column="0" Margin="3" Text="{x:Static lang:GeneralLangRes.Status}"/> <TextBlock Grid.Row="3" Grid.Column="0" Margin="3" Text="{x:Static lang:GeneralLangRes.Status}"/>
<StackPanel Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Margin="3" Orientation="Horizontal" Spacing="6" MinHeight="{Binding MinHeight, ElementName=TextBoxMinecraftProfileFolder}"> <StackPanel Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Margin="3" Orientation="Horizontal" Spacing="6" MinHeight="{Binding MinHeight, ElementName=TextBoxMinecraftProfileFolder}">
<Image Width="{x:Static symbols:SymbolGlobals.DefaultImageSmallSize}" x:Name="ImageStatus"/> <Image Width="{x:Static symbols:SymbolGlobals.DefaultImageSmallSize}" Source="{Binding StatusImage}"/>
<TextBlock x:Name="TextStatus"/> <TextBlock Text="{Binding StatusText}"/>
</StackPanel> </StackPanel>
<!-- TextBoxes: Profile --> <!-- TextBoxes: Profile -->
@@ -45,7 +60,7 @@
Margin="3" Margin="3"
VerticalAlignment="Center" VerticalAlignment="Center"
Watermark="C:\..." Watermark="C:\..."
TextChanged="TextBoxMinecraftProfileFolder_TextChanged"/> Text="{Binding MinecraftProfileFolder}"/>
<!-- TextBoxes: ModpackConfig --> <!-- TextBoxes: ModpackConfig -->
<TextBox <TextBox
@@ -56,7 +71,7 @@
Margin="3" Margin="3"
VerticalAlignment="Center" VerticalAlignment="Center"
Watermark="https://..." Watermark="https://..."
TextChanged="TextBoxModpackConfig_TextChanged"/> Text="{Binding ModpackConfigUrl}"/>
<!-- TextBoxes: InstallKey --> <!-- TextBoxes: InstallKey -->
<TextBox <TextBox
@@ -67,11 +82,11 @@
Margin="3" Margin="3"
VerticalAlignment="Center" VerticalAlignment="Center"
Watermark="XXXXX-YYYYY-ZZZZZ-AAAAA-BBBBB" Watermark="XXXXX-YYYYY-ZZZZZ-AAAAA-BBBBB"
TextChanged="TextBoxInstallKey_TextChanged" Text="{Binding InstallKey}"
IsVisible="false"/> IsVisible="{Binding CanUseExtrasKey}"/>
<!-- Button: SearchProfileFolder --> <!-- Button: SearchProfileFolder -->
<pilz:ImageButton <pilz:ImageSplitButton
x:Name="ButtonSearchProfileFolder" x:Name="ButtonSearchProfileFolder"
Grid.Row="0" Grid.Row="0"
Grid.Column="2" Grid.Column="2"
@@ -79,7 +94,28 @@
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Text="{x:Static lang:GeneralLangRes.Select}" Text="{x:Static lang:GeneralLangRes.Select}"
Click="ButtonSearchProfileFolder_Click"/> Click="ButtonSearchProfileFolder_Click">
<pilz:ImageSplitButton.DataTemplates>
<DataTemplate DataType="gui:MainViewModel+EmptyRecentFilesItem">
<TextBlock Text="{x:Static lang:GeneralLangRes.NoRecentProfilesAvailable}" FontStyle="Italic"/>
</DataTemplate>
<DataTemplate DataType="gui:MainViewModel+RecentFilesItem">
<MenuItem Header="{Binding Display}" Command="{Binding DataContext.OpenRecentPathCommand, ElementName=window}" CommandParameter="{Binding Path}" />
</DataTemplate>
</pilz:ImageSplitButton.DataTemplates>
<pilz:ImageSplitButton.Flyout>
<MenuFlyout ItemsSource="{Binding RecentMinecraftProfilePathItems}">
<MenuFlyout.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding}"/>
</DataTemplate>
</MenuFlyout.ItemTemplate>
</MenuFlyout>
</pilz:ImageSplitButton.Flyout>
</pilz:ImageSplitButton>
<!-- Button: CheckForUpdates --> <!-- Button: CheckForUpdates -->
<pilz:ImageButton <pilz:ImageButton
@@ -90,7 +126,8 @@
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Text="{x:Static lang:GeneralLangRes.CheckForUpdates}" Text="{x:Static lang:GeneralLangRes.CheckForUpdates}"
Click="ButtonCheckForUpdates_Click"/> Command="{Binding CheckForUpdatesCommand}"
IsEnabled="{Binding CanUpdate}"/>
<!-- Button: Install --> <!-- Button: Install -->
<pilz:ImageSplitButton <pilz:ImageSplitButton
@@ -102,10 +139,12 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center" HorizontalContentAlignment="Center"
Text="{x:Static lang:GeneralLangRes.Install}" Text="{x:Static lang:GeneralLangRes.Install}"
Click="ButtonInstall_Click"> Command="{Binding InstallCommand}"
IsEnabled="{Binding CanUpdate}">
<SplitButton.Flyout> <SplitButton.Flyout>
<MenuFlyout> <MenuFlyout>
<MenuItem x:Name="MenuItemRepair" Header="{x:Static lang:GeneralLangRes.Repair}" Click="MenuItemRepair_Click"/> <MenuItem x:Name="MenuItemRepair" Header="{x:Static lang:GeneralLangRes.Repair}" Command="{Binding RepairCommand}"/>
</MenuFlyout> </MenuFlyout>
</SplitButton.Flyout> </SplitButton.Flyout>
</pilz:ImageSplitButton> </pilz:ImageSplitButton>

View File

@@ -0,0 +1,51 @@
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using ModpackUpdater.Apps.Client.Gui.LangRes;
using Pilz.Extensions;
using Pilz.UI.Symbols;
namespace ModpackUpdater.Apps.Client.Gui;
public partial class MainView : Window
{
public MainViewModel Model => DataContext as MainViewModel ?? throw new NullReferenceException();
public MainView()
{
InitializeComponent();
Title = $"{Title} (v{Assembly.GetExecutingAssembly().GetAppVersion().ToShortHumanString()})";
ButtonSearchProfileFolder.ImageSource = AppGlobals.Symbols.GetImageSource(AppSymbols.opened_folder);
ButtonCheckForUpdates.ImageSource = AppGlobals.Symbols.GetImageSource(AppSymbols.update_done);
ButtonInstall.ImageSource = AppGlobals.Symbols.GetImageSource(AppSymbols.software_installer);
MenuItemRepair.Icon = AppGlobals.Symbols.GetImage(AppSymbols.wrench, SymbolSize.Small);
}
private async void InitializeViewModel()
{
await Model.CheckForUpdates(this);
await Model.Initialize();
}
private void Control_OnLoaded(object? sender, RoutedEventArgs e)
{
Dispatcher.UIThread.Post(InitializeViewModel, DispatcherPriority.Background);
}
private async void ButtonSearchProfileFolder_Click(object? sender, RoutedEventArgs e)
{
var filePaths = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = GeneralLangRes.SelectMinecraftProfileFolder,
SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)),
AllowMultiple = false,
});
if (filePaths.Count >= 1)
Model.MinecraftProfileFolder = filePaths[0].Path.AbsolutePath;
}
}

View File

@@ -0,0 +1,340 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows.Input;
using Avalonia.Controls;
using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ModpackUpdater.Apps.Client.Gui.LangRes;
using ModpackUpdater.Manager;
using Pilz.Extensions.Collections;
namespace ModpackUpdater.Apps.Client.Gui;
public partial class MainViewModel : ObservableObject
{
public class EmptyRecentFilesItem;
public class RecentFilesItem(string path)
{
public string Path => path;
public string Display => path.Length > 50 ? $"...{path[^50..]}" : path;
}
private readonly UpdateCheckOptions updateOptions = new();
private ModpackInfo modpackInfo = new();
private ModpackConfig updateConfig = new();
private UpdateCheckResult? lastUpdateCheckResult;
[ObservableProperty] private string? minecraftProfileFolder;
[ObservableProperty] private string? modpackConfigUrl;
[ObservableProperty] private string? installKey;
[ObservableProperty] private string statusText = "-";
[ObservableProperty] private IImage? statusImage;
[ObservableProperty] private bool hasInitialized;
[ObservableProperty] private bool isUpdating;
[ObservableProperty] private bool loadingData;
[ObservableProperty] private bool canUpdate;
[ObservableProperty] private bool canUseExtrasKey;
public ObservableCollection<object> RecentMinecraftProfilePathItems { get; } = [];
public ICommand CheckForUpdatesCommand { get; }
public ICommand InstallCommand { get; }
public ICommand RepairCommand { get; }
public MainViewModel()
{
CheckForUpdatesCommand = new AsyncRelayCommand(async () => await CheckStatusAndUpdate(false));
InstallCommand = new AsyncRelayCommand(async () => await ExecuteUpdate(true, false));
RepairCommand = new AsyncRelayCommand(async () => await ExecuteUpdate(true, true));
}
public async Task CheckForUpdates(Window parent)
{
var updates = new AppUpdates("client", parent);
updates.OnDownloadProgramUpdate += (_, _) => SetStatus(GeneralLangRes.DownloadProgramUpdate, AppGlobals.Symbols.GetImageSource(AppSymbols.software_installer));
await updates.UpdateApp();
}
public async Task Initialize()
{
ClearStatus();
LoadProfileToUi();
LoadRecentFilesToUi();
HasInitialized = true;
await CheckStatusAndUpdate(true);
}
partial void OnMinecraftProfileFolderChanged(string? value)
{
if (!LoadingData)
_ = CheckStatusAndUpdate(true);
}
partial void OnModpackConfigUrlChanged(string? value)
{
if (!LoadingData)
_ = CheckStatusAndUpdate(false);
}
partial void OnInstallKeyChanged(string? value)
{
if (!LoadingData)
_ = CheckStatusAndUpdate(false);
}
public void SetStatus(string statusText, IImage? image)
{
StatusText = statusText;
StatusImage = image;
}
public void ClearStatus()
{
StatusText = "-";
StatusImage = null;
}
private bool AllowExtras()
{
return !string.IsNullOrWhiteSpace(modpackInfo.ExtrasKey) && updateConfig.ExtrasKeys.Contains(modpackInfo.ExtrasKey);
}
public void LoadProfileToUi()
{
LoadingData = true;
MinecraftProfileFolder = modpackInfo.LocalPath ?? AppConfig.Instance.RecentMinecraftProfilePaths.FirstOrDefault() ?? MinecraftProfileFolder;
ModpackConfigUrl = modpackInfo.ConfigUrl ?? ModpackConfigUrl;
InstallKey = modpackInfo.ExtrasKey ?? InstallKey;
LoadingData = false;
}
private void LoadOptionsToUi()
{
//foreach (var set in )
//{
// // ...
//}
}
public void LoadRecentFilesToUi()
{
RecentMinecraftProfilePathItems.Clear();
if (AppConfig.Instance.RecentMinecraftProfilePaths.Count == 0)
{
RecentMinecraftProfilePathItems.Add(new EmptyRecentFilesItem());
return;
}
AppConfig.Instance.RecentMinecraftProfilePaths.ForEach(path =>
{
if (Directory.Exists(path))
RecentMinecraftProfilePathItems.Add(new RecentFilesItem(path));
});
}
private void StoreRecentMinecraftProfilePath(string? path)
{
if (string.IsNullOrWhiteSpace(path))
return;
AppConfig.Instance.RecentMinecraftProfilePaths.RemoveAll(n => n == path);
AppConfig.Instance.RecentMinecraftProfilePaths.Insert(0, path);
AppConfig.Instance.RecentMinecraftProfilePaths.Skip(10).ForEach(n => AppConfig.Instance.RecentMinecraftProfilePaths.Remove(n));
}
[RelayCommand]
private void OpenRecentPath(string path)
{
if (!string.IsNullOrWhiteSpace(path))
MinecraftProfileFolder = path;
}
public async Task CheckStatusAndUpdate(bool loadProfileToUi)
{
if (!CheckStatus(loadProfileToUi))
return;
await ExecuteUpdate(false, false);
StoreRecentMinecraftProfilePath(modpackInfo.LocalPath);
LoadRecentFilesToUi();
}
private bool CheckStatus(bool loadProfileToUi)
{
ClearStatus();
try
{
modpackInfo = ModpackInfo.TryLoad(MinecraftProfileFolder?.Trim());
}
catch
{
// Ignore
}
if (loadProfileToUi)
LoadProfileToUi();
try
{
updateConfig = ModpackConfig.LoadFromUrl(ModpackConfigUrl);
}
catch
{
// Ignore
}
modpackInfo.ExtrasKey = InstallKey?.Trim();
if (!string.IsNullOrWhiteSpace(modpackInfo.ExtrasKey) && !AllowExtras())
{
SetStatus(GeneralLangRes.InstallationKeyNotValid, AppGlobals.Symbols.GetImageSource(AppSymbols.general_warning_sign));
return false;
}
CanUseExtrasKey = updateConfig.ExtrasKeys.Count > 0;
if (string.IsNullOrWhiteSpace(MinecraftProfileFolder) /*|| modpackInfo.Valid*/)
{
SetStatus(GeneralLangRes.MinecraftProfileFolderSeemsInvalid, AppGlobals.Symbols.GetImageSource(AppSymbols.general_warning_sign));
CanUpdate = false;
return false;
}
else if (string.IsNullOrWhiteSpace(ModpackConfigUrl))
{
SetStatus(GeneralLangRes.ConfigIncompleteOrNotLoaded, AppGlobals.Symbols.GetImageSource(AppSymbols.general_warning_sign));
CanUpdate = false;
return false;
}
else if (updateConfig.Maintenance && !updateOptions.IgnoreMaintenance)
{
SetStatus(GeneralLangRes.UpdateServerInMaintenance, AppGlobals.Symbols.GetImageSource(AppSymbols.services));
CanUpdate = false;
return false;
}
LoadOptionsToUi();
CanUpdate = true;
return true;
}
private async Task ExecuteUpdate(bool doInstall, bool repair)
{
ClearStatus();
// Ensure set extras key
modpackInfo.ExtrasKey = InstallKey?.Trim();
var updater = new ModpackInstaller(updateConfig, modpackInfo);
updater.InstallProgessUpdated += Updater_InstallProgressUpdated;
updater.CheckingProgressUpdated += Updater_CheckingProgressUpdated;
void error()
{
SetStatus(GeneralLangRes.ErrorOnUpdateCheckOrUpdating, AppGlobals.Symbols.GetImageSource(AppSymbols.close));
IsUpdating = false;
}
void installing()
{
SetStatus(GeneralLangRes.Installing, AppGlobals.Symbols.GetImageSource(AppSymbols.software_installer));
IsUpdating = true;
}
void updatesAvailable()
{
SetStatus(GeneralLangRes.AnUpdateIsAvailable, AppGlobals.Symbols.GetImageSource(AppSymbols.software_installer));
IsUpdating = false;
}
void everythingOk()
{
SetStatus(GeneralLangRes.EverythingIsRightAndUpToDate, AppGlobals.Symbols.GetImageSource(AppSymbols.done));
IsUpdating = false;
}
// Check only if not pressed "install", not really needed otherwise.
if (lastUpdateCheckResult is null || !doInstall || repair)
{
SetStatus(GeneralLangRes.CheckingForUpdates, AppGlobals.Symbols.GetImageSource(AppSymbols.update_done));
// Check for extras once again
updateOptions.IncludeExtras = AllowExtras();
// Force re-install on repair
updateOptions.IgnoreInstalledVersion = repair;
try
{
lastUpdateCheckResult = await updater.Check(updateOptions);
}
catch
{
error();
if (Debugger.IsAttached)
throw;
}
}
// Error while update check
if (lastUpdateCheckResult is null || lastUpdateCheckResult.HasError)
{
error();
return;
}
// Load options
// lastUpdateCheckResult.OptionsAvailable...
// lastUpdateCheckResult.OptionsEnabled...
// No updates available
if (!lastUpdateCheckResult.HasUpdates)
{
everythingOk();
return;
}
// Updates available (but don't install)
if (!doInstall)
{
updatesAvailable();
return;
}
// Install updates
installing();
IsUpdating = true;
try
{
// Install
if (await updater.Install(lastUpdateCheckResult) == false)
{
error();
return;
}
// Success
lastUpdateCheckResult = null; // Reset last update check, a new one would be needed now.
everythingOk();
}
catch
{
// Error
error();
if (Debugger.IsAttached)
throw;
}
}
private void Updater_CheckingProgressUpdated(int toCheck, int processed)
{
SetStatus(Math.Round(processed / (double)toCheck * 100d, 1) + "%", AppGlobals.Symbols.GetImageSource(AppSymbols.update_done));
}
private void Updater_InstallProgressUpdated(UpdateCheckResult result, int processedSyncs)
{
var actionCount = result.Actions.Count;
SetStatus(Math.Round(processedSyncs / (double)actionCount * 100d, 1) + "%", AppGlobals.Symbols.GetImageSource(AppSymbols.software_installer));
}
}

View File

@@ -66,27 +66,28 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MessageBox.Avalonia" Version="3.3.0" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="MessageBox.Avalonia" Version="3.3.1" />
<PackageReference Include="Mono.Options" Version="6.12.0.148" /> <PackageReference Include="Mono.Options" Version="6.12.0.148" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Pilz" Version="2.6.1" /> <PackageReference Include="Pilz" Version="2.7.8" />
<PackageReference Include="Pilz.Configuration" Version="3.2.7" /> <PackageReference Include="Pilz.Configuration" Version="3.2.8" />
<PackageReference Include="Pilz.Cryptography" Version="2.1.2" /> <PackageReference Include="Pilz.Cryptography" Version="2.1.3" />
<PackageReference Include="Pilz.IO" Version="2.1.0" /> <PackageReference Include="Pilz.IO" Version="2.1.1" />
<PackageReference Include="Pilz.UI" Version="3.1.4" /> <PackageReference Include="Pilz.UI" Version="3.1.5" />
<PackageReference Include="Pilz.UI.AvaloniaUI" Version="1.2.18" /> <PackageReference Include="Pilz.UI.AvaloniaUI" Version="1.2.21" />
<PackageReference Include="Avalonia" Version="11.3.8" /> <PackageReference Include="Avalonia" Version="11.3.10" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.8" /> <PackageReference Include="Avalonia.Desktop" Version="11.3.10" />
<PackageReference Include="Avalonia.Svg" Version="11.3.0" /> <PackageReference Include="Avalonia.Svg" Version="11.3.0" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.8" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.10" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.8" /> <PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.10" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.8"> <PackageReference Include="Avalonia.Diagnostics" Version="11.3.10">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets> <IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets> <PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Pilz.Updating" Version="4.3.5" /> <PackageReference Include="Pilz.Updating" Version="4.3.6" />
<PackageReference Include="Pilz.Updating.Client" Version="4.4.6" /> <PackageReference Include="Pilz.Updating.Client" Version="4.4.8" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -8,8 +8,6 @@ namespace ModpackUpdater.Apps.Client.Gui;
public static class Program public static class Program
{ {
public const string UpdateUrl = "https://git.pilzinsel64.de/litw-refined/minecraft-modpack-updater/-/snippets/3/raw/main/updates-new.json";
private static readonly SettingsManager settingsManager; private static readonly SettingsManager settingsManager;
public static ISettings Settings => settingsManager.Instance; public static ISettings Settings => settingsManager.Instance;
@@ -18,7 +16,6 @@ public static class Program
static Program() static Program()
{ {
settingsManager = new(GetSettingsPath(2), true); settingsManager = new(GetSettingsPath(2), true);
MigrateLegacySettings(GetSettingsPath(null));
} }
[STAThread] [STAThread]
@@ -47,25 +44,4 @@ public static class Program
return settingsPath; return settingsPath;
} }
private static void MigrateLegacySettings(string settingsPath)
{
// Try load legacy config file
if (!File.Exists(settingsPath) || JsonConvert.DeserializeObject<AppConfig>(File.ReadAllText(settingsPath)) is not AppConfig legacyConfig)
return;
// Migrate
var newConfig = Settings.Get<AppConfig>();
newConfig.LastMinecraftProfilePath = legacyConfig.LastMinecraftProfilePath;
if (ModpackInfo.TryLoad(legacyConfig.LastMinecraftProfilePath) is ModpackInfo info)
#pragma warning disable CS0612 // Typ oder Element ist veraltet
info.ConfigUrl = legacyConfig.ConfigFilePath;
// Ensure save settings
settingsManager.Save();
// Delete legacy config file
File.Delete(settingsPath);
}
} }

View File

@@ -1,12 +0,0 @@
pupnet -y -v "$1[1]" -r linux-x64 -k appimage
#pupnet -y -v "$1[1]" -r linux-x64 -k flatpak -p DefineConstants=DISABLE_UPDATE
#pupnet -y -v "$1[1]"-r linux-x64 -k deb -p DefineConstants=DISABLE_UPDATE
#pupnet -y -v "$1[1]"-r linux-x64 -k rpm -p DefineConstants=DISABLE_UPDATE
pupnet -y -v "$1[1]" -r linux-arm64 -k appimage
#pupnet -y -v "$1[1]" -r linux-arm64 -k flatpak -p DefineConstants=DISABLE_UPDATE
#pupnet -y -v "$1[1]"-r linux-arm64 -k deb -p DefineConstants=DISABLE_UPDATE
#pupnet -y -v "$1[1]"-r linux-arm64 -k rpm -p DefineConstants=DISABLE_UPDATE
pupnet -y -v "$1[1]" -r win-x64 -k zip
pupnet -y -v "$1[1]" -r win-arm64 -k zip
pupnet -y -v "$1[1]" -r osx-x64 -k zip
pupnet -y -v "$1[1]" -r osx-arm64 -k zip

View File

@@ -23,9 +23,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Mono.Options" Version="6.12.0.148" /> <PackageReference Include="Mono.Options" Version="6.12.0.148" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Pilz" Version="2.6.1" /> <PackageReference Include="Pilz" Version="2.7.8" />
<PackageReference Include="Pilz.Cryptography" Version="2.1.2" /> <PackageReference Include="Pilz.Cryptography" Version="2.1.3" />
<PackageReference Include="Pilz.IO" Version="2.1.0" /> <PackageReference Include="Pilz.IO" Version="2.1.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,5 +1,7 @@
using Castle.Core.Logging; using System.Reflection;
using Castle.Core.Logging;
using ModpackUpdater.Manager; using ModpackUpdater.Manager;
using Pilz.Extensions;
namespace ModpackUpdater.Apps.Client; namespace ModpackUpdater.Apps.Client;
@@ -15,22 +17,34 @@ public static class Program
{ {
Options = new Options(args); Options = new Options(args);
if (Options.Help) if (Options.Help)
{
DrawInfo();
Options.DrawHelp(); Options.DrawHelp();
else return;
}
if (!Options.Silent)
DrawInfo();
InstallWithoutGui(Options.UpdateOptions, Options.Silent); InstallWithoutGui(Options.UpdateOptions, Options.Silent);
} }
private static void DrawInfo()
{
Console.WriteLine("Minecraft Modpack Updater CLI");
Console.WriteLine("Version " + Assembly.GetExecutingAssembly().GetAppVersion().ToShortHumanString());
Console.WriteLine("------------------------------");
}
private static void InstallWithoutGui(UpdateCheckOptionsAdv updateOptions, bool silent) private static void InstallWithoutGui(UpdateCheckOptionsAdv updateOptions, bool silent)
{ {
var info = ModpackInfo.TryLoad(updateOptions.ProfileFolder); var info = ModpackInfo.TryLoad(updateOptions.ProfileFolder);
var config = ModpackConfig.LoadFromUrl(CheckModpackConfigUrl(updateOptions.ModpackConfig!, info)); var config = ModpackConfig.LoadFromUrl(CheckModpackConfigUrl(updateOptions.ModpackConfig!, info));
var features = new ModpackFeatures(config);
// Check features // Check features
if (!string.IsNullOrWhiteSpace(updateOptions.ExtrasKey)) if (!string.IsNullOrWhiteSpace(updateOptions.ExtrasKey))
info.ExtrasKey = updateOptions.ExtrasKey; info.ExtrasKey = updateOptions.ExtrasKey;
if (!string.IsNullOrWhiteSpace(info.ExtrasKey)) if (!string.IsNullOrWhiteSpace(info.ExtrasKey))
updateOptions.IncludeExtras = features.IsEnabled(ModpackFeatures.FeatureAllowExtas, new AllowExtrasFeatureContext(info)); updateOptions.IncludeExtras = !string.IsNullOrWhiteSpace(info.ExtrasKey) && config.ExtrasKeys.Contains(info.ExtrasKey);
// Check for update // Check for update
var installer = new ModpackInstaller(config, info) var installer = new ModpackInstaller(config, info)

View File

@@ -1,12 +0,0 @@
pupnet -y -v "$1[1]" -r linux-x64 -k appimage
#pupnet -y -v "$1[1]" -r linux-x64 -k flatpak
#pupnet -y -v "$1[1]"-r linux-x64 -k deb
#pupnet -y -v "$1[1]"-r linux-x64 -k rpm
pupnet -y -v "$1[1]" -r linux-arm64 -k appimage
#pupnet -y -v "$1[1]" -r linux-arm64 -k flatpak
#pupnet -y -v "$1[1]"-r linux-arm64 -k deb
#pupnet -y -v "$1[1]"-r linux-arm64 -k rpm
pupnet -y -v "$1[1]" -r win-x64 -k zip
pupnet -y -v "$1[1]" -r win-arm64 -k zip
pupnet -y -v "$1[1]" -r osx-x64 -k zip
pupnet -y -v "$1[1]" -r osx-arm64 -k zip

View File

@@ -70,7 +70,7 @@ internal static class SharedFunctions
foreach (var update in updates) foreach (var update in updates)
{ {
var sourceTag = update.AvailableVersions[update.NewVersion].Tag; var sourceTag = update.AvailableVersions[update.NewVersion].Tag;
if (api.Model.CurrentGridRows?.FirstOrDefault(n => n.Action == update.Origin) is { } row) if (api.Model.CurrentGridRows.List.Items.FirstOrDefault(n => n.Action == update.Origin) is { } row)
row.SourceTag = sourceTag; row.SourceTag = sourceTag;
else else
update.Origin.SourceTag = sourceTag; update.Origin.SourceTag = sourceTag;

View File

@@ -17,10 +17,10 @@ internal class CheckAllActionsHealthyFeature : PluginFunction, IPluginFeaturePro
protected override async Task<object?> ExecuteFunctionAsync(PluginFunctionParameter? @params) protected override async Task<object?> ExecuteFunctionAsync(PluginFunctionParameter? @params)
{ {
if (@params is not MainApiParameters p || p.Api.Model.CurrentGridRows is null) if (@params is not MainApiParameters p)
return null; return null;
await SharedFunctions.CheckActionHealthy(p.Api, [.. p.Api.Model.CurrentGridRows]); await SharedFunctions.CheckActionHealthy(p.Api, [.. p.Api.Model.CurrentGridRows.View]);
return null; return null;
} }

View File

@@ -17,8 +17,8 @@ internal class ClearDirectLinksFeature : PluginFunction, IPluginFeatureProvider<
protected override Task<object?> ExecuteFunctionAsync(PluginFunctionParameter? @params) protected override Task<object?> ExecuteFunctionAsync(PluginFunctionParameter? @params)
{ {
if (@params is MainApiParameters p && p.Api.Model.CurrentGridRows is not null) if (@params is MainApiParameters p)
SharedFunctions.ClearDirectLinks(p.Api, [.. p.Api.Model.CurrentGridRows]); SharedFunctions.ClearDirectLinks(p.Api, [.. p.Api.Model.CurrentGridRows.View]);
return Task.FromResult<object?>(null); return Task.FromResult<object?>(null);
} }
} }

View File

@@ -17,10 +17,10 @@ internal class UpdateDirectLinksFeature : PluginFunction, IPluginFeatureProvider
protected override async Task<object?> ExecuteFunctionAsync(PluginFunctionParameter? @params) protected override async Task<object?> ExecuteFunctionAsync(PluginFunctionParameter? @params)
{ {
if (@params is not MainApiParameters p || p.Api.Model.CurrentGridRows is null) if (@params is not MainApiParameters p)
return null; return null;
await SharedFunctions.FindNewDirectLinks(p.Api, [.. p.Api.Model.CurrentGridRows]); await SharedFunctions.FindNewDirectLinks(p.Api, [.. p.Api.Model.CurrentGridRows.View]);
return null; return null;
} }

View File

@@ -273,6 +273,12 @@ namespace ModpackUpdater.Apps.Manager.LangRes {
} }
} }
public static string SourceTag {
get {
return ResourceManager.GetString("SourceTag", resourceCulture);
}
}
public static string SourceRegex { public static string SourceRegex {
get { get {
return ResourceManager.GetString("SourceRegex", resourceCulture); return ResourceManager.GetString("SourceRegex", resourceCulture);
@@ -314,5 +320,11 @@ namespace ModpackUpdater.Apps.Manager.LangRes {
return ResourceManager.GetString("SelectRootFolder", resourceCulture); return ResourceManager.GetString("SelectRootFolder", resourceCulture);
} }
} }
public static string Search {
get {
return ResourceManager.GetString("Search", resourceCulture);
}
}
} }
} }

View File

@@ -231,6 +231,9 @@
<data name="SourceName" xml:space="preserve"> <data name="SourceName" xml:space="preserve">
<value>Source name</value> <value>Source name</value>
</data> </data>
<data name="SourceTag" xml:space="preserve">
<value>Source tag</value>
</data>
<data name="SourceRegex" xml:space="preserve"> <data name="SourceRegex" xml:space="preserve">
<value>Source RegEx</value> <value>Source RegEx</value>
</data> </data>
@@ -252,4 +255,7 @@
<data name="SelectRootFolder" xml:space="preserve"> <data name="SelectRootFolder" xml:space="preserve">
<value>Select root folder</value> <value>Select root folder</value>
</data> </data>
<data name="Search" xml:space="preserve">
<value>Search</value>
</data>
</root> </root>

View File

@@ -49,24 +49,25 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.8" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.10" />
<PackageReference Include="MessageBox.Avalonia" Version="3.3.0" /> <PackageReference Include="DynamicData" Version="9.4.1" />
<PackageReference Include="EPPlus" Version="8.2.1" /> <PackageReference Include="MessageBox.Avalonia" Version="3.3.1" />
<PackageReference Include="NGitLab" Version="11.0.1" /> <PackageReference Include="EPPlus" Version="8.4.0" />
<PackageReference Include="Pilz" Version="2.6.1" /> <PackageReference Include="NGitLab" Version="11.1.0" />
<PackageReference Include="Pilz.Configuration" Version="3.2.7" /> <PackageReference Include="Pilz" Version="2.7.8" />
<PackageReference Include="Pilz.Cryptography" Version="2.1.2" /> <PackageReference Include="Pilz.Configuration" Version="3.2.8" />
<PackageReference Include="Pilz.Features" Version="2.13.0" /> <PackageReference Include="Pilz.Cryptography" Version="2.1.3" />
<PackageReference Include="Pilz.UI" Version="3.1.4" /> <PackageReference Include="Pilz.Features" Version="2.13.1" />
<PackageReference Include="Pilz.UI.AvaloniaUI" Version="1.2.18" /> <PackageReference Include="Pilz.UI" Version="3.1.5" />
<PackageReference Include="Pilz.UI.AvaloniaUI.Features" Version="1.0.1" /> <PackageReference Include="Pilz.UI.AvaloniaUI" Version="1.2.21" />
<PackageReference Include="Avalonia" Version="11.3.8" /> <PackageReference Include="Pilz.UI.AvaloniaUI.Features" Version="1.0.2" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.8" /> <PackageReference Include="Avalonia" Version="11.3.10" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.10" />
<PackageReference Include="Avalonia.Svg" Version="11.3.0" /> <PackageReference Include="Avalonia.Svg" Version="11.3.0" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.8" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.10" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.8" /> <PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.10" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.8"> <PackageReference Include="Avalonia.Diagnostics" Version="11.3.10">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets> <IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets> <PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference> </PackageReference>
@@ -142,10 +143,4 @@
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Reference Include="Pilz">
<HintPath>..\..\..\.nuget\packages\pilz\2.6.1\lib\net8.0\Pilz.dll</HintPath>
</Reference>
</ItemGroup>
</Project> </Project>

View File

@@ -7,8 +7,6 @@ namespace ModpackUpdater.Apps.Manager;
public static class Program public static class Program
{ {
public const string UpdateUrl = "https://git.pilzinsel64.de/litw-refined/minecraft-modpack-updater/-/snippets/3/raw/main/updates-manager.json";
internal static readonly SettingsManager settingsManager; internal static readonly SettingsManager settingsManager;
public static ISettings Settings => settingsManager.Instance; public static ISettings Settings => settingsManager.Instance;

View File

@@ -3,12 +3,11 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ModpackUpdater.Apps.Manager.Ui"
xmlns:pilz="https://git.pilzinsel64.de/pilz-framework/pilz" xmlns:pilz="https://git.pilzinsel64.de/pilz-framework/pilz"
xmlns:symbols="clr-namespace:Pilz.UI.Symbols;assembly=Pilz.UI" xmlns:symbols="clr-namespace:Pilz.UI.Symbols;assembly=Pilz.UI"
xmlns:mainWindow="clr-namespace:ModpackUpdater.Apps.Manager.Ui.Models.MainWindow" xmlns:mainWindow="clr-namespace:ModpackUpdater.Apps.Manager.Ui.Models.MainWindow"
xmlns:langRes="clr-namespace:ModpackUpdater.Apps.Manager.LangRes" xmlns:langRes="clr-namespace:ModpackUpdater.Apps.Manager.LangRes"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="450"
x:Class="ModpackUpdater.Apps.Manager.Ui.MainWindow" x:Class="ModpackUpdater.Apps.Manager.Ui.MainWindow"
Title="Minecraft Modpack Manager" Title="Minecraft Modpack Manager"
WindowState="Maximized" WindowState="Maximized"
@@ -25,54 +24,44 @@
ColumnSpacing="6" ColumnSpacing="6"
Margin="3"> Margin="3">
<!-- StackPanel: Workspace --> <!-- Menu: Workspace -->
<StackPanel <Menu
Grid.Column="0" Grid.Column="0"
Grid.Row="0" Grid.Row="0">
Orientation="Horizontal">
<!-- Button: Workspace --> <Menu.Items>
<pilz:ImageButton <!-- MenuItem: Workspace -->
x:Name="ButtonWorkspace" <pilz:HeaderMenuItem
Text="{x:Static langRes:GeneralLangRes.Workspace}" x:Name="MenuItemWorkspace"
Background="Transparent"> HeaderText="{x:Static langRes:GeneralLangRes.Workspace}">
<pilz:ImageButton.Flyout> <pilz:HeaderMenuItem.Items>
<MenuFlyout>
<MenuFlyout.Items>
<MenuItem x:Name="MenuItemWorkspacePreferences" Header="{x:Static langRes:GeneralLangRes.WorkspacePreferences}" Click="MenuItemWorkspacePreferences_OnClick"/> <MenuItem x:Name="MenuItemWorkspacePreferences" Header="{x:Static langRes:GeneralLangRes.WorkspacePreferences}" Click="MenuItemWorkspacePreferences_OnClick"/>
<MenuItem x:Name="MenuItemSaveWorkspace" Header="{x:Static langRes:GeneralLangRes.SaveWorkspace}" HotKey="Ctrl+S" Click="MenuItemSaveWorkspace_OnClick"/> <MenuItem x:Name="MenuItemSaveWorkspace" Header="{x:Static langRes:GeneralLangRes.SaveWorkspace}" HotKey="Ctrl+S" Click="MenuItemSaveWorkspace_OnClick"/>
<Separator/> <Separator/>
<MenuItem x:Name="MenuItemNewWorkspace" Header="{x:Static langRes:GeneralLangRes.NewWorkspace}"/> <MenuItem x:Name="MenuItemNewWorkspace" Header="{x:Static langRes:GeneralLangRes.NewWorkspace}"/>
<Separator/> <Separator/>
<MenuItem x:Name="MenuItemRecentWorkspaces" Header="{x:Static langRes:GeneralLangRes.RecentWorkspaces}"/> <MenuItem x:Name="MenuItemRecentWorkspaces" Header="{x:Static langRes:GeneralLangRes.RecentWorkspaces}"/>
</MenuFlyout.Items> </pilz:HeaderMenuItem.Items>
</MenuFlyout> </pilz:HeaderMenuItem>
</pilz:ImageButton.Flyout>
</pilz:ImageButton>
<!-- Button: Update --> <!-- MenuItem: Update -->
<pilz:ImageButton <pilz:HeaderMenuItem
x:Name="ButtonUpdate" x:Name="MenuItemUpdate"
Text="{x:Static langRes:GeneralLangRes.Update}" HeaderText="{x:Static langRes:GeneralLangRes.Update}">
Background="Transparent">
<pilz:ImageButton.Flyout> <pilz:HeaderMenuItem.Items>
<MenuFlyout>
<MenuFlyout.Items>
<MenuItem x:Name="MenuItemCreateUpdate" Header="{x:Static langRes:GeneralLangRes.CreateUpdate}" Click="MenuItemCreateUpdate_OnClick"/> <MenuItem x:Name="MenuItemCreateUpdate" Header="{x:Static langRes:GeneralLangRes.CreateUpdate}" Click="MenuItemCreateUpdate_OnClick"/>
<MenuItem x:Name="MenuItemRemoveUpdate" Header="{x:Static langRes:GeneralLangRes.RemoveUpdate}" Click="MenuItemRemoveUpdate_OnClick"/> <MenuItem x:Name="MenuItemRemoveUpdate" Header="{x:Static langRes:GeneralLangRes.RemoveUpdate}" Click="MenuItemRemoveUpdate_OnClick"/>
</MenuFlyout.Items> </pilz:HeaderMenuItem.Items>
</MenuFlyout> </pilz:HeaderMenuItem>
</pilz:ImageButton.Flyout>
</pilz:ImageButton>
<!-- Button: Tools --> <!-- MenuItem: Workspace -->
<pilz:ImageButton <pilz:HeaderMenuItem
x:Name="ButtonTools" x:Name="MenuItemTools"
Text="{x:Static langRes:GeneralLangRes.Tools}" HeaderText="{x:Static langRes:GeneralLangRes.Tools}"/>
Background="Transparent"/> </Menu.Items>
</StackPanel> </Menu>
<!-- TreeView: Workspace --> <!-- TreeView: Workspace -->
<ScrollViewer <ScrollViewer
@@ -82,6 +71,7 @@
<StackPanel> <StackPanel>
<TreeView <TreeView
x:Name="TreeViewWorkspace"
ItemsSource="{Binding CurrentTreeNodes}" ItemsSource="{Binding CurrentTreeNodes}"
SelectedItem="{Binding SelectedTreeNode}"> SelectedItem="{Binding SelectedTreeNode}">
@@ -109,6 +99,20 @@
Orientation="Horizontal" Orientation="Horizontal"
VerticalAlignment="Center"> VerticalAlignment="Center">
<!-- Menu: Actions -->
<Menu>
<Menu.Items>
<pilz:HeaderMenuItem x:Name="MenuItemAddAction" HeaderText="{x:Static langRes:GeneralLangRes.Add}" Click="ButtonAddAction_OnClick"/>
<pilz:HeaderMenuItem x:Name="MenuItemRemoveAction" HeaderText="{x:Static langRes:GeneralLangRes.Remove}" Click="ButtonRemoveAction_OnClick"/>
</Menu.Items>
</Menu>
<!-- TextBox: Search -->
<TextBox
Width="200"
Watermark="{x:Static langRes:GeneralLangRes.Search}"
Text="{Binding CurrentGridRows.SearchText}"/>
<!-- Panel: Menu --> <!-- Panel: Menu -->
<ContentControl <ContentControl
Content="{Binding SelectedTreeNode}"> Content="{Binding SelectedTreeNode}">
@@ -118,33 +122,16 @@
DataType="mainWindow:ActionSetTreeNode"> DataType="mainWindow:ActionSetTreeNode">
<StackPanel <StackPanel
Orientation="Horizontal"> Orientation="Horizontal"
Spacing="6">
<!-- Button: Add action -->
<pilz:ImageButton
x:Name="ButtonAddAction"
Text="{x:Static langRes:GeneralLangRes.Add}"
ImageSource="{x:Static local:MainWindow.ButtonImageAddAction}"
Background="Transparent"
Click="ButtonAddAction_OnClick"/>
<!-- Button: Remove action -->
<pilz:ImageButton
x:Name="ButtonRemoveAction"
Text="{x:Static langRes:GeneralLangRes.Remove}"
ImageSource="{x:Static local:MainWindow.ButtonImageRemoveAction}"
Background="Transparent"
Click="ButtonRemoveAction_OnClick"/>
<!-- TextBox: Version --> <!-- TextBox: Version -->
<TextBox <TextBox
Margin="3, 0, 3, 0"
Width="100" Width="100"
Text="{Binding Version}"/> Text="{Binding Version}"/>
<!-- CheckBox: Is public --> <!-- CheckBox: Is public -->
<CheckBox <CheckBox
Margin="3, 0, 3, 0"
Content="{x:Static langRes:GeneralLangRes.Public}" Content="{x:Static langRes:GeneralLangRes.Public}"
IsChecked="{Binding IsPublic}"/> IsChecked="{Binding IsPublic}"/>
</StackPanel> </StackPanel>
@@ -172,7 +159,7 @@
Grid.Row="1" Grid.Row="1"
x:Name="DataGridActions" x:Name="DataGridActions"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
ItemsSource="{Binding CurrentGridRows}" ItemsSource="{Binding CurrentGridRows.View}"
SelectedItem="{Binding SelectedGridRow}"> SelectedItem="{Binding SelectedGridRow}">
<DataGrid.ContextMenu> <DataGrid.ContextMenu>
@@ -182,24 +169,31 @@
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn <DataGridTextColumn
Header="{x:Static langRes:GeneralLangRes.Id}" Header="{x:Static langRes:GeneralLangRes.Id}"
Binding="{Binding InheritedId}"/> Binding="{Binding InheritedId}"
Width="*"/>
<DataGridTextColumn <DataGridTextColumn
Header="{x:Static langRes:GeneralLangRes.Side}" Header="{x:Static langRes:GeneralLangRes.Name}"
Binding="{Binding InheritedSide}"/> Binding="{Binding InheritedName}"
Width="*"/>
<DataGridTextColumn <DataGridTextColumn
Header="{x:Static langRes:GeneralLangRes.UpdateType}" Header="{x:Static langRes:GeneralLangRes.UpdateType}"
Binding="{Binding InheritedUpdateType}" Binding="{Binding InheritedUpdateType}"
IsVisible="{Binding IsUpdate}"/> IsVisible="{Binding IsUpdate}"/>
<DataGridTextColumn
Header="{x:Static langRes:GeneralLangRes.Side}"
Binding="{Binding InheritedSide}"/>
<DataGridTextColumn <DataGridTextColumn
Header="{x:Static langRes:GeneralLangRes.SourceType}" Header="{x:Static langRes:GeneralLangRes.SourceType}"
Binding="{Binding InheritedSourceType}"/> Binding="{Binding InheritedSourceType}"/>
<DataGridTextColumn <DataGridTextColumn
Header="{x:Static langRes:GeneralLangRes.DestinationPath}" Header="{x:Static langRes:GeneralLangRes.DestinationPath}"
Binding="{Binding InheritedDestPath}"/> Binding="{Binding InheritedDestPath}"
Width="*"/>
<DataGridTemplateColumn <DataGridTemplateColumn
Header="{x:Static langRes:GeneralLangRes.State}"> Header="{x:Static langRes:GeneralLangRes.State}">
@@ -356,7 +350,7 @@
<!-- Source --> <!-- Source -->
<Grid <Grid
ColumnDefinitions="150,*" ColumnDefinitions="150,*"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"
RowSpacing="6" RowSpacing="6"
ColumnSpacing="6"> ColumnSpacing="6">
@@ -402,23 +396,30 @@
x:Name="TextBoxInstallActionSourceName" x:Name="TextBoxInstallActionSourceName"
Text="{Binding SourceName}"/> Text="{Binding SourceName}"/>
<!-- Source RegEx --> <!-- Source tag -->
<Label Grid.Row="5" Grid.Column="0" Content="{x:Static langRes:GeneralLangRes.SourceRegex}" Target="TextBoxInstallActionSourceRegEx"/> <Label Grid.Row="5" Grid.Column="0" Content="{x:Static langRes:GeneralLangRes.SourceTag}" Target="TextBoxInstallActionSourceTag"/>
<TextBox <TextBox
Grid.Row="5" Grid.Column="1" Grid.Row="5" Grid.Column="1"
x:Name="TextBoxInstallActionSourceTag"
Text="{Binding SourceTag}"/>
<!-- Source RegEx -->
<Label Grid.Row="6" Grid.Column="0" Content="{x:Static langRes:GeneralLangRes.SourceRegex}" Target="TextBoxInstallActionSourceRegEx"/>
<TextBox
Grid.Row="6" Grid.Column="1"
x:Name="TextBoxInstallActionSourceRegEx" x:Name="TextBoxInstallActionSourceRegEx"
Text="{Binding SourceRegex}"/> Text="{Binding SourceRegex}"/>
<!-- Source url --> <!-- Source url -->
<Label Grid.Row="6" Grid.Column="0" Content="{x:Static langRes:GeneralLangRes.SourceUrl}" Target="TextBoxInstallActionSourceUrl"/> <Label Grid.Row="7" Grid.Column="0" Content="{x:Static langRes:GeneralLangRes.SourceUrl}" Target="TextBoxInstallActionSourceUrl"/>
<TextBox <TextBox
Grid.Row="6" Grid.Column="1" Grid.Row="7" Grid.Column="1"
x:Name="TextBoxInstallActionSourceUrl" Text="{Binding SourceUrl}"/> x:Name="TextBoxInstallActionSourceUrl" Text="{Binding SourceUrl}"/>
<!-- Zip archive path --> <!-- Zip archive path -->
<Label Grid.Row="7" Grid.Column="0" Content="{x:Static langRes:GeneralLangRes.ZipArchivePath}" Target="TextBoxInstallActionZipArchivePath"/> <Label Grid.Row="8" Grid.Column="0" Content="{x:Static langRes:GeneralLangRes.ZipArchivePath}" Target="TextBoxInstallActionZipArchivePath"/>
<TextBox <TextBox
Grid.Row="7" Grid.Column="1" Grid.Row="8" Grid.Column="1"
x:Name="TextBoxInstallActionZipArchivePath" x:Name="TextBoxInstallActionZipArchivePath"
Text="{Binding ZipPath}"/> Text="{Binding ZipPath}"/>
</Grid> </Grid>

View File

@@ -2,7 +2,7 @@ using System.Reflection;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media; using DynamicData;
using ModpackUpdater.Apps.Manager.Api; using ModpackUpdater.Apps.Manager.Api;
using ModpackUpdater.Apps.Manager.Api.Model; using ModpackUpdater.Apps.Manager.Api.Model;
using ModpackUpdater.Apps.Manager.Api.Plugins.Features; using ModpackUpdater.Apps.Manager.Api.Plugins.Features;
@@ -10,6 +10,7 @@ using ModpackUpdater.Apps.Manager.Api.Plugins.Params;
using ModpackUpdater.Apps.Manager.Settings; using ModpackUpdater.Apps.Manager.Settings;
using ModpackUpdater.Apps.Manager.Ui.Models.MainWindow; using ModpackUpdater.Apps.Manager.Ui.Models.MainWindow;
using Pilz.Extensions; using Pilz.Extensions;
using Pilz.Extensions.Collections;
using Pilz.Features; using Pilz.Features;
using Pilz.UI.AvaloniaUI.Features; using Pilz.UI.AvaloniaUI.Features;
using Pilz.UI.Symbols; using Pilz.UI.Symbols;
@@ -18,9 +19,6 @@ namespace ModpackUpdater.Apps.Manager.Ui;
public partial class MainWindow : Window, IMainApi public partial class MainWindow : Window, IMainApi
{ {
public static IImage? ButtonImageAddAction => AppGlobals.Symbols.GetImageSource(AppSymbols.add);
public static IImage? ButtonImageRemoveAction => AppGlobals.Symbols.GetImageSource(AppSymbols.remove);
private WorkspaceFeature? curWs; private WorkspaceFeature? curWs;
public MainWindowViewModel Model { get; } = new(); public MainWindowViewModel Model { get; } = new();
@@ -32,19 +30,21 @@ public partial class MainWindow : Window, IMainApi
{ {
InitializeComponent(); InitializeComponent();
Title = $"{Title} (v{Assembly.GetExecutingAssembly().GetAppVersion().ToShortString()})"; Title = $"{Title} (v{Assembly.GetExecutingAssembly().GetAppVersion().ToShortHumanString()})";
GridMain.DataContext = Model; GridMain.DataContext = Model;
ButtonWorkspace.ImageSource = AppGlobals.Symbols.GetImageSource(AppSymbols.workspace); MenuItemWorkspace.Icon = AppGlobals.Symbols.GetImage(AppSymbols.workspace, SymbolSize.Small);
MenuItemWorkspacePreferences.Icon = AppGlobals.Symbols.GetImage(AppSymbols.settings, SymbolSize.Small); MenuItemWorkspacePreferences.Icon = AppGlobals.Symbols.GetImage(AppSymbols.settings, SymbolSize.Small);
MenuItemSaveWorkspace.Icon = AppGlobals.Symbols.GetImage(AppSymbols.save, SymbolSize.Small); MenuItemSaveWorkspace.Icon = AppGlobals.Symbols.GetImage(AppSymbols.save, SymbolSize.Small);
MenuItemNewWorkspace.Icon = AppGlobals.Symbols.GetImage(AppSymbols.new_window, SymbolSize.Small); MenuItemNewWorkspace.Icon = AppGlobals.Symbols.GetImage(AppSymbols.new_window, SymbolSize.Small);
MenuItemRecentWorkspaces.Icon = AppGlobals.Symbols.GetImage(AppSymbols.time_machine, SymbolSize.Small); MenuItemRecentWorkspaces.Icon = AppGlobals.Symbols.GetImage(AppSymbols.time_machine, SymbolSize.Small);
ButtonUpdate.ImageSource = AppGlobals.Symbols.GetImageSource(AppSymbols.update_done); MenuItemUpdate.Icon = AppGlobals.Symbols.GetImage(AppSymbols.update_done, SymbolSize.Small);
ButtonTools.ImageSource = AppGlobals.Symbols.GetImageSource(AppSymbols.tools); MenuItemTools.Icon = AppGlobals.Symbols.GetImage(AppSymbols.tools, SymbolSize.Small);
MenuItemCreateUpdate.Icon = AppGlobals.Symbols.GetImage(AppSymbols.add, SymbolSize.Small); MenuItemCreateUpdate.Icon = AppGlobals.Symbols.GetImage(AppSymbols.add, SymbolSize.Small);
MenuItemRemoveUpdate.Icon = AppGlobals.Symbols.GetImage(AppSymbols.remove, SymbolSize.Small); MenuItemRemoveUpdate.Icon = AppGlobals.Symbols.GetImage(AppSymbols.remove, SymbolSize.Small);
MenuItemAddAction.HeaderIcon = AppGlobals.Symbols.GetImage(AppSymbols.add, SymbolSize.Small);
MenuItemRemoveAction.HeaderIcon = AppGlobals.Symbols.GetImage(AppSymbols.remove, SymbolSize.Small);
ImageUpdate.Source = AppGlobals.Symbols.GetImageSource(AppSymbols.update_done); ImageUpdate.Source = AppGlobals.Symbols.GetImageSource(AppSymbols.update_done);
ImageMetadata.Source = AppGlobals.Symbols.GetImageSource(AppSymbols.show_property); ImageMetadata.Source = AppGlobals.Symbols.GetImageSource(AppSymbols.show_property);
ImageGeneral.Source = AppGlobals.Symbols.GetImageSource(AppSymbols.normal_screen); ImageGeneral.Source = AppGlobals.Symbols.GetImageSource(AppSymbols.normal_screen);
@@ -59,9 +59,7 @@ public partial class MainWindow : Window, IMainApi
customClickHandler: MenuItemActionItem_Click, customClickHandler: MenuItemActionItem_Click,
insertPrioSplitters: true); insertPrioSplitters: true);
var menuFlyoutTools = new MenuFlyout(); PluginFeatureController.Instance.Functions.Get(FeatureTypes.Tools).InsertItemsTo(MenuItemTools.Items,
ButtonTools.Flyout = menuFlyoutTools;
PluginFeatureController.Instance.Functions.Get(FeatureTypes.Tools).InsertItemsTo(menuFlyoutTools.Items,
customClickHandler: MenuItemToolsItem_Click, customClickHandler: MenuItemToolsItem_Click,
insertPrioSplitters: true); insertPrioSplitters: true);
} }
@@ -118,18 +116,18 @@ public partial class MainWindow : Window, IMainApi
private static void AddToRecentFiles(IWorkspace workspace) private static void AddToRecentFiles(IWorkspace workspace)
{ {
var settings = Program.Settings.Get<WorkspaceSettings>(); var settings = Program.Settings.Get<WorkspaceSettings>();
settings.Workspaces.RemoveAll(n => n == workspace.Config);
settings.Workspaces.Remove(workspace.Config);
settings.Workspaces.Insert(0, workspace.Config); settings.Workspaces.Insert(0, workspace.Config);
settings.Workspaces.Skip(20).ForEach(n => settings.Workspaces.Remove(n));
while (settings.Workspaces.Count > 20)
settings.Workspaces.RemoveAt(20);
} }
private async void Window_OnLoaded(object? sender, RoutedEventArgs e) private async void Window_OnLoaded(object? sender, RoutedEventArgs e)
{ {
var updater = new AppUpdates(Program.UpdateUrl, this); var updater = new AppUpdates("manager", this)
updater.OnDownloadProgramUpdate += (o, args) => Model.Progress.Start(); {
UsePopups = true,
};
updater.OnDownloadProgramUpdate += (_, _) => Model.Progress.Start();
await updater.UpdateApp(); await updater.UpdateApp();
Model.Progress.Stop(); Model.Progress.Stop();
@@ -203,7 +201,10 @@ public partial class MainWindow : Window, IMainApi
Version = new(), Version = new(),
}; };
Model.CurrentWorkspace.UpdateInfos.Updates.Insert(0, update); Model.CurrentWorkspace.UpdateInfos.Updates.Insert(0, update);
nodeUpdates.Nodes.Insert(0, new ActionSetTreeNode(update)); var item = new ActionSetTreeNode(update);
nodeUpdates.Nodes.Insert(0, item);
TreeViewWorkspace.SelectedItem = item;
TreeViewWorkspace.ScrollIntoView(item);
} }
private void MenuItemRemoveUpdate_OnClick(object? sender, RoutedEventArgs e) private void MenuItemRemoveUpdate_OnClick(object? sender, RoutedEventArgs e)
@@ -239,7 +240,10 @@ public partial class MainWindow : Window, IMainApi
return; return;
} }
rows.Add(new MainWindowGridRow(action, rootInfos)); var row = new MainWindowGridRow(action, rootInfos);
rows.List.Add(row);
DataGridActions.SelectedItem = row;
DataGridActions.ScrollIntoView(row, null);
} }
private void ButtonRemoveAction_OnClick(object? sender, RoutedEventArgs e) private void ButtonRemoveAction_OnClick(object? sender, RoutedEventArgs e)
@@ -262,6 +266,6 @@ public partial class MainWindow : Window, IMainApi
return; return;
} }
rows.Remove(row); rows.List.Remove(row);
} }
} }

View File

@@ -0,0 +1,34 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using DynamicData;
namespace ModpackUpdater.Apps.Manager.Ui.Models;
public class DynamicDataView<T> : INotifyPropertyChanged where T : notnull
{
public event PropertyChangedEventHandler? PropertyChanged;
private string? searchText;
private readonly Subject<string?> searchTextSubject = new();
public SourceList<T> List { get; } = new();
public ReadOnlyObservableCollection<T> View { get; }
public DynamicDataView(Func<string?, Func<T, bool>> predicate)
{
List.Connect()
.Filter(searchTextSubject/*.Throttle(TimeSpan.FromMilliseconds(250))*/.Select(predicate))
.Bind(out var view)
.Subscribe();
searchTextSubject?.OnNext(searchText);
View = view;
}
public string? SearchText
{
get => searchText;
set => searchTextSubject.OnNext(searchText = value);
}
}

View File

@@ -23,15 +23,22 @@ public class MainWindowGridRow(InstallAction action, IActionSet baseActions) : I
[DependsOn(nameof(Id), nameof(InheritFrom))] [DependsOn(nameof(Id), nameof(InheritFrom))]
public string? InheritedId => action is UpdateAction ua && !string.IsNullOrWhiteSpace(ua.InheritFrom) ? ua.InheritFrom : action.Id; public string? InheritedId => action is UpdateAction ua && !string.IsNullOrWhiteSpace(ua.InheritFrom) ? ua.InheritFrom : action.Id;
[DependsOn(nameof(Side), nameof(InheritedId), nameof(Id))] [DependsOn(nameof(Side), nameof(InheritedId), nameof(Id))]
public string InheritedSide => Sides[Inherited.Side]; public string InheritedSide => Sides[Inherited.Side];
[DependsOn(nameof(UpdateType), nameof(InheritedId), nameof(Id))] [DependsOn(nameof(UpdateType), nameof(InheritedId), nameof(Id))]
public string InheritedUpdateType => action is UpdateAction ua ? UpdateActionTypes[ua.Type] : string.Empty; public string InheritedUpdateType => action is UpdateAction ua ? UpdateActionTypes[ua.Type] : string.Empty;
[DependsOn(nameof(SourceType), nameof(InheritedId), nameof(Id))] [DependsOn(nameof(SourceType), nameof(InheritedId), nameof(Id))]
public string InheritedSourceType => SourceTypes[Inherited.SourceType]; public string InheritedSourceType => SourceTypes[Inherited.SourceType];
[DependsOn(nameof(DestPath), nameof(InheritedId), nameof(Id))] [DependsOn(nameof(DestPath), nameof(InheritedId), nameof(Id))]
public string? InheritedDestPath => Inherited.DestPath; public string? InheritedDestPath => Inherited.DestPath;
[DependsOn(nameof(Name), nameof(InheritedId), nameof(Id))]
public string? InheritedName => Inherited.Name;
public string? Id public string? Id
{ {
get => action.Id; get => action.Id;

View File

@@ -11,13 +11,13 @@ public class MainWindowViewModel : INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
private ObservableCollection<MainWindowTreeNode>? currentTreeNodes; private ObservableCollection<MainWindowTreeNode>? currentTreeNodes;
private ObservableCollection<MainWindowGridRow>? currentGridRows;
private MainWindowTreeNode? selectedTreeNode; private MainWindowTreeNode? selectedTreeNode;
private IWorkspace? currentWorkspace; private IWorkspace? currentWorkspace;
public bool IsUpdate => selectedTreeNode is ActionSetTreeNode node && node.Infos is UpdateInfo; public bool IsUpdate => selectedTreeNode is ActionSetTreeNode node && node.Infos is UpdateInfo;
public ProgressInfos Progress { get; } = new(); public ProgressInfos Progress { get; } = new();
public MainWindowGridRow? SelectedGridRow { get; set; } public MainWindowGridRow? SelectedGridRow { get; set; }
public DynamicDataView<MainWindowGridRow> CurrentGridRows { get; } = new(FilterGridRows);
[AlsoNotifyFor(nameof(CurrentTreeNodes))] [AlsoNotifyFor(nameof(CurrentTreeNodes))]
public IWorkspace? CurrentWorkspace public IWorkspace? CurrentWorkspace
@@ -48,24 +48,38 @@ public class MainWindowViewModel : INotifyPropertyChanged
} }
} }
[AlsoNotifyFor(nameof(CurrentGridRows))]
public MainWindowTreeNode? SelectedTreeNode public MainWindowTreeNode? SelectedTreeNode
{ {
get => selectedTreeNode; get => selectedTreeNode;
set set
{ {
currentGridRows = null;
selectedTreeNode = value; selectedTreeNode = value;
CurrentGridRows.List.Edit(list =>
{
list.Clear();
if (CurrentWorkspace?.InstallInfos != null && selectedTreeNode is ActionSetTreeNode node)
list.AddRange(node.Infos.Actions.Select(n => new MainWindowGridRow(n, CurrentWorkspace.InstallInfos)));
});
} }
} }
public ObservableCollection<MainWindowGridRow>? CurrentGridRows private static Func<MainWindowGridRow, bool> FilterGridRows(string? searchText)
{ {
get return n => string.IsNullOrWhiteSpace(searchText)
{ || (!string.IsNullOrWhiteSpace(n.Name) && n.Name.Contains(searchText))
if (currentGridRows == null && CurrentWorkspace?.InstallInfos != null && selectedTreeNode is ActionSetTreeNode node) || (!string.IsNullOrWhiteSpace(n.InheritFrom) && n.InheritFrom.Contains(searchText))
currentGridRows = [.. node.Infos.Actions.Select(n => new MainWindowGridRow(n, CurrentWorkspace.InstallInfos))]; || (!string.IsNullOrWhiteSpace(n.InheritedDestPath) && n.InheritedDestPath.Contains(searchText))
return currentGridRows; || (!string.IsNullOrWhiteSpace(n.InheritedId) && n.InheritedId.Contains(searchText))
} || (!string.IsNullOrWhiteSpace(n.InheritedSide) && n.InheritedSide.Contains(searchText))
|| (!string.IsNullOrWhiteSpace(n.InheritedSourceType) && n.InheritedSourceType.Contains(searchText))
|| (!string.IsNullOrWhiteSpace(n.InheritedUpdateType) && n.InheritedUpdateType.Contains(searchText))
|| (!string.IsNullOrWhiteSpace(n.SourceName) && n.SourceName.Contains(searchText))
|| (!string.IsNullOrWhiteSpace(n.SourceOwner) && n.SourceOwner.Contains(searchText))
|| (!string.IsNullOrWhiteSpace(n.SourceRegex) && n.SourceRegex.Contains(searchText))
|| (!string.IsNullOrWhiteSpace(n.SourceTag) && n.SourceTag.Contains(searchText))
|| (!string.IsNullOrWhiteSpace(n.SourceUrl) && n.SourceUrl.Contains(searchText))
|| (!string.IsNullOrWhiteSpace(n.SrcPath) && n.SrcPath.Contains(searchText))
|| (!string.IsNullOrWhiteSpace(n.Website) && n.Website.Contains(searchText))
;
} }
} }

View File

@@ -1,5 +1,9 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using DynamicData;
using PropertyChanged; using PropertyChanged;
namespace ModpackUpdater.Apps.Manager.Ui.Models.UpdatesCollectorViewMode; namespace ModpackUpdater.Apps.Manager.Ui.Models.UpdatesCollectorViewMode;
@@ -9,6 +13,10 @@ public class UpdatesCollectorViewModel : INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
public ProgressInfos Progress { get; } = new(); public ProgressInfos Progress { get; } = new();
public string? SearchText { get; set; } public DynamicDataView<ModUpdateInfo> Updates { get; } = new(FilterUpdates);
public ObservableCollection<ModUpdateInfo> Updates { get; } = [];
private static Func<ModUpdateInfo, bool> FilterUpdates(string? searchText)
{
return n => string.IsNullOrWhiteSpace(searchText) || (n.Origin.Name != null && n.Origin.Name.Contains(searchText, StringComparison.InvariantCultureIgnoreCase));
}
} }

View File

@@ -25,16 +25,15 @@
<!-- TextBox: Search --> <!-- TextBox: Search -->
<TextBox <TextBox
Grid.Row="0" Grid.Row="0"
Watermark="Search" Watermark="{x:Static langRes:GeneralLangRes.Search}"
Text="{Binding SearchText}" Text="{Binding Updates.SearchText}"/>
TextChanged="TextBoxSearch_OnTextChanged"/>
<!-- ScrollViewer: Updates --> <!-- ScrollViewer: Updates -->
<ScrollViewer <ScrollViewer
Grid.Row="1"> Grid.Row="1">
<ItemsControl <ItemsControl
ItemsSource="{Binding Updates}"> ItemsSource="{Binding Updates.View}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
@@ -43,8 +42,7 @@
ColumnDefinitions="20*,20*,20*,Auto" ColumnDefinitions="20*,20*,20*,Auto"
ColumnSpacing="6" ColumnSpacing="6"
RowSpacing="6" RowSpacing="6"
Margin="3" Margin="3">
IsVisible="{Binding Visible}">
<!-- Label: Name --> <!-- Label: Name -->
<TextBlock <TextBlock

View File

@@ -1,5 +1,6 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using DynamicData;
using ModpackUpdater.Apps.Manager.Api.Model; using ModpackUpdater.Apps.Manager.Api.Model;
using ModpackUpdater.Apps.Manager.Ui.Models.UpdatesCollectorViewMode; using ModpackUpdater.Apps.Manager.Ui.Models.UpdatesCollectorViewMode;
using ModpackUpdater.Manager; using ModpackUpdater.Manager;
@@ -36,7 +37,7 @@ public partial class UpdatesCollectorView : AvaloniaFlyoutBase
if (updates == null || updates.Length == 0 || updates[0].Tag == action.SourceTag) if (updates == null || updates.Length == 0 || updates[0].Tag == action.SourceTag)
continue; continue;
Model.Updates.Add(new(updates, action)); Model.Updates.List.Add(new(updates, action));
if (IsClosed) if (IsClosed)
break; break;
@@ -47,7 +48,7 @@ public partial class UpdatesCollectorView : AvaloniaFlyoutBase
protected override object GetResult() protected override object GetResult()
{ {
return new ModUpdates(Model.Updates); return new ModUpdates([.. Model.Updates.List.Items]);
} }
private async void Me_OnLoaded(object? sender, RoutedEventArgs e) private async void Me_OnLoaded(object? sender, RoutedEventArgs e)
@@ -55,18 +56,9 @@ public partial class UpdatesCollectorView : AvaloniaFlyoutBase
await FindUpdates(); await FindUpdates();
} }
private void TextBoxSearch_OnTextChanged(object? sender, TextChangedEventArgs e)
{
var searchString = Model.SearchText?.Trim().ToLowerInvariant();
var hasNoSearch = string.IsNullOrWhiteSpace(searchString);
foreach (var item in Model.Updates)
item.Visible = hasNoSearch || (item.Origin.Name != null && item.Origin.Name.Contains(searchString!, StringComparison.InvariantCultureIgnoreCase));
}
private void ButtonRemoveUpdate_Click(object? sender, RoutedEventArgs e) private void ButtonRemoveUpdate_Click(object? sender, RoutedEventArgs e)
{ {
if (sender is Button button && button.DataContext is ModUpdateInfo update) if (sender is Button button && button.DataContext is ModUpdateInfo update)
Model.Updates.Remove(update); Model.Updates.List.Remove(update);
} }
} }

View File

@@ -1,12 +0,0 @@
pupnet -y -v "$1[1]" -r linux-x64 -k appimage
#pupnet -y -v "$1[1]" -r linux-x64 -k flatpak -p DefineConstants=DISABLE_UPDATE
#pupnet -y -v "$1[1]"-r linux-x64 -k deb -p DefineConstants=DISABLE_UPDATE
#pupnet -y -v "$1[1]"-r linux-x64 -k rpm -p DefineConstants=DISABLE_UPDATE
pupnet -y -v "$1[1]" -r linux-arm64 -k appimage
#pupnet -y -v "$1[1]" -r linux-arm64 -k flatpak -p DefineConstants=DISABLE_UPDATE
#pupnet -y -v "$1[1]"-r linux-arm64 -k deb -p DefineConstants=DISABLE_UPDATE
#pupnet -y -v "$1[1]"-r linux-arm64 -k rpm -p DefineConstants=DISABLE_UPDATE
pupnet -y -v "$1[1]" -r win-x64 -k zip
pupnet -y -v "$1[1]" -r win-arm64 -k zip
pupnet -y -v "$1[1]" -r osx-x64 -k zip
pupnet -y -v "$1[1]" -r osx-arm64 -k zip

View File

@@ -10,13 +10,19 @@ using Pilz.Updating.Client;
namespace ModpackUpdater.Apps; namespace ModpackUpdater.Apps;
public class AppUpdates(string updateUrl, Window mainWindow) public class AppUpdates(string appShortName, Window mainWindow)
{ {
public const string UpdateUrl = "https://git.pilzinsel64.de/LITW-Refined/minecraft-modpack-updater/raw/branch/updates/updates.json";
public event EventHandler? OnDownloadProgramUpdate; public event EventHandler? OnDownloadProgramUpdate;
public bool UsePopups { get; set; }
public async Task UpdateApp() public async Task UpdateApp()
{ {
#if !DISABLE_UPDATE #if DISABLE_UPDATE
await Task.CompletedTask;
#else
try try
{ {
await UpdateAppCore(); await UpdateAppCore();
@@ -35,25 +41,33 @@ public class AppUpdates(string updateUrl, Window mainWindow)
return; return;
var myAppPath = EnvironmentEx.ProcessPath!; var myAppPath = EnvironmentEx.ProcessPath!;
var updater = new UpdateClient(updateUrl, Assembly.GetEntryAssembly()!.GetAppVersion(), AppChannel.Stable) var updater = new UpdateClient(UpdateUrl, Assembly.GetEntryAssembly()!.GetAppVersion(), AppChannel.Stable)
{ {
Distro = RuntimeInformationsEx.GetRuntimeIdentifier(), Distro = $"{appShortName}-{RuntimeInformationsEx.GetRuntimeIdentifier()}",
}; };
if (await updater.CheckForUpdate() is {} packageToInstall if (await updater.CheckForUpdate() is not { } packageToInstall || await AskForUpdate() != ButtonResult.Yes)
&& await MessageBoxManager.GetMessageBoxStandard(GeneralMsgBoxLangRes.UpdateAvailable_Title, GeneralMsgBoxLangRes.UpdateAvailable, ButtonEnum.YesNo, MsBox.Avalonia.Enums.Icon.Info).ShowWindowDialogAsync(mainWindow) == ButtonResult.Yes) return;
{
OnDownloadProgramUpdate?.Invoke(this, EventArgs.Empty); OnDownloadProgramUpdate?.Invoke(this, EventArgs.Empty);
mainWindow.IsEnabled = false; mainWindow.IsEnabled = false;
if (await updater.DownloadPackageAsync(packageToInstall)
&& await updater.InstallPackageAsync(packageToInstall, myAppPath)) if (await updater.DownloadPackageAsync(packageToInstall) && await updater.InstallPackageAsync(packageToInstall, myAppPath))
{ {
mainWindow.IsVisible = false; mainWindow.IsVisible = false;
await Process.Start(myAppPath).WaitForExitAsync(); await Process.Start(myAppPath).WaitForExitAsync();
Environment.Exit(0); Environment.Exit(0);
return; return;
} }
mainWindow.IsEnabled = true; mainWindow.IsEnabled = true;
} }
private Task<ButtonResult> AskForUpdate()
{
var msgBox = MessageBoxManager.GetMessageBoxStandard(GeneralMsgBoxLangRes.UpdateAvailable_Title, GeneralMsgBoxLangRes.UpdateAvailable, ButtonEnum.YesNo, Icon.Info);
if (UsePopups)
return msgBox.ShowAsPopupAsync(mainWindow);
return msgBox.ShowWindowDialogAsync(mainWindow);
} }
} }

View File

@@ -19,22 +19,22 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Pilz" Version="2.6.1" /> <PackageReference Include="Pilz" Version="2.7.8" />
<PackageReference Include="Pilz.UI" Version="3.1.4" /> <PackageReference Include="Pilz.UI" Version="3.1.5" />
<PackageReference Include="Pilz.UI.AvaloniaUI" Version="1.2.18" /> <PackageReference Include="Pilz.UI.AvaloniaUI" Version="1.2.21" />
<PackageReference Include="MessageBox.Avalonia" Version="3.3.0" /> <PackageReference Include="MessageBox.Avalonia" Version="3.3.1" />
<PackageReference Include="Avalonia" Version="11.3.8" /> <PackageReference Include="Avalonia" Version="11.3.10" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.8" /> <PackageReference Include="Avalonia.Desktop" Version="11.3.10" />
<PackageReference Include="Avalonia.Svg" Version="11.3.0" /> <PackageReference Include="Avalonia.Svg" Version="11.3.0" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.8" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.10" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.8" /> <PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.10" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.8"> <PackageReference Include="Avalonia.Diagnostics" Version="11.3.10">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets> <IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets> <PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Pilz.Updating" Version="4.3.5" /> <PackageReference Include="Pilz.Updating" Version="4.3.6" />
<PackageReference Include="Pilz.Updating.Client" Version="4.4.6" /> <PackageReference Include="Pilz.Updating.Client" Version="4.4.8" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -12,7 +12,10 @@ public class ModpackFactory
{ {
Credentials = new Credentials("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"), Credentials = new Credentials("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"),
}; };
private readonly ApiClient curseForge = new("$2a$10$pE4dD09gmr7IcOe8hjWhleWWjXopJcDNpq1P9FlrDMCBw05pCyAXa", "pilzinsel64@gmx.de"); private readonly ApiClient curseForge = new("$2a$10$pE4dD09gmr7IcOe8hjWhleWWjXopJcDNpq1P9FlrDMCBw05pCyAXa", "pilzinsel64@gmx.de")
{
RequestTimeout = new TimeSpan(0, 0, 30),
};
private readonly ModrinthClient modrinth = new(new ModrinthClientConfig private readonly ModrinthClient modrinth = new(new ModrinthClientConfig
{ {
ModrinthToken = "mrp_zUlDSET5actMUdTU3FK242TXgvlWgaErSSEFuegNG7thLgkC50IiK2NCGOzW", ModrinthToken = "mrp_zUlDSET5actMUdTU3FK242TXgvlWgaErSSEFuegNG7thLgkC50IiK2NCGOzW",
@@ -70,7 +73,7 @@ public class ModpackFactory
{ {
var repo = await github.Repository.Get(action.SourceOwner, action.SourceName); var repo = await github.Repository.Get(action.SourceOwner, action.SourceName);
var releases = await github.Repository.Release.GetAll(repo.Id); var releases = await github.Repository.Release.GetAll(repo.Id);
return releases.Select(r => new ModVersionInfo(r.TagName, r.Name ?? r.TagName)).ToArray(); return releases.Select(r => new ModVersionInfo(string.IsNullOrWhiteSpace(r.Name) ? r.TagName : r.Name, r.TagName)).ToArray();
} }
catch catch
{ {

View File

@@ -1,81 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Unleash;
using Unleash.ClientFactory;
namespace ModpackUpdater.Manager;
public class ModpackFeatures(ModpackConfig modpackConfig)
{
private IUnleash? api;
private UnleashContext context;
private UnleashSettings settings;
public static string FeatureAllowExtas => "allow-extras";
~ModpackFeatures()
{
api?.Dispose();
}
public bool IsEnabled(string feature)
{
return IsEnabled(feature, null);
}
public bool IsEnabled(string feature, AppFeatureContext context)
{
return CheckFeature(feature, context);
}
public bool IsInvalid()
{
return string.IsNullOrWhiteSpace(modpackConfig.UnleashApiUrl) || string.IsNullOrWhiteSpace(modpackConfig.UnleashInstanceId);
}
[MemberNotNullWhen(true, nameof(api))]
private bool InitializeApi()
{
if (api != null
|| string.IsNullOrWhiteSpace(modpackConfig.UnleashApiUrl)
|| string.IsNullOrWhiteSpace(modpackConfig.UnleashInstanceId))
return api != null;
settings = new UnleashSettings
{
AppName = "Modpack Updater",
UnleashApi = new Uri(modpackConfig.UnleashApiUrl),
FetchTogglesInterval = TimeSpan.FromSeconds(0),
InstanceTag = modpackConfig.UnleashInstanceId,
};
api = new UnleashClientFactory().CreateClient(settings, synchronousInitialization: true);
return api != null;
}
private bool CheckFeature(string name, AppFeatureContext context)
{
return InitializeApi() && api.IsEnabled(name, GetContext(context));
}
private UnleashContext GetContext(AppFeatureContext ccontext)
{
context ??= new();
context.CurrentTime = DateTime.Now;
ccontext?.Apply(context);
return context;
}
}
public abstract class AppFeatureContext
{
public abstract void Apply(UnleashContext context);
}
public class AllowExtrasFeatureContext(ModpackInfo info) : AppFeatureContext
{
public override void Apply(UnleashContext context)
{
context.UserId = info.ExtrasKey;
}
}

View File

@@ -169,9 +169,7 @@ public class ModpackInstaller(ModpackConfig updateConfig, ModpackInfo modpackInf
foreach (InstallAction iaction in checkResult.Actions) foreach (InstallAction iaction in checkResult.Actions)
{ {
var destFilePath = iaction.GetDestPath(modpackInfo.LocalPath); var destFilePath = iaction.GetDestPath(modpackInfo.LocalPath);
var sourceUrl = updateConfig.PreferDirectLinks && !string.IsNullOrWhiteSpace(iaction.SourceUrl) var sourceUrl = iaction.GetSourceUrl(checkResult.LatestVersion, overwriteVersion: OverwriteVersion);
? iaction.GetSourceUrl(checkResult.LatestVersion, overwriteVersion: OverwriteVersion)
: await factory.ResolveSourceUrl(iaction, targetVersion: checkResult.LatestVersion, overwriteVersion: OverwriteVersion);
if (iaction is UpdateAction uaction) if (iaction is UpdateAction uaction)
{ {

View File

@@ -9,11 +9,11 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Castle.Core" Version="5.2.1" /> <PackageReference Include="Castle.Core" Version="5.2.1" />
<PackageReference Include="CurseForge.APIClient" Version="4.2.0" /> <PackageReference Include="CurseForge.APIClient" Version="4.2.0" />
<PackageReference Include="LaunchDarkly.EventSource" Version="5.2.1" /> <PackageReference Include="LaunchDarkly.EventSource" Version="5.3.0" />
<PackageReference Include="Modrinth.Net" Version="3.6.0" /> <PackageReference Include="Modrinth.Net" Version="3.6.0" />
<PackageReference Include="Octokit" Version="14.0.0" /> <PackageReference Include="Octokit" Version="14.0.0" />
<PackageReference Include="System.IO.Compression.ZipFile" Version="4.3.0" /> <PackageReference Include="System.IO.Compression.ZipFile" Version="4.3.0" />
<PackageReference Include="Unleash.Client" Version="5.5.3" /> <PackageReference Include="Unleash.Client" Version="5.6.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -9,9 +9,7 @@ public class ModpackConfig
public string? Name { get; set; } public string? Name { get; set; }
public string? UpdateUrl { get; set; } public string? UpdateUrl { get; set; }
public string? InstallUrl { get; set; } public string? InstallUrl { get; set; }
public string? UnleashApiUrl { get; set; } public List<string> ExtrasKeys { get; } = [];
public string? UnleashInstanceId { get; set; }
public bool PreferDirectLinks { get; set; }
public string? MinecraftVersion { get; set; } public string? MinecraftVersion { get; set; }
public string? RefTag { get; set; } public string? RefTag { get; set; }

View File

@@ -6,8 +6,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Pilz.Cryptography" Version="2.1.2" /> <PackageReference Include="Pilz.Cryptography" Version="2.1.3" />
<PackageReference Include="Pilz.Extensions" Version="2.1.1" /> <PackageReference Include="Pilz.Extensions" Version="2.1.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<packageSources> <packageSources>
<add key="Pilz" value="https://git.pilzinsel64.de/api/v4/projects/6/packages/nuget/index.json" /> <add key="Pilz" value="https://git.pilzinsel64.de/api/packages/Pilz.NET/nuget/index.json" />
<add key="Pilz.Updating" value="https://git.pilzinsel64.de/api/v4/projects/8/packages/nuget/index.json" />
</packageSources> </packageSources>
</configuration> </configuration>

View File

@@ -1,3 +1,3 @@
using Pilz; using Pilz;
[assembly: AssemblyAppVersion(AssemblyAppVersionAttribute.EntryAssemblyVersionKey)] [assembly: AssemblyAppVersion]

1
publish-scripts Submodule

Submodule publish-scripts added at fd658bd9d1

View File

@@ -1,6 +0,0 @@
#!/bin/bash
set -e
for dir in ModpackUpdater.Apps.Client ModpackUpdater.Apps.Client.Gui ModpackUpdater.Apps.Manager; do
( cd "$dir" && ./publish.sh "$1" )
done